Add raw envelope fast path to CultCacheTS
This commit is contained in:
parent
6c2e1e3691
commit
ed3f2b8dd7
6 changed files with 190 additions and 1 deletions
19
README.md
19
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.
|
||||
|
|
|
|||
4
dist/cult-cache.d.ts
vendored
4
dist/cult-cache.d.ts
vendored
|
|
@ -21,8 +21,11 @@ export declare class CultCache {
|
|||
pullAllBackingStores(): Promise<void>;
|
||||
get<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition, key: string): CultCacheDocumentValue<TDefinition> | undefined;
|
||||
getRequired<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition, key: string): CultCacheDocumentValue<TDefinition>;
|
||||
getEnvelope<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition, key: string): CultCacheEnvelope | undefined;
|
||||
getRequiredEnvelope<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition, key: string): CultCacheEnvelope;
|
||||
getGlobal<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition): CultCacheDocumentValue<TDefinition> | undefined;
|
||||
getRequiredGlobal<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition): CultCacheDocumentValue<TDefinition>;
|
||||
getGlobalEnvelope<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition): CultCacheEnvelope | undefined;
|
||||
getAll<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition): CultCacheDocumentValue<TDefinition>[];
|
||||
getKeyByName<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition, name: string): string | undefined;
|
||||
getIdByName<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition, name: string): string | undefined;
|
||||
|
|
@ -31,6 +34,7 @@ export declare class CultCache {
|
|||
getIdByIndex<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition, indexName: string, value: string): string | undefined;
|
||||
getByIndex<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition, indexName: string, value: string): CultCacheDocumentValue<TDefinition> | undefined;
|
||||
put<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition, key: string, value: CultCacheDocumentValue<TDefinition>): Promise<CultCacheDocumentValue<TDefinition>>;
|
||||
putEnvelope<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition, envelope: CultCacheEnvelope): Promise<CultCacheDocumentValue<TDefinition>>;
|
||||
putGlobal<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition, value: CultCacheDocumentValue<TDefinition>): Promise<CultCacheDocumentValue<TDefinition>>;
|
||||
update<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition, key: string, updater: (current: CultCacheDocumentValue<TDefinition> | undefined) => CultCacheDocumentValue<TDefinition>): Promise<CultCacheDocumentValue<TDefinition>>;
|
||||
updateGlobal<TDefinition extends AnyCultCacheDocumentDefinition>(definition: TDefinition, updater: (current: CultCacheDocumentValue<TDefinition> | undefined) => CultCacheDocumentValue<TDefinition>): Promise<CultCacheDocumentValue<TDefinition>>;
|
||||
|
|
|
|||
2
dist/cult-cache.d.ts.map
vendored
2
dist/cult-cache.d.ts.map
vendored
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"cult-cache.d.ts","sourceRoot":"","sources":["../src/cult-cache.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"cult-cache.d.ts","sourceRoot":"","sources":["../src/cult-cache.ts"],"names":[],"mappings":"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"}
|
||||
54
dist/cult-cache.js
vendored
54
dist/cult-cache.js
vendored
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -215,6 +215,27 @@ export class CultCache {
|
|||
return value;
|
||||
}
|
||||
|
||||
getEnvelope<TDefinition extends AnyCultCacheDocumentDefinition>(
|
||||
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<TDefinition extends AnyCultCacheDocumentDefinition>(
|
||||
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<TDefinition extends AnyCultCacheDocumentDefinition>(
|
||||
definition: TDefinition,
|
||||
): CultCacheDocumentValue<TDefinition> | undefined {
|
||||
|
|
@ -238,6 +259,18 @@ export class CultCache {
|
|||
return value;
|
||||
}
|
||||
|
||||
getGlobalEnvelope<TDefinition extends AnyCultCacheDocumentDefinition>(
|
||||
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<TDefinition extends AnyCultCacheDocumentDefinition>(
|
||||
definition: TDefinition,
|
||||
): CultCacheDocumentValue<TDefinition>[] {
|
||||
|
|
@ -345,6 +378,50 @@ export class CultCache {
|
|||
return parsed;
|
||||
}
|
||||
|
||||
async putEnvelope<TDefinition extends AnyCultCacheDocumentDefinition>(
|
||||
definition: TDefinition,
|
||||
envelope: CultCacheEnvelope,
|
||||
): Promise<CultCacheDocumentValue<TDefinition>> {
|
||||
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<TDefinition>;
|
||||
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<TDefinition extends AnyCultCacheDocumentDefinition>(
|
||||
definition: TDefinition,
|
||||
value: CultCacheDocumentValue<TDefinition>,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue