From 2738f4c2db9293383c787fe86930dca9d0ae4e10 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 27 Jun 2026 21:46:05 +0100 Subject: [PATCH] Resolve TS snapshot filters by schema alias --- packages/cultnet-ts/src/replication.ts | 37 +++++++++++++++++- packages/cultnet-ts/test/cultnet.test.ts | 50 ++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/packages/cultnet-ts/src/replication.ts b/packages/cultnet-ts/src/replication.ts index 9925fd1d..f53f7c26 100644 --- a/packages/cultnet-ts/src/replication.ts +++ b/packages/cultnet-ts/src/replication.ts @@ -159,7 +159,7 @@ export class CultNetDocumentRegistry { const binding = this.#requireBinding(envelope.type); const schemaId = schemaIdForEnvelope(envelope, binding); - if (requestedSchemaIds && !requestedSchemaIds.has(schemaId)) { + if (requestedSchemaIds && !schemaMatchesBinding(schemaId, binding, requestedSchemaIds)) { continue; } @@ -196,7 +196,7 @@ export class CultNetDocumentRegistry { const binding = this.#requireBinding(envelope.type); const schemaId = schemaIdForEnvelope(envelope, binding); - if (requestedSchemaIds && !requestedSchemaIds.has(schemaId)) { + if (requestedSchemaIds && !schemaMatchesBinding(schemaId, binding, requestedSchemaIds)) { continue; } @@ -340,6 +340,39 @@ function schemaIdForBinding(binding: CultNetDocumentBinding): string { return binding.definition.schemaId ?? binding.definition.type; } +function schemaMatchesBinding( + schemaId: string, + binding: CultNetDocumentBinding, + requestedSchemaIds: Set, +): boolean { + if (requestedSchemaIds.has(schemaId)) { + return true; + } + + const definition = binding.definition; + if (definition.schemaId && requestedSchemaIds.has(definition.schemaId)) { + return true; + } + if (definition.schemaName && requestedSchemaIds.has(definition.schemaName)) { + return true; + } + if (definition.compatibleSchemaIds?.some(candidate => requestedSchemaIds.has(candidate))) { + return true; + } + + return Array.from(requestedSchemaIds) + .some(candidate => inferSchemaName(candidate) === definition.schemaName); +} + +function inferSchemaName(schemaId: string): string | undefined { + const marker = schemaId.lastIndexOf(".v"); + if (marker <= 0 || marker + 2 >= schemaId.length) { + return undefined; + } + const version = schemaId.slice(marker + 2); + return /^\d+$/u.test(version) ? schemaId.slice(0, marker) : undefined; +} + function schemaIdForEnvelope( envelope: CultCacheEnvelope, binding: CultNetDocumentBinding, diff --git a/packages/cultnet-ts/test/cultnet.test.ts b/packages/cultnet-ts/test/cultnet.test.ts index 838dfbb2..bd9d79f3 100644 --- a/packages/cultnet-ts/test/cultnet.test.ts +++ b/packages/cultnet-ts/test/cultnet.test.ts @@ -1266,6 +1266,56 @@ test("CultNet raw replication applies compatible foreign-schema snapshots", asyn } }); +test("CultNet document registry filters snapshots by schema aliases", async () => { + const tempDir = mkdtempSync(join(tmpdir(), "cultnetts-alias-snapshot-")); + + try { + const documentDefinition = defineDocumentType({ + type: "cultnet.alias_note", + schemaId: "sha256:cultnet-alias-note", + schemaName: "cultnet.alias_note", + schemaVersion: "cultnet.alias_note.v1", + schema: z.object({ + schema_version: z.string(), + noteId: z.string(), + body: z.string(), + }), + name: "noteId", + }); + const registry = new CultNetDocumentRegistry([ + defineCultNetDocumentBinding({ definition: documentDefinition }), + ]); + const cache = CultCache.builder() + .withDocumentType(documentDefinition) + .withGenericStore(new SingleFileMessagePackBackingStore(join(tempDir, "alias.msgpack"))) + .build(); + + await cache.put(documentDefinition, "note:alias", { + schema_version: "cultnet.alias_note.v1", + noteId: "note:alias", + body: "filtered by alias", + }); + + const typedSnapshot = registry.createSnapshotResponse(cache, "typed-alias", { + schemaVersion: "cultnet.snapshot_request.v0", + messageId: "typed-alias", + schemaIds: ["cultnet.alias_note.v1"], + }); + const rawSnapshot = registry.createRawSnapshotResponse(cache, "raw-alias", { + schemaVersion: "cultnet.snapshot_request.v0", + messageId: "raw-alias", + schemaIds: ["cultnet.alias_note.v1"], + }); + + assert.equal(typedSnapshot.documents[0]?.schemaId, "sha256:cultnet-alias-note"); + assert.equal(typedSnapshot.documents[0]?.recordKey, "note:alias"); + assert.equal(rawSnapshot.documents[0]?.schemaId, "sha256:cultnet-alias-note"); + assert.equal(rawSnapshot.documents[0]?.recordKey, "note:alias"); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } +}); + test("CultNet interop slot compatibility defaults missing trailing fields and rejects mismatched slots", () => { const note: InteropNote = { schemaVersion: INTEROP_SCHEMA_VERSION,