diff --git a/README.md b/README.md index 332c613..b7509b2 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,10 @@ const settings = cache.getRequiredGlobal(settingsDocument); - `delete(...)` - `deleteGlobal(...)` - `snapshot()` +- `getEnvelope(...)` +- `getRequiredEnvelope(...)` +- `getGlobalEnvelope(...)` +- `putEnvelope(...)` ## Name, Index, and Global Semantics @@ -180,6 +184,21 @@ The important boundary is this: So callers get typed values, and the persistence layer does not become an open polymorphic sewer. +## Raw Envelope Fast Path + +For bit-compatible neighbors such as CultNet peers that already share the same +formatter contract, `CultCacheTS` can now move the persisted envelope directly: + +- `getEnvelope(...)` / `getRequiredEnvelope(...)` export the canonical + MessagePack payload bytes for a typed document +- `putEnvelope(...)` ingests that envelope into another cache instance without + re-encoding the payload first + +That still is not magic shared memory. The cache decodes once so lookups and +typed reads stay honest. But it stops doing the stupid part where identical +MessagePack payload bytes get decoded into a generic value, then re-encoded +into the same bytes again just to cross the room. + ## Security Model `CultCacheTS` should never deserialize arbitrary persisted payloads into arbitrary runtime shapes. diff --git a/dist/cult-cache.d.ts b/dist/cult-cache.d.ts index 2eb1d5c..7ed56bc 100644 --- a/dist/cult-cache.d.ts +++ b/dist/cult-cache.d.ts @@ -21,8 +21,11 @@ export declare class CultCache { pullAllBackingStores(): Promise; get(definition: TDefinition, key: string): CultCacheDocumentValue | undefined; getRequired(definition: TDefinition, key: string): CultCacheDocumentValue; + getEnvelope(definition: TDefinition, key: string): CultCacheEnvelope | undefined; + getRequiredEnvelope(definition: TDefinition, key: string): CultCacheEnvelope; getGlobal(definition: TDefinition): CultCacheDocumentValue | undefined; getRequiredGlobal(definition: TDefinition): CultCacheDocumentValue; + getGlobalEnvelope(definition: TDefinition): CultCacheEnvelope | undefined; getAll(definition: TDefinition): CultCacheDocumentValue[]; getKeyByName(definition: TDefinition, name: string): string | undefined; getIdByName(definition: TDefinition, name: string): string | undefined; @@ -31,6 +34,7 @@ export declare class CultCache { getIdByIndex(definition: TDefinition, indexName: string, value: string): string | undefined; getByIndex(definition: TDefinition, indexName: string, value: string): CultCacheDocumentValue | undefined; put(definition: TDefinition, key: string, value: CultCacheDocumentValue): Promise>; + putEnvelope(definition: TDefinition, envelope: CultCacheEnvelope): Promise>; putGlobal(definition: TDefinition, value: CultCacheDocumentValue): Promise>; update(definition: TDefinition, key: string, updater: (current: CultCacheDocumentValue | undefined) => CultCacheDocumentValue): Promise>; updateGlobal(definition: TDefinition, updater: (current: CultCacheDocumentValue | undefined) => CultCacheDocumentValue): Promise>; diff --git a/dist/cult-cache.d.ts.map b/dist/cult-cache.d.ts.map index af85f4a..8a61ac3 100644 --- a/dist/cult-cache.d.ts.map +++ b/dist/cult-cache.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"cult-cache.d.ts","sourceRoot":"","sources":["../src/cult-cache.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"cult-cache.d.ts","sourceRoot":"","sources":["../src/cult-cache.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/cult-cache.js b/dist/cult-cache.js index 32094fa..3bedf55 100644 --- a/dist/cult-cache.js +++ b/dist/cult-cache.js @@ -123,6 +123,18 @@ class CultCache { } return value; } + getEnvelope(definition, key) { + this.#requireDefinition(definition); + const entry = this.#entries.get(this.#entryId(definition.type, key)); + return entry ? this.#toEnvelope(entry) : undefined; + } + getRequiredEnvelope(definition, key) { + const entry = this.getEnvelope(definition, key); + if (!entry) { + throw new Error(`CultCache has no "${definition.type}" envelope at key "${key}".`); + } + return entry; + } getGlobal(definition) { const registered = this.#requireDefinition(definition); if (!registered.global) { @@ -138,6 +150,14 @@ class CultCache { } return value; } + getGlobalEnvelope(definition) { + const registered = this.#requireDefinition(definition); + if (!registered.global) { + throw new Error(`CultCache document type "${definition.type}" is not marked as global.`); + } + const globalKey = this.#globalKeys.get(definition.type); + return globalKey ? this.getEnvelope(definition, globalKey) : undefined; + } getAll(definition) { this.#requireDefinition(definition); const values = []; @@ -203,6 +223,40 @@ class CultCache { this.#applyHydratedEntry(registered, entry, parsed, "put"); return parsed; } + async putEnvelope(definition, envelope) { + const registered = this.#requireDefinition(definition); + if (envelope.type !== definition.type) { + throw new Error(`CultCache envelope type "${envelope.type}" does not match definition "${definition.type}".`); + } + if (!envelope.key || envelope.key.trim().length === 0) { + throw new Error(`CultCache envelope key for type "${definition.type}" must be non-empty.`); + } + if (!envelope.storedAt || envelope.storedAt.trim().length === 0) { + throw new Error(`CultCache envelope storedAt for type "${definition.type}" must be non-empty.`); + } + const payload = this.#cloneBytes(envelope.payload); + const parsed = registered.formatter.decode(payload); + const entry = { + key: envelope.key, + type: envelope.type, + payload, + storedAt: envelope.storedAt, + }; + const route = this.#resolveRoute(definition.type); + if (!route.primary) { + throw new Error(`No backing store is registered for document type "${definition.type}".`); + } + if (registered.global) { + const existingGlobalKey = this.#globalKeys.get(definition.type); + if (existingGlobalKey && existingGlobalKey !== entry.key) { + await this.delete(definition, existingGlobalKey); + } + } + await route.primary.push(entry); + await Promise.all(route.mirrors.map(async (mirror) => mirror.push(entry))); + this.#applyHydratedEntry(registered, entry, parsed, "put"); + return parsed; + } async putGlobal(definition, value) { const registered = this.#requireDefinition(definition); if (!registered.global) { diff --git a/src/cult-cache.ts b/src/cult-cache.ts index fd7c768..da7294f 100644 --- a/src/cult-cache.ts +++ b/src/cult-cache.ts @@ -215,6 +215,27 @@ export class CultCache { return value; } + getEnvelope( + definition: TDefinition, + key: string, + ): CultCacheEnvelope | undefined { + this.#requireDefinition(definition); + const entry = this.#entries.get(this.#entryId(definition.type, key)); + return entry ? this.#toEnvelope(entry) : undefined; + } + + getRequiredEnvelope( + definition: TDefinition, + key: string, + ): CultCacheEnvelope { + const entry = this.getEnvelope(definition, key); + if (!entry) { + throw new Error(`CultCache has no "${definition.type}" envelope at key "${key}".`); + } + + return entry; + } + getGlobal( definition: TDefinition, ): CultCacheDocumentValue | undefined { @@ -238,6 +259,18 @@ export class CultCache { return value; } + getGlobalEnvelope( + definition: TDefinition, + ): CultCacheEnvelope | undefined { + const registered = this.#requireDefinition(definition); + if (!registered.global) { + throw new Error(`CultCache document type "${definition.type}" is not marked as global.`); + } + + const globalKey = this.#globalKeys.get(definition.type); + return globalKey ? this.getEnvelope(definition, globalKey) : undefined; + } + getAll( definition: TDefinition, ): CultCacheDocumentValue[] { @@ -345,6 +378,50 @@ export class CultCache { return parsed; } + async putEnvelope( + definition: TDefinition, + envelope: CultCacheEnvelope, + ): Promise> { + const registered = this.#requireDefinition(definition); + if (envelope.type !== definition.type) { + throw new Error( + `CultCache envelope type "${envelope.type}" does not match definition "${definition.type}".`, + ); + } + if (!envelope.key || envelope.key.trim().length === 0) { + throw new Error(`CultCache envelope key for type "${definition.type}" must be non-empty.`); + } + if (!envelope.storedAt || envelope.storedAt.trim().length === 0) { + throw new Error(`CultCache envelope storedAt for type "${definition.type}" must be non-empty.`); + } + + const payload = this.#cloneBytes(envelope.payload); + const parsed = registered.formatter.decode(payload) as CultCacheDocumentValue; + const entry: CultCacheEnvelope = { + key: envelope.key, + type: envelope.type, + payload, + storedAt: envelope.storedAt, + }; + + const route = this.#resolveRoute(definition.type); + if (!route.primary) { + throw new Error(`No backing store is registered for document type "${definition.type}".`); + } + + if (registered.global) { + const existingGlobalKey = this.#globalKeys.get(definition.type); + if (existingGlobalKey && existingGlobalKey !== entry.key) { + await this.delete(definition, existingGlobalKey); + } + } + + await route.primary.push(entry); + await Promise.all(route.mirrors.map(async (mirror) => mirror.push(entry))); + this.#applyHydratedEntry(registered, entry, parsed, "put"); + return parsed; + } + async putGlobal( definition: TDefinition, value: CultCacheDocumentValue, diff --git a/test/cult-cache.test.ts b/test/cult-cache.test.ts index 4108e70..6d4e65d 100644 --- a/test/cult-cache.test.ts +++ b/test/cult-cache.test.ts @@ -235,3 +235,38 @@ test("CultCache accepts generated parse-style schemas without a Zod mirror", asy assert.equal(settings.theme, "ash"); assert.equal(settings.retries, 3); }); + +test("CultCache can ingest raw envelopes without re-encoding the payload", async () => { + const noteDocument = defineDocumentType({ + type: "note", + schema: z.object({ + title: z.string(), + body: z.string(), + }), + }); + + const storePath = join(await mkdtemp(join(tmpdir(), "cultcache-ts-")), "cache.msgpack"); + const origin = CultCache.builder() + .withDocumentType(noteDocument) + .withGenericStore(new SingleFileMessagePackBackingStore(storePath)) + .build(); + const target = CultCache.builder() + .withDocumentType(noteDocument) + .withGenericStore(new SingleFileMessagePackBackingStore(join(await mkdtemp(join(tmpdir(), "cultcache-ts-")), "target.msgpack"))) + .build(); + + await origin.put(noteDocument, "note:hello", { + title: "Hello", + body: "world", + }); + + const envelope = origin.getRequiredEnvelope(noteDocument, "note:hello"); + const applied = await target.putEnvelope(noteDocument, envelope); + + assert.deepEqual(applied, { + title: "Hello", + body: "world", + }); + assert.deepEqual(target.getRequired(noteDocument, "note:hello"), applied); + assert.deepEqual(target.getRequiredEnvelope(noteDocument, "note:hello").payload, envelope.payload); +});