diff --git a/packages/cultmesh-ts/README.md b/packages/cultmesh-ts/README.md index b4946d26..a6dbf6c8 100644 --- a/packages/cultmesh-ts/README.md +++ b/packages/cultmesh-ts/README.md @@ -321,6 +321,10 @@ The same typed-definition overload is available for `CultMesh.documentFromStore(...)` and `CultMesh.documentFromSingleFile(...)`, so local schema publications can be read as typed reactive documents without manually pulling records, decoding MessagePack, or parsing the payload. +When the caller wants source selection to be configuration, use +`CultMesh.documentFromPublication(...)` with a `single-file`, `store`, or +`peer-snapshot` source; it returns the same typed handle while hiding whether +sync came from local persistence or a remote CultNet snapshot. ## Streaming Mode diff --git a/packages/cultmesh-ts/src/index.ts b/packages/cultmesh-ts/src/index.ts index 8751b50e..741f52be 100644 --- a/packages/cultmesh-ts/src/index.ts +++ b/packages/cultmesh-ts/src/index.ts @@ -67,6 +67,21 @@ export interface CultMeshRouteRecord { readonly description: string; } +export type CultMeshDocumentPublicationSource = + | { + readonly kind: "single-file"; + readonly path: string; + } + | { + readonly kind: "store"; + readonly store: CacheBackingStore; + } + | { + readonly kind: "peer-snapshot"; + readonly peer: CultNetPeer | (() => CultNetPeer | Promise); + readonly endpoint?: string; + }; + export interface CultMeshAuthorityClaim { readonly role: string; readonly shardId?: string; @@ -2129,6 +2144,102 @@ export function cultMeshDocumentFromPeerSnapshot( ); } +export function cultMeshDocumentFromPublication( + source: CultMeshDocumentPublicationSource, + schemaId: string, + recordKey: string, + options?: { + documentId?: string; + routeHint?: CultMeshRouteHint; + sourceId?: string; + timeoutMs?: number; + pollMs?: number; + messageIdPrefix?: string; + }, +): CultMeshDocumentHandle; +export function cultMeshDocumentFromPublication( + source: CultMeshDocumentPublicationSource, + definition: TDefinition, + recordKey: string, + options?: { + documentId?: string; + routeHint?: CultMeshRouteHint; + sourceId?: string; + timeoutMs?: number; + pollMs?: number; + messageIdPrefix?: string; + }, +): CultMeshDocumentHandle>; +export function cultMeshDocumentFromPublication( + source: CultMeshDocumentPublicationSource, + schemaOrDefinition: string | AnyCultCacheDocumentDefinition, + recordKey: string, + options: { + documentId?: string; + routeHint?: CultMeshRouteHint; + sourceId?: string; + timeoutMs?: number; + pollMs?: number; + messageIdPrefix?: string; + } = {}, +): CultMeshDocumentHandle { + if (!source) { + throw new Error("CultMesh publication source is required."); + } + requireNonEmpty(recordKey, "recordKey"); + + switch (source.kind) { + case "single-file": + return typeof schemaOrDefinition === "string" + ? cultMeshDocumentFromSingleFile(source.path, schemaOrDefinition, { + documentId: options.documentId ?? recordKey, + routeHint: options.routeHint, + sourceId: options.sourceId ?? recordKey, + pollMs: options.pollMs, + }) + : cultMeshDocumentFromSingleFile(source.path, schemaOrDefinition, { + documentId: options.documentId ?? recordKey, + routeHint: options.routeHint, + sourceId: options.sourceId ?? recordKey, + pollMs: options.pollMs, + }); + case "store": + return typeof schemaOrDefinition === "string" + ? cultMeshDocumentFromStore(source.store, schemaOrDefinition, { + documentId: options.documentId ?? recordKey, + routeHint: options.routeHint, + sourceId: options.sourceId ?? recordKey, + pollMs: options.pollMs, + }) + : cultMeshDocumentFromStore(source.store, schemaOrDefinition, { + documentId: options.documentId ?? recordKey, + routeHint: options.routeHint, + sourceId: options.sourceId ?? recordKey, + pollMs: options.pollMs, + }); + case "peer-snapshot": + return typeof schemaOrDefinition === "string" + ? cultMeshDocumentFromPeerSnapshot(source.peer, schemaOrDefinition, recordKey, { + documentId: options.documentId ?? recordKey, + routeHint: options.routeHint ?? cultMeshRouteHint("network", source.endpoint ?? "CultNet snapshot"), + sourceId: options.sourceId ?? recordKey, + timeoutMs: options.timeoutMs, + pollMs: options.pollMs, + messageIdPrefix: options.messageIdPrefix, + }) + : cultMeshDocumentFromPeerSnapshot(source.peer, schemaOrDefinition, recordKey, { + documentId: options.documentId ?? recordKey, + routeHint: options.routeHint ?? cultMeshRouteHint("network", source.endpoint ?? "CultNet snapshot"), + sourceId: options.sourceId ?? recordKey, + timeoutMs: options.timeoutMs, + pollMs: options.pollMs, + messageIdPrefix: options.messageIdPrefix, + }); + default: + throw new Error(`Unsupported CultMesh publication source kind '${(source as { kind?: string }).kind}'.`); + } +} + export function cultMeshGlobalDocumentFromCache( cache: CultCache, definition: TDefinition, @@ -3925,6 +4036,50 @@ export class CultMesh { : cultMeshDocumentFromPeerSnapshot(peer, schemaOrDefinition, recordKey, options); } + public static documentFromPublication( + source: CultMeshDocumentPublicationSource, + schemaId: string, + recordKey: string, + options?: { + documentId?: string; + routeHint?: CultMeshRouteHint; + sourceId?: string; + timeoutMs?: number; + pollMs?: number; + messageIdPrefix?: string; + }, + ): CultMeshDocumentHandle; + public static documentFromPublication( + source: CultMeshDocumentPublicationSource, + definition: TDefinition, + recordKey: string, + options?: { + documentId?: string; + routeHint?: CultMeshRouteHint; + sourceId?: string; + timeoutMs?: number; + pollMs?: number; + messageIdPrefix?: string; + }, + ): CultMeshDocumentHandle>; + public static documentFromPublication( + source: CultMeshDocumentPublicationSource, + schemaOrDefinition: string | AnyCultCacheDocumentDefinition, + recordKey: string, + options: { + documentId?: string; + routeHint?: CultMeshRouteHint; + sourceId?: string; + timeoutMs?: number; + pollMs?: number; + messageIdPrefix?: string; + } = {}, + ): CultMeshDocumentHandle { + return typeof schemaOrDefinition === "string" + ? cultMeshDocumentFromPublication(source, schemaOrDefinition, recordKey, options) + : cultMeshDocumentFromPublication(source, schemaOrDefinition, recordKey, options); + } + public static globalDocumentFromCache( cache: CultCache, definition: TDefinition, diff --git a/packages/cultmesh-ts/test/cultmesh.test.ts b/packages/cultmesh-ts/test/cultmesh.test.ts index b80c375b..4aa1ab0c 100644 --- a/packages/cultmesh-ts/test/cultmesh.test.ts +++ b/packages/cultmesh-ts/test/cultmesh.test.ts @@ -196,7 +196,10 @@ test("CultMesh TS document handles read schema publications from single-file sto }); await node.flush(); - const document = CultMesh.documentFromSingleFile(filePath, noteDocument, { + const document = CultMesh.documentFromPublication({ + kind: "single-file", + path: filePath, + }, noteDocument, "note:publication", { documentId: "daemon:cultmesh.note.latest", sourceId: "daemon:cultmesh.note.latest.v0", pollMs: 50, @@ -1949,8 +1952,12 @@ test("CultMesh TS reads remote RUDP snapshots through document handles", async ( }, ); - const document = CultMesh.documentFromPeerSnapshot( - peer, + const document = CultMesh.documentFromPublication( + { + kind: "peer-snapshot", + peer, + endpoint: `rudp://127.0.0.1:${server.bind.port}`, + }, noteDocument, "note:remote", {