From 828514def5ffecd3591a4ddedaae9b68878ef3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Tue, 2 Jun 2026 05:54:32 +0000 Subject: [PATCH] feat(core): define CasStore, VarStore, TagStore, Store types Fixes #39 --- packages/core/src/index.ts | 9 ++ packages/core/src/types-store.test.ts | 154 ++++++++++++++++++++++++++ packages/core/src/types.ts | 97 ++++++++++++++++ 3 files changed, 260 insertions(+) create mode 100644 packages/core/src/types-store.test.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c946608..5a38e4e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -25,11 +25,20 @@ export { export { createMemoryStore } from "./store.js"; export type { CasNode, + CasStore, Hash, + HistoryEntry, ListEntry, ListOptions, ListSort, + OcasStore, Store, + Tag, + TagOp, + TagStore, + VarListOptions, + VarSetOptions, + VarStore, } from "./types.js"; export type { Variable } from "./variable.js"; export { diff --git a/packages/core/src/types-store.test.ts b/packages/core/src/types-store.test.ts new file mode 100644 index 0000000..51dfe2d --- /dev/null +++ b/packages/core/src/types-store.test.ts @@ -0,0 +1,154 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import type { + CasNode, + CasStore, + Hash, + HistoryEntry, + ListEntry, + ListOptions, + OcasStore, + Tag, + TagOp, + TagStore, + Variable, + VarListOptions, + VarSetOptions, + VarStore, +} from "./index.js"; + +describe("CasStore type", () => { + test("has all required methods with correct signatures", () => { + const stub: CasStore = { + get: (_h: Hash): CasNode | null => null, + put: (_t: Hash, _p: unknown): Hash => "0000000000000", + has: (_h: Hash): boolean => false, + delete: (_h: Hash): boolean => false, + listByType: (_t: Hash, _o?: ListOptions): ListEntry[] => [], + listMeta: (_o?: ListOptions): ListEntry[] => [], + listSchemas: (_o?: ListOptions): ListEntry[] => [], + }; + expect(typeof stub.get).toBe("function"); + expect(typeof stub.put).toBe("function"); + expect(typeof stub.has).toBe("function"); + expect(typeof stub.delete).toBe("function"); + expect(typeof stub.listByType).toBe("function"); + expect(typeof stub.listMeta).toBe("function"); + expect(typeof stub.listSchemas).toBe("function"); + }); +}); + +describe("VarStore type", () => { + test("VarStore shape", () => { + const sample: Variable = { + name: "@x/y", + schema: "0", + value: "0", + created: 0, + updated: 0, + tags: {}, + labels: [], + }; + const stub: VarStore = { + set: (_n: string, _h: Hash, _o?: VarSetOptions): Variable => sample, + get: (_n: string, _s?: Hash): Variable | null => null, + remove: (_n: string, _s?: Hash): Variable[] => [], + update: (_n: string, _h: Hash, _o?: VarSetOptions): Variable => sample, + list: (_o?: VarListOptions): Variable[] => [], + history: (_n: string, _s?: Hash): HistoryEntry[] => [], + close: (): void => {}, + }; + expect(typeof stub.set).toBe("function"); + expect(typeof stub.get).toBe("function"); + expect(typeof stub.remove).toBe("function"); + expect(typeof stub.update).toBe("function"); + expect(typeof stub.list).toBe("function"); + expect(typeof stub.history).toBe("function"); + expect(typeof stub.close).toBe("function"); + }); + + test("VarSetOptions shape", () => { + const opts: VarSetOptions = { tags: { k: "v" }, labels: ["l"] }; + expect(opts.tags?.k).toBe("v"); + }); + + test("VarListOptions extends ListOptions", () => { + const opts: VarListOptions = { + namePrefix: "@x/", + exactName: "@x/y", + schema: "0", + tags: { k: "v" }, + labels: ["l"], + sort: "created", + desc: true, + limit: 10, + offset: 0, + }; + expect(opts.namePrefix).toBe("@x/"); + }); + + test("HistoryEntry shape", () => { + const e: HistoryEntry = { value: "0", position: 0, setAt: 0 }; + expect(e.position).toBe(0); + }); +}); + +describe("TagStore type", () => { + test("TagStore shape", () => { + const stub: TagStore = { + tag: (_t: Hash, _ops: TagOp[]): Tag[] => [], + untag: (_t: Hash, _k: string[]): void => {}, + tags: (_t: Hash): Tag[] => [], + listByTag: (_t: string, _o?: ListOptions): Hash[] => [], + }; + expect(typeof stub.tag).toBe("function"); + expect(typeof stub.untag).toBe("function"); + expect(typeof stub.tags).toBe("function"); + expect(typeof stub.listByTag).toBe("function"); + }); + + test("Tag and TagOp shapes", () => { + const t: Tag = { key: "k", value: "v", target: "0", created: 0 }; + const tNull: Tag = { key: "label", value: null, target: "0", created: 0 }; + const op1: TagOp = { op: "set", key: "k", value: "v" }; + const op2: TagOp = { op: "delete", key: "k" }; + expect(t.value).toBe("v"); + expect(tNull.value).toBeNull(); + expect(op1.op).toBe("set"); + expect(op2.op).toBe("delete"); + }); +}); + +describe("Aggregate OcasStore type", () => { + test("has cas, var, tag fields", () => { + type _AssertCas = OcasStore["cas"] extends CasStore ? true : false; + type _AssertVar = OcasStore["var"] extends VarStore ? true : false; + type _AssertTag = OcasStore["tag"] extends TagStore ? true : false; + const a: _AssertCas = true; + const b: _AssertVar = true; + const c: _AssertTag = true; + expect(a && b && c).toBe(true); + }); +}); + +describe("source convention", () => { + test("types.ts uses 'type' not 'interface' for new types", () => { + const src = readFileSync(join(import.meta.dir, "types.ts"), "utf8"); + for (const name of ["CasStore", "VarStore", "TagStore"]) { + expect(src).toMatch(new RegExp(`export\\s+type\\s+${name}\\b`)); + expect(src).not.toMatch(new RegExp(`interface\\s+${name}\\b`)); + } + }); +}); + +describe("Exports surface", () => { + test("@ocas/core exports the new type names (compile-time only)", () => { + type _C = CasStore extends object ? true : never; + type _V = VarStore extends object ? true : never; + type _T = TagStore extends object ? true : never; + type _O = OcasStore extends object ? true : never; + const _checks: [_C, _V, _T, _O] = [true, true, true, true]; + expect(_checks.length).toBe(4); + }); +}); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 4e8ed7a..3450846 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,3 +1,5 @@ +import type { Variable } from "./variable.js"; + /** * 13-character uppercase Crockford Base32 string produced by XXH64. */ @@ -58,3 +60,98 @@ export type Store = { listSchemas(options?: ListOptions): ListEntry[]; delete(hash: Hash): void; }; + +/** + * Synchronous content-addressable store interface (new unified design). + * Unlike legacy `Store`, `put` returns the hash synchronously. + */ +export type CasStore = { + get(hash: Hash): CasNode | null; + put(typeHash: Hash, payload: unknown): Hash; + has(hash: Hash): boolean; + delete(hash: Hash): boolean; + listByType(typeHash: Hash, options?: ListOptions): ListEntry[]; + listMeta(options?: ListOptions): ListEntry[]; + listSchemas(options?: ListOptions): ListEntry[]; +}; + +/** + * Options for setting/updating a variable. + */ +export type VarSetOptions = { + tags?: Record; + labels?: string[]; +}; + +/** + * Options for listing variables. + */ +export type VarListOptions = ListOptions & { + namePrefix?: string; + exactName?: string; + schema?: Hash; + tags?: Record; + labels?: string[]; +}; + +/** + * One entry in a variable's history (most-recent-first per `position`). + */ +export type HistoryEntry = { + value: Hash; + position: number; + setAt: number; +}; + +/** + * Variable store interface — mutable bindings (name, schema) → hash. + */ +export type VarStore = { + set(name: string, hash: Hash, options?: VarSetOptions): Variable; + get(name: string, schema?: Hash): Variable | null; + remove(name: string, schema?: Hash): Variable[]; + update(name: string, hash: Hash, options?: VarSetOptions): Variable; + list(options?: VarListOptions): Variable[]; + history(name: string, schema?: Hash): HistoryEntry[]; + close(): void; +}; + +/** + * A tag attached to a CAS target. `value === null` indicates a label + * (bare identifier); otherwise a key-value tag. + */ +export type Tag = { + key: string; + value: string | null; + target: Hash; + created: number; +}; + +/** + * A tag mutation operation: set (with value) or delete (key only). + */ +export type TagOp = { + op: "set" | "delete"; + key: string; + value?: string; +}; + +/** + * Tag store interface — manages key-value tags and labels on CAS targets. + */ +export type TagStore = { + tag(target: Hash, operations: TagOp[]): Tag[]; + untag(target: Hash, keys: string[]): void; + tags(target: Hash): Tag[]; + listByTag(tag: string, options?: ListOptions): Hash[]; +}; + +/** + * Aggregate OCAS store: bundles CAS, variable, and tag stores. + * Named `OcasStore` to avoid colliding with the legacy `Store` export. + */ +export type OcasStore = { + cas: CasStore; + var: VarStore; + tag: TagStore; +};