diff --git a/packages/cultmesh-ts/src/index.ts b/packages/cultmesh-ts/src/index.ts index d8edb3aa..dab2089e 100644 --- a/packages/cultmesh-ts/src/index.ts +++ b/packages/cultmesh-ts/src/index.ts @@ -3673,6 +3673,39 @@ export class CultMeshNode { return cultMeshDocumentFromCache(this.cache, definition, key, options); } + public reactiveDocument( + definition: TDefinition, + key: string, + options: { + context?: CultMeshQueryContext | string; + watch?: boolean; + debounceMs?: number; + onError?: (error: unknown) => void; + documentId?: string; + routeHint?: CultMeshRouteHint; + pollMs?: number; + } = {}, + ): CultMeshReactiveDocument & object> { + const { + documentId, + routeHint, + 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); + } + public globalDocument( definition: TDefinition, options: { @@ -4590,6 +4623,23 @@ export class CultMesh { return cultMeshGlobalDocumentFromCache(cache, definition, options); } + public static reactiveDocument( + node: CultMeshNode, + definition: TDefinition, + key: string, + options: { + context?: CultMeshQueryContext | string; + watch?: boolean; + debounceMs?: number; + onError?: (error: unknown) => void; + documentId?: string; + routeHint?: CultMeshRouteHint; + pollMs?: number; + } = {}, + ): CultMeshReactiveDocument & object> { + return node.reactiveDocument(definition, key, options); + } + public static documents( ...documents: readonly CultMeshDocumentHandle[] ): CultMeshDocumentCatalog { diff --git a/packages/cultmesh-ts/test/cultmesh.test.ts b/packages/cultmesh-ts/test/cultmesh.test.ts index 8266bc24..d917d5a4 100644 --- a/packages/cultmesh-ts/test/cultmesh.test.ts +++ b/packages/cultmesh-ts/test/cultmesh.test.ts @@ -240,6 +240,43 @@ 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 () => { + const filePath = join(await mkdtemp(join(tmpdir(), "cultmesh-ts-node-reactive-")), "node.ccmp"); + const node = await CultMesh.startNode(filePath, { + documents: [noteDocument], + }); + await node.put(noteDocument, "note:node-reactive", { + noteId: "note:node-reactive", + body: "initial", + }); + + const reactive = node.reactiveDocument(noteAliasDocument, "note:node-reactive", { + context: CultMesh.queryContext("browser-client"), + watch: false, + }); + await reactive.ready; + reactive.current.body = "edited-through-node-helper"; + await waitFor( + () => node.getRequired(noteDocument, "note:node-reactive").body === "edited-through-node-helper", + "node-level reactive alias write", + ); + + assert.equal(reactive.current.noteId, "note:node-reactive"); + assert.equal( + node.document(noteDocument, "note:node-reactive").asSchemaAlias(noteAliasDocument, { + parse: value => noteAliasDocument.schema.parse(value), + }).canSet, + true, + ); + assert.equal( + (await CultMesh.reactiveDocument(node, noteAliasDocument, "note:node-reactive", { + watch: false, + }).ready).body, + "edited-through-node-helper", + ); + reactive.dispose(); +}); + test("CultMesh TS reactive documents store reconciliation deltas after misprediction", async () => { let current = { noteId: "note:reconcile",