From ae342f5e7173e8b79e3f96d823ec4d93df9046f3 Mon Sep 17 00:00:00 2001 From: Xinyu Ma Date: Thu, 18 Jan 2024 18:01:53 -0800 Subject: [PATCH] Add workspace --- README.md | 7 ++- src/security/cert-storage.ts | 5 ++- src/security/mod.ts | 1 + src/security/types.ts | 6 +++ src/storage/deno-kv.ts | 52 ++++++++++++++++++++++ src/storage/mod.ts | 1 + src/workspace/mod.ts | 1 + src/workspace/workspace.ts | 84 ++++++++++++++++++++++++++++++++++++ 8 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 src/security/types.ts create mode 100644 src/storage/deno-kv.ts create mode 100644 src/workspace/workspace.ts diff --git a/README.md b/README.md index e7b2b15..834b7c3 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,12 @@ echo "@ucla-irl:registry=https://npm.pkg.github.com" >> .npmrc pnpm add @ucla-irl/ndnts-aux ``` +If you are asked to login, create a GitHub access token and use the following command: +```bash +pnpm login --scope=@ucla-irl --auth-type=legacy --registry=https://npm.pkg.github.com +# Use the token for password +``` + The current release does not currently mark peer-dependencies. So please ignore the warnings given by `pnpm`. Just install NDNts nightly build as usual and it will work. @@ -18,7 +24,6 @@ Unfortunately, the denoland release does not work. Please ignore that. ## TODOs -- Add CI for automatic release - Add more test - Add a class for NDN workspace - Add name pattern match and some ntschema for better namespace management. diff --git a/src/security/cert-storage.ts b/src/security/cert-storage.ts index ca0e07e..e328bb8 100644 --- a/src/security/cert-storage.ts +++ b/src/security/cert-storage.ts @@ -3,13 +3,14 @@ import { Data, Interest, Name, Signer, Verifier } from '@ndn/packet'; import { Certificate, createSigner, createVerifier, ECDSA } from '@ndn/keychain'; import { Endpoint } from '@ndn/endpoint'; import { Storage } from '../storage/mod.ts'; +import { SecurityAgent } from './types.ts'; /** * A Signer & Verifier that handles security authentication. * CertStorage itself is not a storage, actually. Depend on an external storage. * Note: CertStorage will not serve the certificate. */ -export class CertStorage { +export class CertStorage implements SecurityAgent { private _signer: Signer | undefined; readonly readyEvent: Promise; @@ -36,7 +37,7 @@ export class CertStorage { /** Obtain the signer */ get signer() { - return this._signer; + return this._signer!; } /** Obtain this node's own certificate */ diff --git a/src/security/mod.ts b/src/security/mod.ts index 0ba042c..52190ac 100644 --- a/src/security/mod.ts +++ b/src/security/mod.ts @@ -1 +1,2 @@ export * from './cert-storage.ts'; +export * from './types.ts'; diff --git a/src/security/types.ts b/src/security/types.ts new file mode 100644 index 0000000..11f6416 --- /dev/null +++ b/src/security/types.ts @@ -0,0 +1,6 @@ +import type { Signer, Verifier } from '@ndn/packet'; + +export interface SecurityAgent { + signer: Signer; + verifier: Verifier; +} diff --git a/src/storage/deno-kv.ts b/src/storage/deno-kv.ts new file mode 100644 index 0000000..7a4ddad --- /dev/null +++ b/src/storage/deno-kv.ts @@ -0,0 +1,52 @@ +import { Storage } from './types.ts'; + +/** + * A storage based on DenoKV. + * Not actually included in the exported package. May need Custom Shims. + */ +export class DenoKvStorage implements Storage { + constructor( + public readonly kv: Deno.Kv, + ) { + } + + public static async create(path?: string) { + return new DenoKvStorage(await Deno.openKv(path)); + } + + async get(key: string): Promise { + const ret = await this.kv.get([key]); + return ret.value ?? undefined; + } + + async set(key: string, value: Uint8Array | undefined): Promise { + await this.kv.set([key], value); + } + + async has(key: string): Promise { + const ret = await this.kv.get([key]); + return !!ret.value; + } + + async delete(key: string): Promise { + const ret = await this.kv.get([key]); + if (ret.value) { + await this.kv.delete([key]); + return true; + } else { + return false; + } + } + + async clear(): Promise { + const entries = this.kv.list({ prefix: [] }); + for await (const entry of entries) { + await this.kv.delete(entry.key); + } + } + + close(): Promise { + this.kv.close(); + return Promise.resolve(); + } +} diff --git a/src/storage/mod.ts b/src/storage/mod.ts index c5ef3fa..25b64c5 100644 --- a/src/storage/mod.ts +++ b/src/storage/mod.ts @@ -1,3 +1,4 @@ export * from './types.ts'; export * from './in-memory.ts'; export * from './file-system.ts'; +// export * from './deno-kv.ts'; diff --git a/src/workspace/mod.ts b/src/workspace/mod.ts index e69de29..4ac5a1d 100644 --- a/src/workspace/mod.ts +++ b/src/workspace/mod.ts @@ -0,0 +1 @@ +export * from './workspace.ts'; diff --git a/src/workspace/workspace.ts b/src/workspace/workspace.ts new file mode 100644 index 0000000..5ea06d8 --- /dev/null +++ b/src/workspace/workspace.ts @@ -0,0 +1,84 @@ +import { Storage } from '../storage/mod.ts'; +import { Endpoint } from '@ndn/endpoint'; +import type { Name, Signer, Verifier } from '@ndn/packet'; +import { encodeSyncState, parseSyncState, SyncAgent } from '../sync-agent/mod.ts'; +import { NdnSvsAdaptor, YjsStateManager } from '../adaptors/mod.ts'; +import * as Y from 'yjs'; + +export class Workspace { + private constructor( + public readonly nodeId: Name, + public readonly persistStore: Storage, + public readonly endpoint: Endpoint, + public readonly onReset: (() => void) | undefined, + public readonly syncAgent: SyncAgent, + public readonly yjsSnapshotMgr: YjsStateManager, + public readonly yjsAdaptor: NdnSvsAdaptor, + ) { + } + + public static async create(opts: { + nodeId: Name; + persistStore: Storage; + endpoint: Endpoint; + rootDoc: Y.Doc; + signer: Signer; + verifier: Verifier; + onReset?: () => void; + createNewDoc?: () => Promise; + }) { + // Sync Agents + const syncAgent = await SyncAgent.create( + opts.nodeId, + opts.persistStore, + opts.endpoint, + opts.signer, + opts.verifier, + opts.onReset, + ); + + // Root doc using CRDT and Sync + const yjsAdaptor = new NdnSvsAdaptor( + syncAgent, + opts.rootDoc, + 'doc', + ); + const yjsSnapshotMgr = new YjsStateManager( + () => encodeSyncState(syncAgent!.getUpdateSyncSV()), + opts.rootDoc, + // No key conflict in this case. If we are worried, use anothe sub-folder. + opts.persistStore, + ); + + // Load or create + if (opts.createNewDoc) { + await opts.createNewDoc(); + } else { + const state = await yjsSnapshotMgr.loadLocalSnapshot((update) => { + yjsAdaptor!.handleSyncUpdate(update); + return Promise.resolve(); + }); + await syncAgent.replayUpdates('doc', state ? parseSyncState(state) : undefined); + } + + // Start Sync + syncAgent.ready = true; + + return new Workspace( + opts.nodeId, + opts.persistStore, + opts.endpoint, + opts.onReset, + syncAgent, + yjsSnapshotMgr, + yjsAdaptor, + ); + } + + public destroy() { + this.syncAgent.ready = false; + this.yjsSnapshotMgr.destroy(); + this.syncAgent.destroy(); + this.persistStore.close(); + } +}