cultcache-ts: resolveDocumentFormatter moves out of the ./document entry into a module no export names

The packed-consumer smoke now fails if the document entry exports it, at run time and at compile time.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-11 21:05:09 +02:00
commit 12a0fd7744
4 changed files with 40 additions and 20 deletions

View file

@ -3,9 +3,9 @@ import {
documentPayloadBytes,
documentPayloadFromBytes,
encodeDocumentPayload,
resolveDocumentFormatter,
resolveDocumentMembers,
} from "./document";
import { resolveDocumentFormatter } from "./document-formatter";
import type { DocumentPayload } from "./document";
import type {
AnyCultCacheDocumentDefinition,

View file

@ -0,0 +1,20 @@
import { decode, encode } from "@msgpack/msgpack";
import { codecFormatter } from "./codec";
import type { AnyCultCacheDocumentDefinition, CultCacheDocumentFormatter } from "./types";
// The one place that decides how a document is encoded: its own formatter, or its codec's, or a plain map. A codec and a
// formatter are two answers to one question, so a definition that carries both is refused wherever it enters (the
// builder resolves through here as well). Package-internal: no entry of the package's exports map names this module, so
// a consumer cannot import the formatter and read a payload around the schema.
export function resolveDocumentFormatter(definition: AnyCultCacheDocumentDefinition): CultCacheDocumentFormatter<unknown> {
if (definition.codec && definition.formatter) {
throw new Error(`CultCache document type "${definition.type}" declares both a codec and a formatter.`);
}
return definition.formatter ?? (definition.codec ? codecFormatter(definition.codec) : plainDocumentFormatter);
}
const plainDocumentFormatter: CultCacheDocumentFormatter<unknown> = {
encode: (value: unknown) => encode(value),
decode: (payload: Uint8Array) => decode(payload),
};

View file

@ -1,9 +1,6 @@
import { decode, encode } from "@msgpack/msgpack";
import { codecFormatter } from "./codec";
import { resolveDocumentFormatter } from "./document-formatter";
import type {
AnyCultCacheDocumentDefinition,
CultCacheDocumentFormatter,
CultCacheSchemaCatalogMember,
CultCacheDocumentDefinition,
CultCacheDocumentRegistry,
@ -76,21 +73,6 @@ export function defineDocumentType<TSchema extends CultCacheSchema>(
return Object.freeze(definition.codec ? { ...definition, members: resolveDocumentMembers(definition) } : { ...definition });
}
// The one place that decides how a document is encoded: its own formatter, or its codec's, or a plain map. A codec and a
// formatter are two answers to one question, so a definition that carries both is refused wherever it enters (the
// builder resolves through here as well). Package-internal: not exported from the package index.
export function resolveDocumentFormatter(definition: AnyCultCacheDocumentDefinition): CultCacheDocumentFormatter<unknown> {
if (definition.codec && definition.formatter) {
throw new Error(`CultCache document type "${definition.type}" declares both a codec and a formatter.`);
}
return definition.formatter ?? (definition.codec ? codecFormatter(definition.codec) : plainDocumentFormatter);
}
const plainDocumentFormatter: CultCacheDocumentFormatter<unknown> = {
encode: (value: unknown) => encode(value),
decode: (payload: Uint8Array) => decode(payload),
};
/**
* The bytes of one document, as they travel in a store envelope, a CultNet record or a realtime frame.
*

View file

@ -85,6 +85,7 @@ assert.equal(typeof cache.defineDocumentType, "function");
// The document owner is also an entry of its own, which the browser client imports without the Node-only cache.
assert.equal(documentEntry.decodeDocumentPayload, cache.decodeDocumentPayload);
assert.equal("resolveDocumentFormatter" in cache, false);
assert.equal("resolveDocumentFormatter" in documentEntry, false);
assert.equal(typeof net.CultNetPeer, "function");
assert.equal(typeof mesh.CultMesh, "function");
assert.equal(typeof mesh.CultMeshProviderSession, "function");
@ -184,6 +185,23 @@ void decodeOwner;
"browser-types-smoke.mts",
], { cwd: consumerRoot, stdio: "inherit" });
// The formatter that reads a payload around its schema is in no entry: importing it from the document entry does not compile.
writeFileSync(join(consumerRoot, "formatter-refused.mts"), `
// @ts-expect-error resolveDocumentFormatter is not exported from the document entry
import { resolveDocumentFormatter } from "@gamecult/cultcache-ts/document";
void resolveDocumentFormatter;
`);
execFileSync(process.execPath, [
tsc,
"--noEmit",
"--strict",
"--target", "ES2022",
"--module", "NodeNext",
"--moduleResolution", "NodeNext",
"--skipLibCheck",
"formatter-refused.mts",
], { cwd: consumerRoot, stdio: "inherit" });
console.log("TypeScript package closure smoke: passed");
for (const tarball of tarballs) console.log(`Packed: ${tarball}`);
} finally {