Accept generated parse-style schemas in CultCacheTS

This commit is contained in:
Meta Vens 2026-05-06 06:33:13 +01:00
commit b7cc33a846
6 changed files with 86 additions and 14 deletions

View file

@ -14,6 +14,8 @@ This is not an ORM in a fake mustache. It is for cases where you want:
## Current Shape
- document types are registered explicitly
- a document schema only needs a `parse(...)` function, so generated JSON-schema
contracts can plug in directly without growing a second hand-written Zod body
- each persisted envelope stores:
- `type`
- `key`
@ -166,6 +168,10 @@ const combatLogDocument = defineDocumentType({
If you do not supply one, `CultCacheTS` uses MessagePack plus Zod validation by default.
Zod is no longer mandatory. Any schema object with a `parse(...)` function will
do, which means generated Ajv-backed JSON-schema contracts can feed the cache
directly as long as they expose the same parse surface.
The important boundary is this:
- payload bytes are persisted as bytes

View file

@ -30,7 +30,7 @@ type HydratedEntry = CultCacheEnvelope & {
};
type BackingStoreTypeReference = string | AnyCultCacheDocumentDefinition;
type AccessorInput = string | ((value: unknown) => unknown);
type AccessorInput = string | ((value: any) => unknown);
export class CultCacheBuilder {
readonly #cache = new CultCache();

View file

@ -1,8 +1,11 @@
import type { ZodType } from "zod";
import type {
AnyCultCacheDocumentDefinition,
CultCacheDocumentDefinition,
CultCacheDocumentRegistry,
CultCacheSchema,
} from "./types";
import type { AnyCultCacheDocumentDefinition, CultCacheDocumentDefinition, CultCacheDocumentRegistry } from "./types";
export function defineDocumentType<TSchema extends ZodType>(
export function defineDocumentType<TSchema extends CultCacheSchema>(
definition: CultCacheDocumentDefinition<TSchema>,
): CultCacheDocumentDefinition<TSchema> {
if (!definition.type || definition.type.trim().length === 0) {

View file

@ -10,9 +10,11 @@ export type {
CultCacheDocumentFormatter,
CultCacheDocumentIndexDefinition,
CultCacheDocumentRegistry,
CultCacheSchema,
CultCacheDocumentValue,
CultCacheEnvelope,
CultCacheIndexScalar,
InferCultCacheSchemaValue,
CultCacheStoreRegistration,
PushAllOptions,
} from "./types";

View file

@ -1,7 +1,12 @@
import { z, type ZodType } from "zod";
export type CultCacheIndexScalar = string | number | boolean | bigint;
export interface CultCacheSchema<TValue = unknown> {
parse(input: unknown): TValue;
}
export type InferCultCacheSchemaValue<TSchema extends CultCacheSchema> =
TSchema extends CultCacheSchema<infer TValue> ? TValue : never;
export type CultCacheDocumentFieldName<TValue> = TValue extends object ? Extract<keyof TValue, string> : never;
export type CultCacheDocumentAccessor<TValue> =
@ -18,19 +23,19 @@ export interface CultCacheDocumentIndexDefinition<TValue> {
accessor: CultCacheDocumentAccessor<TValue>;
}
export interface CultCacheDocumentDefinition<TSchema extends ZodType = ZodType> {
export interface CultCacheDocumentDefinition<TSchema extends CultCacheSchema = CultCacheSchema> {
type: string;
schema: TSchema;
schemaName?: string;
global?: boolean;
name?: CultCacheDocumentAccessor<z.infer<TSchema>>;
name?: CultCacheDocumentAccessor<InferCultCacheSchemaValue<TSchema>>;
indexes?:
| Readonly<Record<string, CultCacheDocumentAccessor<z.infer<TSchema>>>>
| readonly CultCacheDocumentIndexDefinition<z.infer<TSchema>>[];
formatter?: CultCacheDocumentFormatter<z.infer<TSchema>>;
| Readonly<Record<string, CultCacheDocumentAccessor<InferCultCacheSchemaValue<TSchema>>>>
| readonly CultCacheDocumentIndexDefinition<InferCultCacheSchemaValue<TSchema>>[];
formatter?: CultCacheDocumentFormatter<InferCultCacheSchemaValue<TSchema>>;
}
export type CultCacheDocumentValue<TDefinition extends CultCacheDocumentDefinition> = z.infer<
export type CultCacheDocumentValue<TDefinition extends CultCacheDocumentDefinition> = InferCultCacheSchemaValue<
TDefinition["schema"]
>;

View file

@ -10,7 +10,7 @@ import { z } from "zod";
import { CultCache } from "../src/cult-cache";
import { defineDocumentRegistry, defineDocumentType } from "../src/document";
import { SingleFileMessagePackBackingStore } from "../src/single-file-messagepack-backing-store";
import type { CacheBackingStore, CultCacheEnvelope } from "../src/types";
import type { CacheBackingStore, CultCacheEnvelope, CultCacheSchema } from "../src/types";
test("CultCache supports registry bootstrap, global documents, and lookup by name and index", async () => {
const itemDocument = defineDocumentType({
@ -179,3 +179,59 @@ test("CultCache fails closed on unknown or illegal persisted polymorphic state",
/has multiple persisted entries/,
);
});
test("CultCache accepts generated parse-style schemas without a Zod mirror", async () => {
type GeneratedSettings = {
schema_version: "generated.settings.v0";
theme: string;
retries: number;
};
const generatedSchema: CultCacheSchema<GeneratedSettings> = {
parse(input: unknown): GeneratedSettings {
if (!input || typeof input !== "object") {
throw new Error("generated settings must be an object");
}
const value = input as Record<string, unknown>;
if (value.schema_version !== "generated.settings.v0") {
throw new Error("generated settings schema_version mismatch");
}
if (typeof value.theme !== "string") {
throw new Error("generated settings theme must be a string");
}
if (typeof value.retries !== "number") {
throw new Error("generated settings retries must be a number");
}
return {
schema_version: "generated.settings.v0",
theme: value.theme,
retries: value.retries,
};
},
};
const generatedDocument = defineDocumentType({
type: "generated-settings",
schema: generatedSchema,
global: true,
});
const storePath = join(await mkdtemp(join(tmpdir(), "cultcache-ts-")), "generated.msgpack");
const cache = CultCache.builder()
.withDocumentType(generatedDocument)
.withGenericStore(new SingleFileMessagePackBackingStore(storePath))
.build();
await cache.putGlobal(generatedDocument, {
schema_version: "generated.settings.v0",
theme: "ash",
retries: 3,
});
const settings = cache.getRequiredGlobal(generatedDocument);
assert.equal(settings.schema_version, "generated.settings.v0");
assert.equal(settings.theme, "ash");
assert.equal(settings.retries, 3);
});