diff --git a/examples/demo/package.json b/examples/demo/package.json index 5c9649d..680bcb3 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "private": true, "description": "", - "author": "Hugo Dias (hugodias.me)", + "author": "Steven Vandevelde (tokono.ma)", "license": "MIT", "keywords": [], "main": "src/main.jsx", @@ -15,7 +15,7 @@ "serve": "vite preview --port 3000" }, "dependencies": { - "package1": "*" + "@wnfs-wg/nest": "file:../../packages/nest" }, "devDependencies": { "@babel/core": "^7.23.5", @@ -24,11 +24,7 @@ "vite": "^5.0.7" }, "eslintConfig": { - "extends": [ - "@fission-codes" - ], - "ignorePatterns": [ - "dist" - ] + "extends": ["@fission-codes"], + "ignorePatterns": ["dist"] } } diff --git a/examples/demo/src/main.ts b/examples/demo/src/main.ts index a9e1933..d643d76 100644 --- a/examples/demo/src/main.ts +++ b/examples/demo/src/main.ts @@ -1,4 +1,4 @@ -import { randomBytes } from 'package1' +import { randomBytes } from '@wnfs-wg/nest' // eslint-disable-next-line no-console console.log(randomBytes(10)) diff --git a/packages/nest/package.json b/packages/nest/package.json index ada914f..f7a8762 100644 --- a/packages/nest/package.json +++ b/packages/nest/package.json @@ -2,7 +2,7 @@ "name": "@wnfs-wg/nest", "type": "module", "version": "0.1.0", - "description": "Example package description.", + "description": "A utility layer around the `wnfs` package.", "author": "Steven Vandevelde (tokono.ma)", "license": "(Apache-2.0 AND MIT)", "homepage": "https://github.com/wnfs-wg/nest/tree/main/packages/nest", diff --git a/packages/nest/readme.md b/packages/nest/readme.md index e0d8dbf..9c6ebca 100644 --- a/packages/nest/readme.md +++ b/packages/nest/readme.md @@ -3,6 +3,8 @@ [![npm (scoped)](https://img.shields.io/npm/v/%40fission-codes/eslint-config)](https://www.npmjs.com/package/@fission-codes/eslint-config) [![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/fission-codes/stack/eslint-config.yml)](https://github.com/fission-codes/stack/actions/workflows/eslint-config.yml) +A layer around the `wnfs` package that provides a `FileSystem` class, a root tree, mounts, transactions and some other essentials. + ## Features - A file system class that allows for an easy-to-use mutable API. @@ -22,8 +24,67 @@ pnpm install @wnfs-wg/nest ## Usage -```js -import { module } from '@wnfs-wg/nest' +```ts +import { FileSystem, Path } from '@wnfs-wg/nest' + +// Provide some block store of the `Blockstore` type from the `interface-blockstore` package +import { MemoryBlockstore } from 'blockstore-core/memory' +``` + +Scenario 1: +🚀 Create a new file system, create a new file and read it back. + +```ts +const fs = await FileSystem.create({ blockstore: new MemoryBlockstore() }) + +await fs.write( + Path.file('private', 'file'), + 'utf8', + '🪺' +) + +const contents = await fs.read(Path.file('private', 'file'), 'utf8') +``` + +Scenario 2: +🛰️ Listen to commit and/or publish events. + +_A commit is a (optionally verified) modification to the file system, +and publishes are the debounced events resulting from the commits._ + +This will allow us the store the latest state of our file system, +for this we need what we call the data root. This is the top-level CID +of our root tree, the pointer to our file system. + +```ts +let fsPointer: CID = await fs.calculateDataRoot() + +// When we make a modification to the file system a verification is performed. +await fs.write( + Path.file('private', 'file'), + 'utf8', + '🪺' +) + +// If the commit is approved, the changes are reflected in the file system and +// the `commit` and `publish` events are emitted. +fs.on('commit', ({ dataRoot, modifications }) => { + // Commit approved and performed ✅ +}) + +fs.on('publish', ({ dataRoot }) => { + // Commit approved and performed ✅ + // Debounced and delayed ✅ + fsPointer +}) +``` + +Scenario 3: +🧳 Load a file system from a previous pointer. + +```ts +// `blockstore` from scenario 1 & `fsPointer` from scenario 2 +const fs = await FileSystem.fromCID(fsPointer, { blockstore }) ``` ## Docs diff --git a/packages/nest/src/class.ts b/packages/nest/src/class.ts index 176d108..da8718f 100644 --- a/packages/nest/src/class.ts +++ b/packages/nest/src/class.ts @@ -119,6 +119,7 @@ export class FileSystem { static async fromCID(cid: CID, opts: Options): Promise { const { blockstore, onCommit, rootTreeClass, settleTimeBeforePublish } = opts + const rootTree = await (rootTreeClass ?? BasicRootTree).fromCID( blockstore, cid @@ -647,12 +648,10 @@ export class FileSystem { const dataRoot = await this.calculateDataRoot() // Emit events - for (const change of changes) { - await this.#eventEmitter.emit('local-change', { - dataRoot, - ...change, - }) - } + await this.#eventEmitter.emit('commit', { + dataRoot, + modifications: [...changes], + }) // Publish if ( diff --git a/packages/nest/src/events.ts b/packages/nest/src/events.ts index 915168b..8a030ed 100644 --- a/packages/nest/src/events.ts +++ b/packages/nest/src/events.ts @@ -1,13 +1,11 @@ import type { CID } from 'multiformats/cid' -import type { DistinctivePath, Partition, Partitioned } from './path.js' -import type { MutationType } from './types.js' +import type { Modification } from './types.js' // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type Events = { - 'local-change': { + commit: { dataRoot: CID - path: DistinctivePath> - type: MutationType + modifications: Modification[] } publish: { dataRoot: CID } } diff --git a/packages/nest/src/index.ts b/packages/nest/src/index.ts index 67e7d05..72b9f84 100644 --- a/packages/nest/src/index.ts +++ b/packages/nest/src/index.ts @@ -3,3 +3,5 @@ export * from './class.js' export * from './root-tree.js' export * from './root-tree/basic.js' export * from './types.js' + +export * as Path from './path.js' diff --git a/packages/nest/test/class.test.ts b/packages/nest/test/class.test.ts index 9e4de87..d95b889 100644 --- a/packages/nest/test/class.test.ts +++ b/packages/nest/test/class.test.ts @@ -53,11 +53,7 @@ describe('File System Class', () => { const privatePath = Path.file('private', 'nested-private', 'private.txt') const { contentCID } = await fs.write(publicPath, 'utf8', 'public') - const { _capsuleKey, dataRoot } = await fs.write( - privatePath, - 'utf8', - 'private' - ) + const { dataRoot } = await fs.write(privatePath, 'utf8', 'private') const contentBytes = await Unix.exportFile(contentCID, blockstore) @@ -140,7 +136,7 @@ describe('File System Class', () => { const path = Path.file('public', 'a') const bytes = new TextEncoder().encode('🚀') - const { _contentCID } = await fs.write(path, 'bytes', bytes) + await fs.write(path, 'bytes', bytes) assert.equal(await fs.read(path, 'utf8'), '🚀') await assertUnixFsFile({ blockstore }, fs, path, bytes) @@ -149,7 +145,7 @@ describe('File System Class', () => { it('writes and reads private files', async () => { const path = Path.file('private', 'a') - const { _capsuleKey } = await fs.write(path, 'json', { foo: 'bar', a: 1 }) + await fs.write(path, 'json', { foo: 'bar', a: 1 }) assert.deepEqual(await fs.read(path, 'json'), { foo: 'bar', a: 1 }) }) @@ -854,8 +850,8 @@ describe('File System Class', () => { const fromPath = Path.file('public', 'a', 'b', 'file') const toPath = Path.file('private', 'a', 'b', 'c', 'd', 'file') - const { _capsuleCID } = await fs.write(fromPath, 'utf8', '💃') - const { _capsuleKey } = await fs.move(fromPath, toPath) + await fs.write(fromPath, 'utf8', '💃') + await fs.move(fromPath, toPath) assert.equal(await fs.read(toPath, 'utf8'), '💃') assert.equal(await fs.exists(fromPath), false) @@ -865,8 +861,8 @@ describe('File System Class', () => { const fromPath = Path.file('private', 'a', 'b', 'file') const toPath = Path.file('public', 'a', 'b', 'c', 'd', 'file') - const { _capsuleKey } = await fs.write(fromPath, 'utf8', '💃') - const { _capsuleCID } = await fs.move(fromPath, toPath) + await fs.write(fromPath, 'utf8', '💃') + await fs.move(fromPath, toPath) assert.equal(await fs.read(toPath, 'utf8'), '💃') assert.equal(await fs.exists(fromPath), false) @@ -919,35 +915,27 @@ describe('File System Class', () => { setTimeout(resolve, fsOpts.settleTimeBeforePublish * 1.5) ) - const promise = new Promise((resolve, reject) => { + const promise = new Promise((resolve, reject) => { setTimeout(reject, 10_000) fs.once('publish') .then((event) => event.dataRoot) .then(resolve, reject) }) - const a = await fs.write( - Path.file('private', 'a'), - 'bytes', - new Uint8Array() - ) - const b = await fs.write( - Path.file('private', 'b'), - 'bytes', - new Uint8Array() - ) - const c = await fs.write( - Path.file('private', 'c'), - 'bytes', - new Uint8Array() - ) + await fs.write(Path.file('private', 'a'), 'bytes', new Uint8Array()) + + await fs.write(Path.file('private', 'b'), 'bytes', new Uint8Array()) + + await fs.write(Path.file('private', 'c'), 'bytes', new Uint8Array()) + const d = await fs.write( Path.file('private', 'd'), 'bytes', new Uint8Array() ) - assert.equal((await promise).toString(), d.dataRoot.toString()) + const result = await promise + assert.equal(result.toString(), d.dataRoot.toString()) }) it("doesn't publish when asked not to do so", async () => { @@ -986,10 +974,10 @@ describe('File System Class', () => { // Other than "publish" it('emits an event for a mutation', async () => { - const eventPromise: Promise = new Promise((resolve, reject) => { - setTimeout(reject, 10000) + const eventPromise = new Promise((resolve, reject) => { + setTimeout(reject, 10_000) - fs.on('local-change', ({ dataRoot }) => { + fs.on('commit', ({ dataRoot }) => { resolve(dataRoot) }) }) @@ -1000,10 +988,8 @@ describe('File System Class', () => { new Uint8Array() ) - assert.equal( - (await eventPromise).toString(), - mutationResult.dataRoot.toString() - ) + const eventResult = await eventPromise + assert.equal(eventResult.toString(), mutationResult.dataRoot.toString()) }) // TRANSACTIONS @@ -1022,14 +1008,14 @@ describe('File System Class', () => { assert.equal(await fs.read(Path.file('public', 'file'), 'utf8'), '💃') }) - async function transaction(): Promise { - await fs - .transaction(async (t) => { - await t.write(Path.file('private', 'file'), 'utf8', '💃') - throw new Error('Whoops') - }) - .catch((_error) => {}) - } + // async function transaction(): Promise { + // await fs + // .transaction(async (t) => { + // await t.write(Path.file('private', 'file'), 'utf8', '💃') + // throw new Error('Whoops') + // }) + // .catch((_error) => {}) + // } // it("doesn't commit a transaction when an error occurs inside of the transaction", async () => { // const tracker = new assert.CallTracker()