CultMesh TS: a catalog refuses a second type for a schema it holds, typed
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea, I support retiring it." A catalog indexed handles by schema id and by schema name and version with plain map writes, so a handle of another type for a held schema silently replaced it for every lookup by schema alone. - Document and collection catalogs refuse, with the SchemaConflictError the registries use, a typed handle whose type differs from the one holding its schema id or schema name and version, or whose type already holds another schema id. Nothing is indexed when a handle is refused. - Allowed: handles of one type for one schema (one per record), the same handle again, and a handle without a type, which claims none. - Test: other-type same-id, other-type same-name-and-version and same-type other-id claimants, both orders, via add and via construction, for both catalogs; the first handle still answers afterwards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
43ed9396d3
commit
3763a53cef
2 changed files with 73 additions and 0 deletions
|
|
@ -3,6 +3,7 @@ import { decode, encode } from "@msgpack/msgpack";
|
|||
|
||||
import {
|
||||
CultCache,
|
||||
SchemaConflictError,
|
||||
SingleFileMessagePackBackingStore,
|
||||
type AnyCultCacheDocumentDefinition,
|
||||
type CacheBackingStore,
|
||||
|
|
@ -1473,6 +1474,7 @@ export class CultMeshDocumentCatalog {
|
|||
}
|
||||
|
||||
public add<TDocument>(document: CultMeshDocumentHandle<TDocument>): this {
|
||||
cultMeshRefuseSecondCatalogType(document.schema, this.#byType, this.#bySchemaId, this.#bySchemaNameVersion);
|
||||
this.#byDocumentId.set(document.documentId, document as CultMeshDocumentHandle<any>);
|
||||
if (document.schema.type) {
|
||||
this.#byType.set(document.schema.type, document as CultMeshDocumentHandle<any>);
|
||||
|
|
@ -1684,6 +1686,7 @@ export class CultMeshCollectionCatalog {
|
|||
}
|
||||
|
||||
public add<TDocument>(collection: CultMeshCollectionHandle<TDocument>): this {
|
||||
cultMeshRefuseSecondCatalogType(collection.schema, this.#byType, this.#bySchemaId, this.#bySchemaNameVersion);
|
||||
this.#byCollectionId.set(collection.collectionId, collection as CultMeshCollectionHandle<any>);
|
||||
if (collection.schema.type) {
|
||||
this.#byType.set(collection.schema.type, collection as CultMeshCollectionHandle<any>);
|
||||
|
|
@ -6193,6 +6196,32 @@ function normalizeCultMeshDocumentSchema(
|
|||
return normalized;
|
||||
}
|
||||
|
||||
// One type owns a schema in a catalog, as in a registry. A handle whose type differs from the one already holding its
|
||||
// schema id or schema name and version, or whose type already holds another schema, is refused. Handles of one type
|
||||
// for one schema may repeat (one per record), and a handle without a type claims no type.
|
||||
function cultMeshRefuseSecondCatalogType(
|
||||
schema: CultMeshDocumentSchemaDescriptor,
|
||||
byType: ReadonlyMap<string, { readonly schema: CultMeshDocumentSchemaDescriptor }>,
|
||||
bySchemaId: ReadonlyMap<string, { readonly schema: CultMeshDocumentSchemaDescriptor }>,
|
||||
bySchemaNameVersion: ReadonlyMap<string, { readonly schema: CultMeshDocumentSchemaDescriptor }>,
|
||||
): void {
|
||||
if (!schema.type) {
|
||||
return;
|
||||
}
|
||||
const key = cultMeshSchemaNameVersionKey(schema);
|
||||
const sameType = byType.get(schema.type)?.schema;
|
||||
const holder =
|
||||
(sameType?.schemaId && schema.schemaId && sameType.schemaId !== schema.schemaId ? sameType : undefined) ??
|
||||
[schema.schemaId ? bySchemaId.get(schema.schemaId) : undefined, key ? bySchemaNameVersion.get(key) : undefined]
|
||||
.map(held => held?.schema)
|
||||
.find(held => held?.type !== undefined && held.type !== schema.type);
|
||||
if (holder) {
|
||||
const name = (held: CultMeshDocumentSchemaDescriptor) => held.schemaName ?? held.type ?? held.schemaId ?? "";
|
||||
throw new SchemaConflictError(schema.schemaId ?? schema.type, [name(holder), name(schema)], "",
|
||||
`CultMesh catalog schema ${cultMeshSchemaLabel(schema)} is held by type "${holder.type}" and cannot also be held by type "${schema.type}".`);
|
||||
}
|
||||
}
|
||||
|
||||
function cultMeshSchemaNameVersionKey(
|
||||
schema: CultMeshDocumentSchemaDescriptor,
|
||||
): string | undefined {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const noteDocument = defineDocumentType({
|
|||
});
|
||||
|
||||
type Note = z.infer<typeof noteDocument.schema>;
|
||||
type CultMeshDocumentSchemaDescriptorLike = { type?: string; schemaId?: string; schemaName?: string; schemaVersion?: string };
|
||||
|
||||
// Another type claiming noteDocument's schema. It is never registered beside noteDocument: one type owns a schema.
|
||||
const otherTypeForNoteSchema = defineDocumentType({
|
||||
|
|
@ -185,6 +186,49 @@ test("CultMesh TS catalogs infer a lookup's value type from its document definit
|
|||
void typeOnly;
|
||||
});
|
||||
|
||||
test("CultMesh TS catalogs hold one type per schema and refuse a second, typed", () => {
|
||||
const sameNameVersion = { type: "cultmesh.note.named", schemaId: "sha256:note-named", schemaName: "cultmesh.note", schemaVersion: "cultmesh.note.v0" };
|
||||
const sameTypeOtherSchema = { type: "cultmesh.note", schemaId: "cultmesh.note.v9" };
|
||||
const note = { noteId: "note:held", body: "held" };
|
||||
const kinds = [
|
||||
{
|
||||
catalog: (...schemas: CultMeshDocumentSchemaDescriptorLike[]) =>
|
||||
CultMesh.documents(...schemas.map((schema, index) => CultMesh.document(`doc:${index}`, schema, async () => note))),
|
||||
handle: (schema: CultMeshDocumentSchemaDescriptorLike, id: string) => CultMesh.document(id, schema, async () => note),
|
||||
find: (catalog: any, schema: CultMeshDocumentSchemaDescriptorLike) => catalog.document(schema).documentId,
|
||||
},
|
||||
{
|
||||
catalog: (...schemas: CultMeshDocumentSchemaDescriptorLike[]) =>
|
||||
CultMesh.collections(...schemas.map((schema, index) => CultMesh.collection(`doc:${index}`, schema, async () => [note]))),
|
||||
handle: (schema: CultMeshDocumentSchemaDescriptorLike, id: string) => CultMesh.collection(id, schema, async () => [note]),
|
||||
find: (catalog: any, schema: CultMeshDocumentSchemaDescriptorLike) => catalog.collection(schema).collectionId,
|
||||
},
|
||||
];
|
||||
for (const kind of kinds) {
|
||||
for (const claimant of [otherTypeForNoteSchema, sameNameVersion, sameTypeOtherSchema]) {
|
||||
for (const [first, second] of [[noteDocument, claimant], [claimant, noteDocument]] as const) {
|
||||
const catalog = kind.catalog(first);
|
||||
assert.throws(() => catalog.add(kind.handle(second, "doc:second") as any), (error: unknown) => {
|
||||
assert.ok(error instanceof SchemaConflictError, String(error));
|
||||
assert.equal(error.schemaId, second.schemaId);
|
||||
assert.equal(error.recordKey, "");
|
||||
return true;
|
||||
});
|
||||
assert.throws(() => kind.catalog(first, second), SchemaConflictError);
|
||||
// The refused handle left nothing behind: the first type still answers.
|
||||
assert.equal(kind.find(catalog, first), "doc:0");
|
||||
assert.equal(kind.find(catalog, { schemaId: first.schemaId }), "doc:0");
|
||||
}
|
||||
}
|
||||
// Handles of one type for one schema may repeat, the same handle may be added again, and an untyped handle claims no type.
|
||||
const held = kind.handle(noteDocument, "doc:held");
|
||||
const catalog = kind.catalog(noteDocument);
|
||||
catalog.add(held as any).add(held as any);
|
||||
catalog.add(kind.handle({ schemaId: "cultmesh.note.v0" }, "doc:untyped") as any);
|
||||
assert.equal(kind.find(catalog, noteDocument), "doc:held");
|
||||
}
|
||||
});
|
||||
|
||||
test("CultMesh TS document handles submit predictions through configured authority hooks", async () => {
|
||||
let current = {
|
||||
noteId: "note:prediction",
|
||||
|
|
|
|||
Loading…
Reference in a new issue