From ac1b2c7582c479a1344af76efb47db1a89ccedb9 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Thu, 14 Dec 2023 19:35:15 +0100 Subject: [PATCH] feat: add onCommit option --- packages/nest/src/class.ts | 83 ++++++++++++++++++++-------- packages/nest/src/root-tree.ts | 6 +- packages/nest/src/root-tree/basic.ts | 6 +- packages/nest/src/transaction.ts | 28 +++++++--- packages/nest/src/types.ts | 29 ++++++---- packages/nest/test/class.test.ts | 14 +++++ 6 files changed, 117 insertions(+), 49 deletions(-) diff --git a/packages/nest/src/class.ts b/packages/nest/src/class.ts index 4d475f7..68ea939 100644 --- a/packages/nest/src/class.ts +++ b/packages/nest/src/class.ts @@ -13,14 +13,15 @@ import Emittery, { import type { AnySupportedDataType, + CommitVerifier, DataForType, - DataRootChange, DataType, DirectoryItem, DirectoryItemWithKind, - FileSystemChange, + Modification, MutationOptions, MutationResult, + NOOP, PrivateMutationResult, PublicMutationResult, } from './types.js' @@ -54,6 +55,7 @@ import { BasicRootTree } from './root-tree/basic.js' export interface Options { blockstore: Blockstore + onCommit?: CommitVerifier rootTreeClass?: typeof RootTree settleTimeBeforePublish?: number } @@ -62,6 +64,7 @@ export class FileSystem { readonly #blockstore: Blockstore readonly #debouncedDataRootUpdate: debounce.DebouncedFunction readonly #eventEmitter: Emittery + readonly #onCommit: CommitVerifier readonly #rng: Rng.Rng #privateNodes: MountedPrivateNodes = {} @@ -70,11 +73,13 @@ export class FileSystem { /** @hidden */ constructor( blockstore: Blockstore, + onCommit: CommitVerifier | undefined, rootTree: RootTree, settleTimeBeforePublish: number ) { this.#blockstore = blockstore this.#eventEmitter = new Emittery() + this.#onCommit = onCommit ?? (async () => ({ commit: true })) this.#rng = Rng.makeRngInterface() this.#rootTree = rootTree @@ -96,23 +101,35 @@ export class FileSystem { * Creates a file system with an empty public tree & an empty private tree at the root. */ static async create(opts: Options): Promise { - const { blockstore, rootTreeClass, settleTimeBeforePublish } = opts + const { blockstore, onCommit, rootTreeClass, settleTimeBeforePublish } = + opts const rootTree = await (rootTreeClass ?? BasicRootTree).create(blockstore) - return new FileSystem(blockstore, rootTree, settleTimeBeforePublish ?? 2500) + return new FileSystem( + blockstore, + onCommit, + rootTree, + settleTimeBeforePublish ?? 2500 + ) } /** * Loads an existing file system from a CID. */ static async fromCID(cid: CID, opts: Options): Promise { - const { blockstore, rootTreeClass, settleTimeBeforePublish } = opts + const { blockstore, onCommit, rootTreeClass, settleTimeBeforePublish } = + opts const rootTree = await (rootTreeClass ?? BasicRootTree).fromCID( blockstore, cid ) - return new FileSystem(blockstore, rootTree, settleTimeBeforePublish ?? 2500) + return new FileSystem( + blockstore, + onCommit, + rootTree, + settleTimeBeforePublish ?? 2500 + ) } // EVENTS @@ -403,7 +420,7 @@ export class FileSystem { | Path.File> | Path.Directory>, mutationOptions: MutationOptions = {} - ): Promise | null> { + ): Promise> { return await this.#infusedTransaction( async (t) => { await t.copy(from, to) @@ -421,15 +438,16 @@ export class FileSystem { path: Path.Directory>, mutationOptions?: MutationOptions ): Promise< - MutationResult

& { path: Path.Directory> } + MutationResult> }> > async createDirectory( path: Path.Directory>, mutationOptions: MutationOptions = {} ): Promise< - MutationResult & { - path: Path.Directory> - } + MutationResult< + Partition, + { path: Path.Directory> } + > > { let finalPath = path @@ -455,7 +473,7 @@ export class FileSystem { data: DataForType, mutationOptions?: MutationOptions ): Promise< - MutationResult

& { path: Path.File> } + MutationResult> }> > async createFile( path: Path.File>, @@ -463,9 +481,10 @@ export class FileSystem { data: DataForType, mutationOptions: MutationOptions = {} ): Promise< - MutationResult & { - path: Path.File> - } + MutationResult< + Partition, + { path: Path.File> } + > > { let finalPath = path @@ -534,11 +553,15 @@ export class FileSystem { async remove( path: Path.Distinctive>, mutationOptions: MutationOptions = {} - ): Promise { + ): Promise { const transactionResult = await this.transaction(async (t) => { await t.remove(path) }, mutationOptions) + if (transactionResult === 'no-op') { + return 'no-op' + } + return { dataRoot: transactionResult.dataRoot, } @@ -596,18 +619,23 @@ export class FileSystem { async transaction( handler: (t: TransactionContext) => Promise, mutationOptions: MutationOptions = {} - ): Promise<{ - changes: FileSystemChange[] - dataRoot: CID - }> { + ): Promise< + | { + changes: Modification[] + dataRoot: CID + } + | NOOP + > { const context = this.#transactionContext() // Execute handler await handler(context) // Commit transaction - const { changes, privateNodes, rootTree } = - await TransactionContext.commit(context) + const commitResult = await TransactionContext.commit(context) + if (commitResult === 'no-op') return 'no-op' + + const { changes, privateNodes, rootTree } = commitResult this.#privateNodes = privateNodes this.#rootTree = rootTree @@ -668,6 +696,12 @@ export class FileSystem { mutationOptions: MutationOptions = {} ): Promise> { const transactionResult = await this.transaction(handler, mutationOptions) + + const dataRoot = + transactionResult === 'no-op' + ? await this.calculateDataRoot() + : transactionResult.dataRoot + const partition = determinePartition(path) switch (partition.name) { @@ -693,7 +727,7 @@ export class FileSystem { : capsuleCID return { - dataRoot: transactionResult.dataRoot, + dataRoot, capsuleCID, contentCID, } @@ -733,7 +767,7 @@ export class FileSystem { .then(([key]) => key) return { - dataRoot: transactionResult.dataRoot, + dataRoot, capsuleKey: accessKey.toBytes(), } } @@ -743,6 +777,7 @@ export class FileSystem { #transactionContext(): TransactionContext { return new TransactionContext( this.#blockstore, + this.#onCommit, { ...this.#privateNodes }, this.#rng, this.#rootTree.clone() diff --git a/packages/nest/src/root-tree.ts b/packages/nest/src/root-tree.ts index 9034073..c5b4e96 100644 --- a/packages/nest/src/root-tree.ts +++ b/packages/nest/src/root-tree.ts @@ -2,7 +2,7 @@ import type { Blockstore } from 'interface-blockstore' import type { CID } from 'multiformats' import type { PrivateForest, PublicDirectory } from 'wnfs' -import type { FileSystemChange } from './types.js' +import type { Modification } from './types.js' /** * The tree that ties different file systems together. @@ -11,12 +11,12 @@ export abstract class RootTree { abstract privateForest(): PrivateForest abstract replacePrivateForest( forest: PrivateForest, - changes: FileSystemChange[] + changes: Modification[] ): Promise abstract publicRoot(): PublicDirectory abstract replacePublicRoot( dir: PublicDirectory, - changes: FileSystemChange[] + changes: Modification[] ): Promise abstract clone(): RootTree diff --git a/packages/nest/src/root-tree/basic.ts b/packages/nest/src/root-tree/basic.ts index e10549c..c0352c3 100644 --- a/packages/nest/src/root-tree/basic.ts +++ b/packages/nest/src/root-tree/basic.ts @@ -15,7 +15,7 @@ import * as Unix from '../unix.js' import * as Version from '../version.js' import type { RootTree } from '../root-tree.js' -import type { FileSystemChange } from '../types.js' +import type { Modification } from '../types.js' import { RootBranch } from '../path.js' import { makeRngInterface } from '../rng.js' @@ -150,7 +150,7 @@ export class BasicRootTree implements RootTree { async replacePrivateForest( forest: PrivateForest, - _changes: FileSystemChange[] + _changes: Modification[] ): Promise { return new BasicRootTree({ blockstore: this.#blockstore, @@ -169,7 +169,7 @@ export class BasicRootTree implements RootTree { async replacePublicRoot( dir: PublicDirectory, - changes: FileSystemChange[] + changes: Modification[] ): Promise { const treeWithNewPublicRoot = new BasicRootTree({ blockstore: this.#blockstore, diff --git a/packages/nest/src/transaction.ts b/packages/nest/src/transaction.ts index a46ac7a..a436503 100644 --- a/packages/nest/src/transaction.ts +++ b/packages/nest/src/transaction.ts @@ -27,11 +27,13 @@ import type { RootTree } from './root-tree.js' import type { AnySupportedDataType, + CommitVerifier, DataForType, DataType, DirectoryItem, DirectoryItemWithKind, MutationType, + NOOP, } from './types.js' import type { @@ -44,6 +46,7 @@ import type { /** @group File System */ export class TransactionContext { readonly #blockstore: Blockstore + readonly #onCommit: CommitVerifier readonly #rng: Rng #privateNodes: MountedPrivateNodes @@ -57,11 +60,13 @@ export class TransactionContext { /** @internal */ constructor( blockstore: Blockstore, + onCommit: CommitVerifier, privateNodes: MountedPrivateNodes, rng: Rng, rootTree: RootTree ) { this.#blockstore = blockstore + this.#onCommit = onCommit this.#privateNodes = privateNodes this.#rng = rng this.#rootTree = rootTree @@ -70,16 +75,23 @@ export class TransactionContext { } /** @internal */ - static async commit(context: TransactionContext): Promise<{ - changes: Array<{ - path: Path.Distinctive> - type: MutationType - }> - privateNodes: MountedPrivateNodes - rootTree: RootTree - }> { + static async commit(context: TransactionContext): Promise< + | { + changes: Array<{ + path: Path.Distinctive> + type: MutationType + }> + privateNodes: MountedPrivateNodes + rootTree: RootTree + } + | NOOP + > { const changes = [...context.#changes] + // Verify + const { commit } = await context.#onCommit([...changes]) + if (!commit) return 'no-op' + // Private forest const newForest = await changes.reduce( async (oldForestPromise, change): Promise => { diff --git a/packages/nest/src/types.ts b/packages/nest/src/types.ts index 57e9a0a..a604f4f 100644 --- a/packages/nest/src/types.ts +++ b/packages/nest/src/types.ts @@ -6,9 +6,9 @@ export type AnySupportedDataType = | Record | string -export interface DataRootChange { - dataRoot: CID -} +export type CommitVerifier = ( + changes: Modification[] +) => Promise<{ commit: boolean }> export type DataType = 'bytes' | 'json' | 'utf8' @@ -30,7 +30,7 @@ export type DirectoryItemWithKind = DirectoryItem & { path: Path.Distinctive> } -export interface FileSystemChange { +export interface Modification { path: Path.Distinctive> type: MutationType } @@ -39,10 +39,13 @@ export interface MutationOptions { skipPublish?: boolean } -export type MutationResult

= P extends Path.Public - ? PublicMutationResult +export type MutationResult< + P extends Path.Partition, + Extension = unknown, +> = P extends Path.Public + ? PublicMutationResult : P extends Path.Private - ? PrivateMutationResult + ? PrivateMutationResult : never export type MutationType = 'added-or-updated' | 'removed' @@ -76,11 +79,15 @@ export type PartitionDiscoveryNonEmpty

= } : never -export type PublicMutationResult = DataRootChange & { +export type NOOP = 'no-op' + +export type PublicMutationResult = { capsuleCID: CID contentCID: CID -} + dataRoot: CID +} & Extension -export type PrivateMutationResult = DataRootChange & { +export type PrivateMutationResult = { capsuleKey: Uint8Array -} + dataRoot: CID +} & Extension diff --git a/packages/nest/test/class.test.ts b/packages/nest/test/class.test.ts index d032bf2..9e4de87 100644 --- a/packages/nest/test/class.test.ts +++ b/packages/nest/test/class.test.ts @@ -10,6 +10,7 @@ import { MemoryBlockstore } from 'blockstore-core/memory' import * as Path from '../src/path.js' import * as Unix from '../src/unix.js' +import type { Modification } from '../src/types.js' import { FileSystem } from '../src/class.js' import { @@ -1043,4 +1044,17 @@ describe('File System Class', () => { // assert(error) // } // }) + + it("doesn't commit a transaction when onCommit returns `false`", async () => { + fs = await FileSystem.create({ + blockstore, + ...fsOpts, + onCommit: async (_changes: Modification[]) => ({ commit: false }), + }) + + mounts = await fs.mountPrivateNodes([{ path: Path.root() }]) + + // TODO: + // await fs.write(Path.file('private', 'test', 'file'), 'utf8', '🔥') + }) })