diff --git a/packages/cultmesh-ts/src/index.ts b/packages/cultmesh-ts/src/index.ts index dab2089e..e968d7e0 100644 --- a/packages/cultmesh-ts/src/index.ts +++ b/packages/cultmesh-ts/src/index.ts @@ -3636,14 +3636,23 @@ export class CultMeshNode { definition: TDefinition, key: string, ): CultCacheDocumentValue | undefined { - return this.cache.get(definition, key); + const registered = this.resolveCacheDefinition(definition); + const value = this.cache.get(registered, key); + return value === undefined + ? undefined + : this.parseDocumentValue(definition, value); } public getRequired( definition: TDefinition, key: string, ): CultCacheDocumentValue { - return this.cache.getRequired(definition, key); + const value = this.get(definition, key); + if (value === undefined) { + throw new Error(`CultMesh node has no "${definition.type}" document at key "${key}".`); + } + + return value; } public put( @@ -3651,14 +3660,17 @@ export class CultMeshNode { key: string, value: CultCacheDocumentValue, ): Promise> { - return this.cache.put(definition, key, value); + const registered = this.resolveCacheDefinition(definition); + return this.cache + .put(registered, key, this.parseDocumentValue(definition, value) as never) + .then(stored => this.parseDocumentValue(definition, stored)); } public delete( definition: TDefinition, key: string, ): Promise { - return this.cache.delete(definition, key); + return this.cache.delete(this.resolveCacheDefinition(definition), key); } public document( @@ -3670,7 +3682,16 @@ export class CultMeshNode { pollMs?: number; } = {}, ): CultMeshDocumentHandle> { - return cultMeshDocumentFromCache(this.cache, definition, key, options); + const documentId = options.documentId ?? `${definition.type}:${key}`; + return this.asRequestedDocument( + cultMeshDocumentFromCache( + this.cache, + this.resolveCacheDefinition(definition), + key, + { ...options, documentId }, + ), + definition, + ); } public reactiveDocument( @@ -3692,18 +3713,8 @@ export class CultMeshNode { pollMs, ...reactiveOptions } = options; - const schema = cultMeshSchemaFromDefinition(definition); - const binding = - this.documents.get(definition.type) ?? - (schema.schemaId ? this.documents.getBySchemaId(schema.schemaId) : undefined); - const document = binding - ? this.document(binding.definition, key, { documentId, routeHint, pollMs }) - .asSchemaAlias>(schema, { - parse: value => definition.schema.parse(value), - }) - : this.document(definition, key, { documentId, routeHint, pollMs }); - - return document.reactive & object>(reactiveOptions); + return this.document(definition, key, { documentId, routeHint, pollMs }) + .reactive & object>(reactiveOptions); } public globalDocument( @@ -3714,7 +3725,15 @@ export class CultMeshNode { pollMs?: number; } = {}, ): CultMeshDocumentHandle> { - return cultMeshGlobalDocumentFromCache(this.cache, definition, options); + const documentId = options.documentId ?? `${definition.type}:global`; + return this.asRequestedDocument( + cultMeshGlobalDocumentFromCache( + this.cache, + this.resolveCacheDefinition(definition), + { ...options, documentId }, + ), + definition, + ); } public collection( @@ -3725,12 +3744,47 @@ export class CultMeshNode { pollMs?: number; } = {}, ): CultMeshCollectionHandle> { - return cultMeshCollectionFromCache(this.cache, definition, options); + const collectionId = options.collectionId ?? definition.type; + return cultMeshCollectionFromCache( + this.cache, + this.resolveCacheDefinition(definition), + { ...options, collectionId }, + ).asSchemaAlias>( + cultMeshSchemaFromDefinition(definition), + { parse: value => this.parseDocumentValue(definition, value) }, + ); } public async flush(soft = false): Promise { await this.store.pushAll?.(this.cache.snapshot(), { soft }); } + + private resolveCacheDefinition( + definition: TDefinition, + ): AnyCultCacheDocumentDefinition { + const schema = cultMeshSchemaFromDefinition(definition); + return ( + this.documents.get(definition.type) ?? + (schema.schemaId ? this.documents.getBySchemaId(schema.schemaId) : undefined) + )?.definition ?? definition; + } + + private asRequestedDocument( + document: CultMeshDocumentHandle, + definition: TDefinition, + ): CultMeshDocumentHandle> { + return document.asSchemaAlias>( + cultMeshSchemaFromDefinition(definition), + { parse: value => this.parseDocumentValue(definition, value) }, + ); + } + + private parseDocumentValue( + definition: TDefinition, + value: unknown, + ): CultCacheDocumentValue { + return definition.schema.parse(value) as CultCacheDocumentValue; + } } export class CultMeshVerseCatalog { diff --git a/packages/cultmesh-ts/test/cultmesh.test.ts b/packages/cultmesh-ts/test/cultmesh.test.ts index d917d5a4..28484033 100644 --- a/packages/cultmesh-ts/test/cultmesh.test.ts +++ b/packages/cultmesh-ts/test/cultmesh.test.ts @@ -240,7 +240,7 @@ test("CultMesh TS reactive documents submit predictions from member writes", asy assert.deepEqual(predictions, ["member-write-prediction-final"]); }); -test("CultMesh TS node opens reactive same-schema aliases with one call", async () => { +test("CultMesh TS node opens same-schema aliases with one call", async () => { const filePath = join(await mkdtemp(join(tmpdir(), "cultmesh-ts-node-reactive-")), "node.ccmp"); const node = await CultMesh.startNode(filePath, { documents: [noteDocument], @@ -250,6 +250,33 @@ test("CultMesh TS node opens reactive same-schema aliases with one call", async body: "initial", }); + const aliasHandle = node.document(noteAliasDocument, "note:node-reactive", { + pollMs: 5, + }); + assert.equal(aliasHandle.documentId, "cultmesh.note.ui:note:node-reactive"); + assert.equal((await aliasHandle.latest()).body, "initial"); + await aliasHandle.set({ + noteId: "note:node-reactive", + body: "edited-through-alias-handle", + }); + assert.equal( + node.getRequired(noteAliasDocument, "note:node-reactive").body, + "edited-through-alias-handle", + ); + + await node.put(noteAliasDocument, "note:node-reactive", { + noteId: "note:node-reactive", + body: "edited-through-alias-put", + }); + assert.equal( + node.getRequired(noteDocument, "note:node-reactive").body, + "edited-through-alias-put", + ); + assert.deepEqual( + (await node.collection(noteAliasDocument).latest()).map(note => note.body), + ["edited-through-alias-put"], + ); + const reactive = node.reactiveDocument(noteAliasDocument, "note:node-reactive", { context: CultMesh.queryContext("browser-client"), watch: false,