diff --git a/packages/cultcache-ts/src/cult-cache.ts b/packages/cultcache-ts/src/cult-cache.ts index c0ee0b58..2f2fb063 100644 --- a/packages/cultcache-ts/src/cult-cache.ts +++ b/packages/cultcache-ts/src/cult-cache.ts @@ -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, diff --git a/packages/cultcache-ts/src/document-formatter.ts b/packages/cultcache-ts/src/document-formatter.ts new file mode 100644 index 00000000..b92636a0 --- /dev/null +++ b/packages/cultcache-ts/src/document-formatter.ts @@ -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 { + 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 = { + encode: (value: unknown) => encode(value), + decode: (payload: Uint8Array) => decode(payload), +}; diff --git a/packages/cultcache-ts/src/document.ts b/packages/cultcache-ts/src/document.ts index 52a5e494..8df6eb82 100644 --- a/packages/cultcache-ts/src/document.ts +++ b/packages/cultcache-ts/src/document.ts @@ -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( 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 { - 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 = { - 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. * diff --git a/scripts/test-typescript-package-closure.mjs b/scripts/test-typescript-package-closure.mjs index 55f1517f..b9d5ab29 100644 --- a/scripts/test-typescript-package-closure.mjs +++ b/scripts/test-typescript-package-closure.mjs @@ -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 {