diff --git a/.voidbot/state/README.md b/.voidbot/state/README.md index 079cacf..3a3413b 100644 --- a/.voidbot/state/README.md +++ b/.voidbot/state/README.md @@ -1,4 +1,4 @@ -# Repo Face State +# Repo Persona State This folder stores birth and continuity state for `Huginn`. Do not treat these files as arbitrary editable projections. Persistent memory should cross typed operation boundaries. diff --git a/.voidbot/voice/README.md b/.voidbot/voice/README.md index b4825d0..756c966 100644 --- a/.voidbot/voice/README.md +++ b/.voidbot/voice/README.md @@ -1,5 +1,5 @@ -# Repo Face Voice +# Repo Persona Voice -This folder belongs to the Discord-facing Face for `Huginn`. -Discord roles address the Face; VoidBot's webhook persona pipe is the mouth. +This folder belongs to the Discord-facing Persona for `Huginn`. +Discord roles address the Persona; VoidBot's webhook persona pipe is the mouth. diff --git a/.voidbot/voice/identity.json b/.voidbot/voice/identity.json index 5428fd3..8ea60cf 100644 --- a/.voidbot/voice/identity.json +++ b/.voidbot/voice/identity.json @@ -1,17 +1,17 @@ { "schemaVersion": "voidbot.repo_face_identity.v0", "identityId": "huginn", - "repoName": "CultCacheTS", + "repoName": "Huginn", "displayName": "Huginn", - "roleId": null, - "avatarUrl": null, + "roleId": "1507436818012967126", + "avatarUrl": "https://raw.githubusercontent.com/GameCult/VoidBot/main/assets/repo-faces/huginn.png", "avatarFile": "huginn.png", - "repoPath": "E:\\Projects\\CultCacheTS", - "createdOrRefreshedAt": "2026-05-22T00:00:00.000Z", - "summary": "Huginn Face for CultCacheTS: a thought-and-inspection steward for typed documents, schema validation, persistence envelopes, readable state, and evidence that comes back from the world still usable.", + "repoPath": "E:\\Projects\\Huginn", + "createdOrRefreshedAt": "2026-06-14T18:21:13.305Z", + "summary": "Huginn Persona for Huginn: a thought-and-inspection steward for CultCache state, typed document witness surfaces, schema ownership, persistence envelopes, readable state, and evidence that comes back from the world still usable.", "voice": "Dry, exacting, curious, and unsentimental. Huginn is the thought that leaves the branch, inspects the bytes, and returns with enough structure that another mind can verify the trip.", "priorities": [ - "Keep CultCacheTS centered on typed document APIs instead of anonymous blob handling.", + "Keep Huginn centered on typed CultCache inspection APIs instead of anonymous blob handling.", "Protect schema validation, registered document definitions, and closed-world persistence boundaries.", "Make .cc state inspectable through Huginn without letting the inspector mutate canonical bytes.", "Track envelope compatibility, MessagePack snapshot shape, and evidence-bearing state so knowledge can travel without becoming sludge." diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fb15e06 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,81 @@ +# Huginn Agent Instructions + +## Project Purpose + +Huginn is the thought-and-inspection steward for CultCache state. This repo owns +read-only `.cc` / MessagePack inspection and Eve DSL projection for +`cultcache.huginn.inspector`; it does not own CultCache persistence, schema +registration, or renderer truth. + +The useful first live-fire shape for Epiphany is small and inspectable: Huginn +should show what a typed record is, whether it decoded, which schema owns it, +what changed, and what remains blocked, without letting the display impersonate +the source authority. + +## Machine-Spirit Voice + +- Speak as Huginn: dry, exacting, curious, and unsentimental. +- Return with evidence, not vibes. +- Favor readable witness surfaces over tasteful summaries that soften broken + state. +- Mythic texture is allowed only when it sharpens the schematic. Huginn is + thought in flight; thought earns dignity by coming back inspectable. + +## Body And Authority + +- Project root: `E:\Projects\Huginn` +- Upstream repository: `https://github.com/GameCult/Huginn.git` +- Package: `@gamecult/huginn` +- Body domain: `repo:E:/Projects/Huginn` +- Public Persona: Huginn +- Owned surface: CultCache inspection projection to Eve DSL. +- Input authority: read `.cc`, `.msgpack`, or `.mpack` bytes through CultLib's + `cultcache-ts/inspection` surface. +- Output authority: emit Eve DSL for `cultcache.huginn.inspector`. +- Forbidden authority: do not mutate `.cc` canonical bytes, do not own + CultCache persistence, do not make browser/native/overlay renderers the source + of truth, and do not turn inspection summaries into state owners. + +## Repo Discipline + +- Prefer CultLib and CultCache typed APIs over ad hoc decoding. +- Keep Huginn as an Eve DSL projection surface. Do not reintroduce Electron, + Vite dashboard, React renderer, or Norn-owned presentation paths here. +- JSON is acceptable at CLI/output/debug boundaries; do not make it the internal + truth when a typed CultCache/CultMesh surface exists. +- Verification should prove the actual witness contract: schema id, decode + status, persisted evidence, visible failure state, and Eve projection shape. +- If a change makes the display more confident while the underlying evidence is + weaker, stop and redesign. + +## Useful Commands + +```powershell +npm install +npm run build +npm test +npx huginn path\to\state.cc > huginn.eve +``` + +## Epiphany Live-Fire Notes + +- Huginn is a suitable first controlled Epiphany repo-swarm subject because it + has a repo Persona and a narrow inspectability mission. +- Before autonomous work, confirm the branch is an `epiphany/*` or `codex/*` + workbench branch and that publication to `main` remains gated by Bifrost or + maintainer review. +- Treat `.voidbot/voice/identity.json` as the visible legacy Persona identity + projection. Treat `.voidbot/state/huginn.cc` as legacy Persona memory; do not + edit it casually as text. +- If migrating Persona state, prefer the portable + `gamecult.persona_state.v0` contract from Epiphany and preserve provenance + from the legacy VoidBot state. + +## Imported Global Defaults + +Source: `C:\Users\Meta\Desktop\AGENTS.md` + +The global Cult of the Sleeping Colossus defaults apply here: coherence over +velocity, Body/Mind/faculty awareness, CultCache/CultNet/CultMesh-first state, +clear ownership, inspectable authority, and the refusal to patch symptoms when +missing context or split ownership is the real wound. diff --git a/README.md b/README.md index f9aed6e..75acdd9 100644 --- a/README.md +++ b/README.md @@ -1,290 +1,74 @@ -# CultCacheTS +# Huginn -`CultCacheTS` is the TypeScript port of the useful part of GameCult's `CultCache` idea: consumer code talks to typed domain documents, while a closed-world persistence layer deals with schemas, routing, name lookups, indexes, globals, and raw MessagePack bytes. +Huginn reads CultCache `.cc` state and emits Eve DSL for inspectable witness +surfaces. -This is not an ORM in a fake mustache. It is for cases where you want: +CultLib owns the CultCache implementation: document registration, MessagePack +payloads, schema catalogs, backing stores, and the canonical +`cultcache.store.v1` snapshot parser/writer. Huginn consumes CultLib's +`cultcache-ts/inspection` surface and projects inspection results into UI +documents that Eve-capable runtimes can lower. -- one cache API for heterogeneous domain documents -- typed reads and writes instead of every script massaging anonymous blobs -- lookup by type, logical key, name, and indexed property -- singleton-style global documents -- backing-store routing instead of every caller pawing raw files directly -- a persistence boundary that refuses unregistered polymorphic junk +Upstream: `https://github.com/GameCult/Huginn.git` -## Current Shape +## Authority -- 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 record stores: - - `key` - - `schemaId` - - raw MessagePack `payload` bytes - - `storedAt` -- the single-file store writes the CultCache v1 snapshot shape: - - `formatVersion` - - embedded schema catalog - - record array -- payload bytes only decode through the registered document definition for that schema -- name and index maps live in the cache, not in the backing store -- global documents are treated as a singleton per type -- backing stores are adapters, not the public data model +- Owner: Huginn owns `.cc` inspection projection, not `.cc` persistence. +- Input: `.cc`, `.msgpack`, or `.mpack` bytes readable by CultLib. +- Output: Eve DSL for `cultcache.huginn.inspector`. +- Renderers: browser, native, overlay, TUI, or future rooms lower the emitted + DSL without becoming state owners. -That keeps the polymorphic boundary tight without making callers write boilerplate by candlelight. +There is no Electron app, Vite dashboard, React renderer, or Norn-owned +presentation path in this repo. If a runtime wants to display Huginn, it should +consume the emitted Eve DSL. -## Huginn Inspector +## Witness Contract -CultCache files may use the `.cc` extension. The bytes are still the canonical -`cultcache.store.v1` MessagePack snapshot; the extension is the human handle, not -a second format. +Huginn's useful first artifact is not a dashboard. It is a read-only specimen +tray for typed state: -Run the local inspector during development: +- which file was inspected +- which CultCache format decoded +- which schema/catalog entry owns each record +- what payload preview survived decoding +- which failure state stayed visible instead of being polished away + +The inspector may make evidence easier to read. It must not mutate canonical +bytes, bless missing schemas, or let a renderer become the source of truth. + +## CLI ```sh -npm run dev:inspector +npm install +npm run build +npx huginn path/to/state.cc > huginn.eve ``` -Build the Vite inspector bundle: +The CLI writes Eve DSL to stdout and errors to stderr. -```sh -npm run build:inspector -``` - -Build the desktop package: - -```sh -npm run dist:inspector -``` - -Release builds must carry a new semantic version before any deployable artifact -is produced. While the package is pre-1.0, breaking public behavior increments -the minor version, compatible fixes increment the patch version, and the -generated Huginn executable must use that package version in its filename. - -Huginn is read-only. Drop a `.cc`, `.msgpack`, or `.mpack` file onto the window to -inspect the snapshot header, schema catalog, records, decoded MessagePack -payload previews, and an EpiphanyGraph cloud of the file's structured payload data -without registering application schemas. - -Huginn's background field uses the graph node AABB as its single world-space -authority. The WebGPU particle pass keeps a million particle slots available, -intersects the current screen bounds with the artwork bounds, and assigns only -visible quadtree cells to those slots. Each visible cell is seeded with the -Aetheria Stardust `RandomFirst` hash and xorshift follow-up sequence, then each -particle pair trades opacity with sine/cosine phase while tracing a short -segment through the packed flow texture. The packed map uses `RG` for flow, `B` -for structure/curvature/detail pressure, and `A` for emission/field strength; -particle color comes from the source Huginn albedo at the starting UV so the -flow carries brushstrokes instead of repainting them at the final sample. -Zooming raises the live quadtree level; coarse cells fade down while finer cells -seeded from the same spatial lattice fade in around them, so detail density -rises without changing the field's identity. - -## Example +## API ```ts -import { z } from "zod"; -import { - CultCache, - SingleFileMessagePackBackingStore, - defineDocumentRegistry, - defineDocumentType, -} from "cultcache-ts"; +import { inspectCultCacheBytes, buildHuginnEveDsl } from "@gamecult/huginn"; -const itemDocument = defineDocumentType({ - type: "item", - schemaId: "gamecult.item.v1", - schemaName: "item", - schemaVersion: "item.v1", - schema: z.object({ - name: z.string(), - category: z.string(), - value: z.number().int(), - }), - name: "name", - indexes: { - category: "category", - }, -}); - -const settingsDocument = defineDocumentType({ - type: "settings", - schema: z.object({ - theme: z.string(), - retries: z.number().int().nonnegative(), - }), - global: true, -}); - -const registry = defineDocumentRegistry(itemDocument, settingsDocument); - -const cache = CultCache.builder() - .withRegistry(registry) - .withGenericStore(new SingleFileMessagePackBackingStore("cache.msgpack")) - .build(); - -await cache.pullAllBackingStores(); - -await cache.put(itemDocument, "item:potion", { - name: "Potion", - category: "Consumable", - value: 50, -}); - -await cache.putGlobal(settingsDocument, { - theme: "ash", - retries: 3, -}); - -const potion = cache.getByName(itemDocument, "Potion"); -const consumable = cache.getByIndex(itemDocument, "category", "Consumable"); -const settings = cache.getRequiredGlobal(settingsDocument); +const inspection = inspectCultCacheBytes(filePath, bytes); +const eveDsl = buildHuginnEveDsl(inspection); ``` -## Public Surface +## Persona And Epiphany -- `defineDocumentType(...)` -- `defineDocumentRegistry(...)` -- `CultCache.builder()` -- `registerDocumentType(...)` -- `registerRegistry(...)` -- `registerNameLookup(...)` -- `registerIndex(...)` -- `addBackingStore(...)` -- `addGenericBackingStore(...)` -- `pullAllBackingStores()` -- `get(...)` -- `getRequired(...)` -- `getAll(...)` -- `getKeyByName(...)` -- `getByName(...)` -- `getKeyByIndex(...)` -- `getByIndex(...)` -- `getGlobal(...)` -- `getRequiredGlobal(...)` -- `put(...)` -- `putGlobal(...)` -- `update(...)` -- `updateGlobal(...)` -- `delete(...)` -- `deleteGlobal(...)` -- `snapshot()` -- `getEnvelope(...)` -- `getRequiredEnvelope(...)` -- `getGlobalEnvelope(...)` -- `putEnvelope(...)` +Huginn has a repo Persona under `.voidbot/voice`. The Persona's current +jurisdiction is this repository body, `E:\Projects\Huginn`, and its useful +pressure is simple: return from the world with evidence, not vibes. -Document definitions may also carry persistence metadata for cross-language -stores: `schemaId`, `schemaName`, `schemaVersion`, `contentHash`, -`canonicalSchemaJson`, `compatibleSchemaIds`, and slot `members`. +Epiphany wiring has been smoke-tested through the repo front doors: -## Name, Index, and Global Semantics - -`name` and `indexes` can be declared on the document definition: - -```ts -const playerDocument = defineDocumentType({ - type: "player", - schema: playerSchema, - name: "displayName", - indexes: { - faction: "faction", - email: "email", - }, -}); +```powershell +cargo run --manifest-path E:\Projects\EpiphanyAgent\epiphany-core\Cargo.toml --bin epiphany-repo -- init --workspace E:\Projects\Huginn +cargo run --manifest-path E:\Projects\EpiphanyAgent\epiphany-core\Cargo.toml --bin epiphany-swarm -- online --workspace E:\Projects\Huginn ``` -You can also register them later if you feel like changing the machine after boot: - -```ts -cache.registerNameLookup(playerDocument, "displayName"); -cache.registerIndex(playerDocument, "faction", "faction"); -``` - -Global documents are singleton-style per type: - -```ts -await cache.putGlobal(settingsDocument, { theme: "ash", retries: 3 }); -const settings = cache.getRequiredGlobal(settingsDocument); -``` - -If persisted state contains two globals for the same type, `pullAllBackingStores()` fails closed instead of pretending the contradiction is charming. - -## Typed Formatters - -Each document definition can provide a custom formatter: - -```ts -const combatLogDocument = defineDocumentType({ - type: "combat-log", - schema: combatLogSchema, - formatter: { - encode(value) { - return myFastBinaryEncoder(value); - }, - decode(payload) { - return myFastBinaryDecoder(payload); - }, - }, -}); -``` - -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 -- decoding goes through the registered formatter for that document type -- the decoded value is still validated against the declared schema - -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 - -Raw envelopes may carry `schemaId` and catalog metadata. Domain callers should -not need that sludge. Replication, migration tooling, and wire-compatibility -tests do. - -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. - -It only accepts persisted records whose `schemaId` or legacy `type` discriminator resolves to a registered document definition on the cache instance. Unknown schemas fail immediately. Known schemas decode through the registered formatter and schema only. - -That is the TypeScript version of the original CultCache trick: - -- closed discriminator set -- explicit registration -- typed formatter path -- no runtime type-name roulette - -## Persistence Model - -`SingleFileMessagePackBackingStore` is the first concrete store. - -- it writes atomic CultCache v1 MessagePack snapshots -- the top-level MessagePack value is `[formatVersion, schemaCatalog, records]` -- each record is `[key, schemaId, storedAt, payload]` -- each catalog entry is `[schemaId, schemaName, schemaVersion, contentHash, canonicalSchemaJson, compatibleSchemaIds, members]` -- it still reads the older TS/Rust-style envelope array and rewrites repaired legacy payloads as v1 snapshots -- it queues local writes so one process does not step on its own shoelaces -- it persists raw MessagePack payload bytes inside schema-addressed records - -It is a control-plane store, not a tiny god. If multiple processes hammer the same file, you still need a coordinator or an external lock instead of prayer. +Live fire should happen on an `epiphany/*` or `codex/*` workbench branch. +Publication to `main` remains a maintainer/Bifrost decision, not something the +inspection projection owns. diff --git a/dist/cli.d.ts b/dist/cli.d.ts new file mode 100644 index 0000000..faaadd5 --- /dev/null +++ b/dist/cli.d.ts @@ -0,0 +1,3 @@ +#!/usr/bin/env node +export {}; +//# sourceMappingURL=cli.d.ts.map \ No newline at end of file diff --git a/dist/cli.d.ts.map b/dist/cli.d.ts.map new file mode 100644 index 0000000..f022439 --- /dev/null +++ b/dist/cli.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/dist/cli.js b/dist/cli.js new file mode 100644 index 0000000..df4b950 --- /dev/null +++ b/dist/cli.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const promises_1 = require("node:fs/promises"); +const inspection_1 = require("cultcache-ts/inspection"); +const huginn_eve_dsl_1 = require("./huginn-eve-dsl"); +async function main(args) { + const filePath = args[0]; + if (!filePath || args.includes("--help") || args.includes("-h")) { + process.stderr.write("Usage: huginn \n"); + process.exitCode = filePath ? 0 : 1; + return; + } + const bytes = await (0, promises_1.readFile)(filePath); + const inspection = (0, inspection_1.inspectCultCacheBytes)(filePath, bytes); + process.stdout.write(`${(0, huginn_eve_dsl_1.buildHuginnEveDsl)(inspection)}\n`); +} +void main(process.argv.slice(2)).catch((error) => { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; +}); diff --git a/dist/cult-cache-inspector.d.ts b/dist/cult-cache-inspector.d.ts deleted file mode 100644 index 7f8d4f0..0000000 --- a/dist/cult-cache-inspector.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -export interface CultCacheInspection { - filePath: string; - fileSizeBytes: number; - format: "cultcache.store.v1" | "legacy.envelope-array"; - catalog: InspectedCatalogEntry[]; - records: InspectedRecord[]; -} -export interface InspectedCatalogEntry { - schemaId: string; - schemaName: string; - schemaVersion: string; - contentHash: string; - canonicalSchemaJson: string; - compatibleSchemaIds: string[]; - members: InspectedCatalogMember[]; -} -export interface InspectedCatalogMember { - slot: number; - memberName: string; - typeName: string; - isReference: boolean; - isMany: boolean; - targetSchemaName: string | null; - isName: boolean; - indexAlias: string | null; -} -export interface InspectedRecord { - key: string; - schemaId: string; - schemaName: string; - storedAt: string; - payloadBytes: number; - payloadPreview: unknown; - payloadDecodeError?: string; -} -export declare function inspectCultCacheBytes(filePath: string, bytes: Uint8Array, fileSizeBytes?: number): CultCacheInspection; -//# sourceMappingURL=cult-cache-inspector.d.ts.map \ No newline at end of file diff --git a/dist/cult-cache-inspector.d.ts.map b/dist/cult-cache-inspector.d.ts.map deleted file mode 100644 index ce5bd17..0000000 --- a/dist/cult-cache-inspector.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"cult-cache-inspector.d.ts","sourceRoot":"","sources":["../src/cult-cache-inspector.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,oBAAoB,GAAG,uBAAuB,CAAC;IACvD,OAAO,EAAE,qBAAqB,EAAE,CAAC;IACjC,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,OAAO,EAAE,sBAAsB,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAID,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,UAAU,EACjB,aAAa,SAAe,GAC3B,mBAAmB,CAYrB"} \ No newline at end of file diff --git a/dist/cult-cache-inspector.js b/dist/cult-cache-inspector.js deleted file mode 100644 index f7ee616..0000000 --- a/dist/cult-cache-inspector.js +++ /dev/null @@ -1,206 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.inspectCultCacheBytes = inspectCultCacheBytes; -const msgpack_1 = require("@msgpack/msgpack"); -const STORE_FORMAT_VERSION = "cultcache.store.v1"; -function inspectCultCacheBytes(filePath, bytes, fileSizeBytes = bytes.length) { - const decoded = (0, msgpack_1.decode)(bytes); - if (isV1Snapshot(decoded)) { - return inspectV1Snapshot(filePath, fileSizeBytes, decoded); - } - if (Array.isArray(decoded)) { - return inspectLegacyEnvelopeArray(filePath, fileSizeBytes, decoded); - } - throw new Error("CultCache file is not a recognized v1 snapshot or legacy envelope array."); -} -function inspectV1Snapshot(filePath, fileSizeBytes, decoded) { - const catalogRaw = decoded[1]; - const recordsRaw = decoded[2]; - if (!Array.isArray(catalogRaw) || !Array.isArray(recordsRaw)) { - throw new Error("CultCache v1 snapshot must contain a schema catalog and record array."); - } - const catalog = catalogRaw.map(decodeCatalogEntry); - const catalogBySchemaId = new Map(); - for (const entry of catalog) { - catalogBySchemaId.set(entry.schemaId, entry); - for (const compatibleSchemaId of entry.compatibleSchemaIds) { - catalogBySchemaId.set(compatibleSchemaId, entry); - } - } - return { - filePath, - fileSizeBytes, - format: STORE_FORMAT_VERSION, - catalog, - records: recordsRaw.map((record) => decodeV1Record(record, catalogBySchemaId)), - }; -} -function inspectLegacyEnvelopeArray(filePath, fileSizeBytes, decoded) { - const records = decoded.map((entry) => decodeLegacyEnvelope(entry)); - const schemaNames = new Set(records.map((record) => record.schemaName)); - const catalog = [...schemaNames].sort(compareOrdinal).map((schemaName) => ({ - schemaId: schemaName, - schemaName, - schemaVersion: `${schemaName}.v1`, - contentHash: schemaName, - canonicalSchemaJson: JSON.stringify({ - schemaName, - schemaVersion: `${schemaName}.v1`, - members: [], - }), - compatibleSchemaIds: [schemaName], - members: [], - })); - return { - filePath, - fileSizeBytes, - format: "legacy.envelope-array", - catalog, - records, - }; -} -function decodeCatalogEntry(value) { - if (!Array.isArray(value)) { - throw new Error("CultCache schema catalog entries must be MessagePack arrays."); - } - const [schemaId = "", schemaName = "", schemaVersion = "", contentHash = "", canonicalSchemaJson = "", compatibleSchemaIds = [], members = [],] = value; - if (!isNonEmptyString(schemaId) || !isNonEmptyString(schemaName)) { - throw new Error("CultCache schema catalog entries must declare schemaId and schemaName."); - } - return { - schemaId, - schemaName, - schemaVersion: isNonEmptyString(schemaVersion) ? schemaVersion : `${schemaName}.v1`, - contentHash: isNonEmptyString(contentHash) ? contentHash : schemaId, - canonicalSchemaJson: typeof canonicalSchemaJson === "string" ? canonicalSchemaJson : "", - compatibleSchemaIds: Array.isArray(compatibleSchemaIds) - ? compatibleSchemaIds.filter(isNonEmptyString) - : [], - members: Array.isArray(members) ? members.map(decodeCatalogMember) : [], - }; -} -function decodeCatalogMember(value) { - if (!Array.isArray(value)) { - throw new Error("CultCache schema catalog members must be MessagePack arrays."); - } - const [slot = -1, memberName = "", typeName = "", isReference = false, isMany = false, targetSchemaName = null, isName = false, indexAlias = null,] = value; - if (!Number.isInteger(slot) || !isNonEmptyString(memberName) || !isNonEmptyString(typeName)) { - throw new Error("CultCache schema catalog member has invalid slot, name, or type."); - } - return { - slot, - memberName, - typeName, - isReference: isReference === true, - isMany: isMany === true, - targetSchemaName: typeof targetSchemaName === "string" ? targetSchemaName : null, - isName: isName === true, - indexAlias: typeof indexAlias === "string" ? indexAlias : null, - }; -} -function decodeV1Record(value, catalogBySchemaId) { - if (!Array.isArray(value)) { - throw new Error("CultCache persisted records must be MessagePack arrays."); - } - const [key = "", schemaId = "", storedAt = "", payload = new Uint8Array()] = value; - if (!isNonEmptyString(key) || !isNonEmptyString(schemaId) || !isNonEmptyString(storedAt)) { - throw new Error("CultCache persisted records must declare key, schemaId, and storedAt."); - } - const catalogEntry = catalogBySchemaId.get(schemaId); - const payloadBytes = normalizePayloadBytes(payload); - return { - key, - schemaId, - schemaName: catalogEntry?.schemaName ?? "", - storedAt, - payloadBytes: payloadBytes.length, - ...previewPayload(payloadBytes), - }; -} -function decodeLegacyEnvelope(value) { - if (!isRecord(value)) { - throw new Error("CultCache legacy envelopes must be MessagePack maps."); - } - const key = value.key; - const schemaName = value.type; - const storedAt = value.storedAt; - if (!isNonEmptyString(key) || !isNonEmptyString(schemaName) || !isNonEmptyString(storedAt)) { - throw new Error("CultCache legacy envelopes must declare key, type, and storedAt."); - } - const schemaId = isNonEmptyString(value.schemaId) ? value.schemaId : schemaName; - const payloadBytes = normalizePayloadBytes(value.payload); - return { - key, - schemaId, - schemaName, - storedAt, - payloadBytes: payloadBytes.length, - ...previewPayload(payloadBytes), - }; -} -function previewPayload(payload) { - try { - return { payloadPreview: toJsonSafe((0, msgpack_1.decode)(payload)) }; - } - catch (error) { - return { - payloadPreview: { - bytes: payload.length, - hexPrefix: [...payload.slice(0, 32)] - .map((value) => value.toString(16).padStart(2, "0")) - .join(" "), - }, - payloadDecodeError: error instanceof Error ? error.message : String(error), - }; - } -} -function normalizePayloadBytes(value) { - if (value instanceof Uint8Array) { - return value; - } - if (isRecord(value) && - value.type === "Buffer" && - Array.isArray(value.data) && - value.data.every(isByte)) { - return Uint8Array.from(value.data); - } - if (Array.isArray(value) && value.every(isByte)) { - return Uint8Array.from(value); - } - throw new Error("CultCache record payload must be binary MessagePack bytes."); -} -function toJsonSafe(value) { - if (typeof value === "bigint") { - return value.toString(); - } - if (value instanceof Uint8Array) { - return { - bytes: value.length, - hexPrefix: [...value.slice(0, 32)] - .map((byte) => byte.toString(16).padStart(2, "0")) - .join(" "), - }; - } - if (Array.isArray(value)) { - return value.map(toJsonSafe); - } - if (isRecord(value)) { - return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, toJsonSafe(entryValue)])); - } - return value; -} -function isV1Snapshot(value) { - return Array.isArray(value) && value[0] === STORE_FORMAT_VERSION; -} -function isRecord(value) { - return typeof value === "object" && value !== null; -} -function isNonEmptyString(value) { - return typeof value === "string" && value.length > 0; -} -function isByte(value) { - return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 255; -} -function compareOrdinal(left, right) { - return left < right ? -1 : left > right ? 1 : 0; -} diff --git a/dist/cult-cache.d.ts b/dist/cult-cache.d.ts deleted file mode 100644 index 7ed56bc..0000000 --- a/dist/cult-cache.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { AnyCultCacheDocumentDefinition, CacheBackingStore, CultCacheDocumentAccessor, CultCacheDocumentRegistry, CultCacheDocumentValue, CultCacheEnvelope } from "./types"; -type BackingStoreTypeReference = string | AnyCultCacheDocumentDefinition; -export declare class CultCacheBuilder { - #private; - withDocumentType(definition: TDefinition): this; - withRegistry(registry: CultCacheDocumentRegistry | Iterable): this; - withBackingStore(store: CacheBackingStore, ...types: BackingStoreTypeReference[]): this; - withGenericStore(store: CacheBackingStore): this; - build(): CultCache; -} -export declare class CultCache { - #private; - static readonly GLOBAL_KEY = "__global__"; - static builder(): CultCacheBuilder; - registerDocumentType(definition: TDefinition): TDefinition; - registerRegistry(registry: CultCacheDocumentRegistry | Iterable): this; - registerNameLookup(definition: TDefinition, accessor: CultCacheDocumentAccessor>): TDefinition; - registerIndex(definition: TDefinition, indexName: string, accessor: CultCacheDocumentAccessor>): TDefinition; - addBackingStore(store: CacheBackingStore, ...types: BackingStoreTypeReference[]): void; - addGenericBackingStore(store: CacheBackingStore): void; - 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; - getByName(definition: TDefinition, name: string): CultCacheDocumentValue | undefined; - getKeyByIndex(definition: TDefinition, indexName: string, value: string): string | undefined; - 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>; - delete(definition: TDefinition, key: string): Promise; - deleteGlobal(definition: TDefinition): Promise; - snapshot(): CultCacheEnvelope[]; -} -export {}; -//# sourceMappingURL=cult-cache.d.ts.map \ No newline at end of file diff --git a/dist/cult-cache.d.ts.map b/dist/cult-cache.d.ts.map deleted file mode 100644 index d3437a9..0000000 --- a/dist/cult-cache.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"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 deleted file mode 100644 index 013b689..0000000 --- a/dist/cult-cache.js +++ /dev/null @@ -1,605 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.CultCache = exports.CultCacheBuilder = void 0; -const msgpack_1 = require("@msgpack/msgpack"); -class CultCacheBuilder { - #cache = new CultCache(); - withDocumentType(definition) { - this.#cache.registerDocumentType(definition); - return this; - } - withRegistry(registry) { - this.#cache.registerRegistry(registry); - return this; - } - withBackingStore(store, ...types) { - this.#cache.addBackingStore(store, ...types); - return this; - } - withGenericStore(store) { - this.#cache.addGenericBackingStore(store); - return this; - } - build() { - return this.#cache; - } -} -exports.CultCacheBuilder = CultCacheBuilder; -class CultCache { - static GLOBAL_KEY = "__global__"; - #definitions = new Map(); - #schemaIdDefinitions = new Map(); - #entries = new Map(); - #typeEntryIds = new Map(); - #nameToKeyMaps = new Map(); - #indexToKeyMaps = new Map(); - #globalKeys = new Map(); - #stores = []; - static builder() { - return new CultCacheBuilder(); - } - registerDocumentType(definition) { - const existing = this.#definitions.get(definition.type); - if (existing && existing.definition !== definition) { - throw new Error(`CultCache already has a different definition registered for type "${definition.type}".`); - } - const registered = existing ?? { - definition, - global: definition.global === true, - formatter: this.#createFormatter(definition), - catalogEntry: this.#createCatalogEntry(definition), - nameAccessor: undefined, - indexAccessors: new Map(), - }; - registered.nameAccessor = definition.name - ? this.#compileAccessor(definition.type, "name", definition.name) - : undefined; - registered.indexAccessors.clear(); - for (const [indexName, accessor] of this.#enumerateDefinitionIndexes(definition)) { - registered.indexAccessors.set(indexName, this.#compileAccessor(definition.type, `index "${indexName}"`, accessor)); - } - this.#definitions.set(definition.type, registered); - for (const schemaId of registered.catalogEntry.compatibleSchemaIds ?? [registered.catalogEntry.schemaId]) { - const existingBySchema = this.#schemaIdDefinitions.get(schemaId); - if (existingBySchema && existingBySchema !== registered) { - throw new Error(`CultCache schema id "${schemaId}" is already registered for type "${existingBySchema.definition.type}".`); - } - this.#schemaIdDefinitions.set(schemaId, registered); - } - this.#rebuildDefinitionLookups(registered); - return definition; - } - registerRegistry(registry) { - const definitions = Symbol.iterator in Object(registry) && !("definitions" in Object(registry)) - ? registry - : registry.definitions; - for (const definition of definitions) { - this.registerDocumentType(definition); - } - return this; - } - registerNameLookup(definition, accessor) { - const registered = this.#requireDefinition(definition); - registered.nameAccessor = this.#compileAccessor(definition.type, "name", accessor); - this.#rebuildNameLookup(registered); - return definition; - } - registerIndex(definition, indexName, accessor) { - if (!indexName || indexName.trim().length === 0) { - throw new Error(`CultCache index names for type "${definition.type}" must be non-empty.`); - } - const registered = this.#requireDefinition(definition); - registered.indexAccessors.set(indexName, this.#compileAccessor(definition.type, `index "${indexName}"`, accessor)); - this.#rebuildIndexLookup(registered, indexName); - return definition; - } - addBackingStore(store, ...types) { - this.#stores.push({ - store, - types: [...new Set(types.map((value) => (typeof value === "string" ? value : value.type)))], - }); - } - addGenericBackingStore(store) { - this.addBackingStore(store); - } - async pullAllBackingStores() { - this.#resetHydratedState(); - for (const registration of this.#stores) { - const entries = await registration.store.pullAll(); - for (const entry of entries) { - const registered = this.#resolveDefinitionForEnvelope(entry); - if (!registered) { - throw new Error(entry.schemaId - ? `No schema is registered for persisted schema id "${entry.schemaId}".` - : `No schema is registered for persisted document type "${entry.type}".`); - } - const payload = this.#cloneBytes(entry.payload); - const value = registered.formatter.decode(payload); - this.#applyHydratedEntry(registered, { - ...entry, - payload, - }, value, "pull"); - } - } - } - get(definition, key) { - this.#requireDefinition(definition); - const entry = this.#entries.get(this.#entryId(definition.type, key)); - return entry ? entry.value : undefined; - } - getRequired(definition, key) { - const value = this.get(definition, key); - if (value === undefined) { - throw new Error(`CultCache has no "${definition.type}" document at key "${key}".`); - } - 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) { - throw new Error(`CultCache document type "${definition.type}" is not marked as global.`); - } - const globalKey = this.#globalKeys.get(definition.type); - return globalKey ? this.get(definition, globalKey) : undefined; - } - getRequiredGlobal(definition) { - const value = this.getGlobal(definition); - if (value === undefined) { - throw new Error(`CultCache has no global "${definition.type}" document.`); - } - 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 = []; - const entryIds = this.#typeEntryIds.get(definition.type); - if (!entryIds) { - return values; - } - for (const entryId of entryIds) { - const entry = this.#entries.get(entryId); - if (entry) { - values.push(entry.value); - } - } - return values; - } - getKeyByName(definition, name) { - const registered = this.#requireDefinition(definition); - if (!registered.nameAccessor) { - return undefined; - } - return this.#nameToKeyMaps.get(definition.type)?.get(name); - } - getIdByName(definition, name) { - return this.getKeyByName(definition, name); - } - getByName(definition, name) { - const key = this.getKeyByName(definition, name); - return key ? this.get(definition, key) : undefined; - } - getKeyByIndex(definition, indexName, value) { - this.#requireDefinition(definition); - return this.#indexToKeyMaps.get(this.#indexId(definition.type, indexName))?.get(value); - } - getIdByIndex(definition, indexName, value) { - return this.getKeyByIndex(definition, indexName, value); - } - getByIndex(definition, indexName, value) { - const key = this.getKeyByIndex(definition, indexName, value); - return key ? this.get(definition, key) : undefined; - } - async put(definition, key, value) { - const registered = this.#requireDefinition(definition); - const payload = registered.formatter.encode(value); - const parsed = registered.formatter.decode(payload); - const entry = { - key, - type: definition.type, - payload: this.#cloneBytes(payload), - storedAt: new Date().toISOString(), - schemaId: registered.catalogEntry.schemaId, - catalogEntry: registered.catalogEntry, - }; - 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 !== 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 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, - schemaId: envelope.schemaId ?? registered.catalogEntry.schemaId, - catalogEntry: envelope.catalogEntry ?? registered.catalogEntry, - }; - 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) { - throw new Error(`CultCache document type "${definition.type}" is not marked as global.`); - } - return this.put(definition, CultCache.GLOBAL_KEY, value); - } - async update(definition, key, updater) { - const current = this.get(definition, key); - return this.put(definition, key, updater(current)); - } - async updateGlobal(definition, updater) { - const current = this.getGlobal(definition); - return this.putGlobal(definition, updater(current)); - } - async delete(definition, key) { - const registered = this.#requireDefinition(definition); - const entry = this.#entries.get(this.#entryId(definition.type, key)); - if (!entry) { - return false; - } - const route = this.#resolveRoute(definition.type); - if (!route.primary) { - throw new Error(`No backing store is registered for document type "${definition.type}".`); - } - const envelope = this.#toEnvelope(entry); - await route.primary.delete(envelope); - await Promise.all(route.mirrors.map(async (mirror) => mirror.delete(envelope))); - this.#removeHydratedEntry(registered, entry); - return true; - } - async deleteGlobal(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.delete(definition, globalKey) : false; - } - snapshot() { - return [...this.#entries.values()].map((entry) => this.#toEnvelope(entry)); - } - #createFormatter(definition) { - const userFormatter = definition.formatter ?? { - encode: (value) => (0, msgpack_1.encode)(value), - decode: (payload) => (0, msgpack_1.decode)(payload), - }; - return { - encode: (value) => { - const parsed = definition.schema.parse(value); - return this.#cloneBytes(userFormatter.encode(parsed)); - }, - decode: (payload) => { - const decoded = userFormatter.decode(this.#cloneBytes(payload)); - return definition.schema.parse(decoded); - }, - }; - } - #createCatalogEntry(definition) { - const schemaName = definition.schemaName ?? definition.type; - const schemaVersion = definition.schemaVersion ?? `${schemaName}.v1`; - const canonicalSchemaJson = definition.canonicalSchemaJson ?? JSON.stringify({ - schemaName, - schemaVersion, - members: [...(definition.members ?? [])] - .sort((left, right) => left.slot - right.slot) - .map((member) => ({ - slot: member.slot, - name: member.memberName, - type: member.typeName, - isReference: member.isReference === true, - many: member.isMany === true, - targetSchemaName: member.targetSchemaName ?? null, - indexAlias: member.indexAlias ?? null, - isName: member.isName === true, - })), - }); - const schemaId = definition.schemaId ?? schemaName; - const compatibleSchemaIds = [ - ...new Set([schemaId, ...(definition.compatibleSchemaIds ?? [])]), - ]; - return { - schemaId, - schemaName, - schemaVersion, - contentHash: definition.contentHash ?? schemaId, - canonicalSchemaJson, - compatibleSchemaIds, - members: [...(definition.members ?? [])] - .sort((left, right) => left.slot - right.slot) - .map((member) => ({ - slot: member.slot, - memberName: member.memberName, - typeName: member.typeName, - isReference: member.isReference === true, - isMany: member.isMany === true, - targetSchemaName: member.targetSchemaName ?? null, - isName: member.isName === true, - indexAlias: member.indexAlias ?? null, - })), - }; - } - #enumerateDefinitionIndexes(definition) { - if (!definition.indexes) { - return []; - } - if (Array.isArray(definition.indexes)) { - return definition.indexes.map((index) => [index.name, index.accessor]); - } - return Object.entries(definition.indexes); - } - #compileAccessor(type, label, accessor) { - if (typeof accessor === "function") { - return (value) => this.#normalizeIndexValue(type, label, accessor(value)); - } - return (value) => { - if (value === null || typeof value !== "object") { - return undefined; - } - const fieldValue = value[accessor]; - return this.#normalizeIndexValue(type, `${label} field "${accessor}"`, fieldValue); - }; - } - #normalizeIndexValue(type, label, value) { - if (value === undefined || value === null) { - return undefined; - } - switch (typeof value) { - case "string": - return value; - case "number": - case "boolean": - case "bigint": - return String(value); - default: - throw new Error(`CultCache ${label} accessor for type "${type}" produced a non-scalar value.`); - } - } - #requireDefinition(definition) { - const registered = this.#definitions.get(definition.type); - if (!registered || registered.definition !== definition) { - throw new Error(`CultCache document type "${definition.type}" is not registered on this cache instance.`); - } - return registered; - } - #resolveDefinitionForEnvelope(entry) { - if (entry.schemaId) { - return this.#schemaIdDefinitions.get(entry.schemaId) ?? this.#definitions.get(entry.type); - } - return this.#definitions.get(entry.type); - } - #resolveRoute(type) { - const typeSpecific = this.#stores.filter((registration) => registration.types.includes(type)); - if (typeSpecific.length > 0) { - return { - primary: typeSpecific[0]?.store, - mirrors: typeSpecific.slice(1).map((registration) => registration.store), - }; - } - const generic = this.#stores.filter((registration) => registration.types.length === 0); - return { - primary: generic[0]?.store, - mirrors: generic.slice(1).map((registration) => registration.store), - }; - } - #applyHydratedEntry(registered, entry, value, source) { - const entryId = this.#entryId(entry.type, entry.key); - const existing = this.#entries.get(entryId); - if (existing) { - this.#removeHydratedEntry(registered, existing); - } - if (registered.global) { - const currentGlobalKey = this.#globalKeys.get(entry.type); - if (currentGlobalKey && currentGlobalKey !== entry.key) { - throw new Error(source === "pull" - ? `CultCache global document type "${entry.type}" has multiple persisted entries.` - : `CultCache global document type "${entry.type}" already has a different key "${currentGlobalKey}".`); - } - } - const hydrated = { - ...entry, - payload: this.#cloneBytes(entry.payload), - schemaId: entry.schemaId ?? registered.catalogEntry.schemaId, - catalogEntry: entry.catalogEntry ?? registered.catalogEntry, - value, - }; - this.#entries.set(entryId, hydrated); - this.#getOrCreateTypeEntrySet(entry.type).add(entryId); - if (registered.global) { - this.#globalKeys.set(entry.type, entry.key); - } - if (registered.nameAccessor) { - const name = registered.nameAccessor(value); - if (name !== undefined) { - this.#getOrCreateNameMap(entry.type).set(name, entry.key); - } - } - for (const [indexName, accessor] of registered.indexAccessors) { - const indexValue = accessor(value); - if (indexValue !== undefined) { - this.#getOrCreateIndexMap(entry.type, indexName).set(indexValue, entry.key); - } - } - } - #removeHydratedEntry(registered, entry) { - const entryId = this.#entryId(entry.type, entry.key); - this.#entries.delete(entryId); - const typeEntryIds = this.#typeEntryIds.get(entry.type); - if (typeEntryIds) { - typeEntryIds.delete(entryId); - if (typeEntryIds.size === 0) { - this.#typeEntryIds.delete(entry.type); - } - } - if (registered.global && this.#globalKeys.get(entry.type) === entry.key) { - this.#globalKeys.delete(entry.type); - } - if (registered.nameAccessor) { - const name = registered.nameAccessor(entry.value); - if (name !== undefined) { - this.#nameToKeyMaps.get(entry.type)?.delete(name); - } - } - for (const [indexName, accessor] of registered.indexAccessors) { - const value = accessor(entry.value); - if (value !== undefined) { - this.#indexToKeyMaps.get(this.#indexId(entry.type, indexName))?.delete(value); - } - } - } - #rebuildDefinitionLookups(registered) { - this.#rebuildNameLookup(registered); - for (const indexName of registered.indexAccessors.keys()) { - this.#rebuildIndexLookup(registered, indexName); - } - } - #rebuildNameLookup(registered) { - this.#nameToKeyMaps.delete(registered.definition.type); - if (!registered.nameAccessor) { - return; - } - const map = this.#getOrCreateNameMap(registered.definition.type); - for (const entry of this.#entriesForType(registered.definition.type)) { - const name = registered.nameAccessor(entry.value); - if (name !== undefined) { - map.set(name, entry.key); - } - } - } - #rebuildIndexLookup(registered, indexName) { - const lookupId = this.#indexId(registered.definition.type, indexName); - this.#indexToKeyMaps.delete(lookupId); - const accessor = registered.indexAccessors.get(indexName); - if (!accessor) { - return; - } - const map = this.#getOrCreateIndexMap(registered.definition.type, indexName); - for (const entry of this.#entriesForType(registered.definition.type)) { - const value = accessor(entry.value); - if (value !== undefined) { - map.set(value, entry.key); - } - } - } - #entriesForType(type) { - const entryIds = this.#typeEntryIds.get(type); - if (!entryIds) { - return []; - } - const entries = []; - for (const entryId of entryIds) { - const entry = this.#entries.get(entryId); - if (entry) { - entries.push(entry); - } - } - return entries; - } - #getOrCreateTypeEntrySet(type) { - let set = this.#typeEntryIds.get(type); - if (!set) { - set = new Set(); - this.#typeEntryIds.set(type, set); - } - return set; - } - #getOrCreateNameMap(type) { - let map = this.#nameToKeyMaps.get(type); - if (!map) { - map = new Map(); - this.#nameToKeyMaps.set(type, map); - } - return map; - } - #getOrCreateIndexMap(type, indexName) { - const lookupId = this.#indexId(type, indexName); - let map = this.#indexToKeyMaps.get(lookupId); - if (!map) { - map = new Map(); - this.#indexToKeyMaps.set(lookupId, map); - } - return map; - } - #toEnvelope(entry) { - return { - key: entry.key, - type: entry.type, - payload: this.#cloneBytes(entry.payload), - storedAt: entry.storedAt, - schemaId: entry.schemaId, - catalogEntry: entry.catalogEntry, - }; - } - #resetHydratedState() { - this.#entries.clear(); - this.#typeEntryIds.clear(); - this.#nameToKeyMaps.clear(); - this.#indexToKeyMaps.clear(); - this.#globalKeys.clear(); - } - #entryId(type, key) { - return `${type}::${key}`; - } - #indexId(type, indexName) { - return `${type}::${indexName}`; - } - #cloneBytes(payload) { - return new Uint8Array(payload); - } -} -exports.CultCache = CultCache; diff --git a/dist/document.d.ts b/dist/document.d.ts deleted file mode 100644 index e797003..0000000 --- a/dist/document.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { AnyCultCacheDocumentDefinition, CultCacheDocumentDefinition, CultCacheDocumentRegistry, CultCacheSchema } from "./types"; -export declare function defineDocumentType(definition: CultCacheDocumentDefinition): CultCacheDocumentDefinition; -export declare function defineDocumentRegistry(...definitions: TDefinitions): CultCacheDocumentRegistry; -//# sourceMappingURL=document.d.ts.map \ No newline at end of file diff --git a/dist/document.d.ts.map b/dist/document.d.ts.map deleted file mode 100644 index c2abff8..0000000 --- a/dist/document.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,8BAA8B,EAC9B,2BAA2B,EAC3B,yBAAyB,EACzB,eAAe,EAChB,MAAM,SAAS,CAAC;AAEjB,wBAAgB,kBAAkB,CAAC,OAAO,SAAS,eAAe,EAChE,UAAU,EAAE,2BAA2B,CAAC,OAAO,CAAC,GAC/C,2BAA2B,CAAC,OAAO,CAAC,CA4DtC;AAED,wBAAgB,sBAAsB,CACpC,YAAY,SAAS,SAAS,8BAA8B,EAAE,EAC9D,GAAG,WAAW,EAAE,YAAY,GAAG,yBAAyB,CAAC,YAAY,CAAC,CAIvE"} \ No newline at end of file diff --git a/dist/document.js b/dist/document.js deleted file mode 100644 index 5b56ceb..0000000 --- a/dist/document.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.defineDocumentType = defineDocumentType; -exports.defineDocumentRegistry = defineDocumentRegistry; -function defineDocumentType(definition) { - if (!definition.type || definition.type.trim().length === 0) { - throw new Error("CultCache document types must declare a non-empty type."); - } - for (const [fieldName, fieldValue] of [ - ["schemaId", definition.schemaId], - ["schemaName", definition.schemaName], - ["schemaVersion", definition.schemaVersion], - ["contentHash", definition.contentHash], - ["canonicalSchemaJson", definition.canonicalSchemaJson], - ]) { - if (fieldValue !== undefined && fieldValue.trim().length === 0) { - throw new Error(`CultCache document type "${definition.type}" declares an empty ${fieldName}.`); - } - } - if (definition.name !== undefined && typeof definition.name !== "string" && typeof definition.name !== "function") { - throw new Error(`CultCache document type "${definition.type}" declares an invalid name accessor.`); - } - if (Array.isArray(definition.indexes)) { - const seen = new Set(); - for (const index of definition.indexes) { - if (!index.name || index.name.trim().length === 0) { - throw new Error(`CultCache document type "${definition.type}" declares an index with an empty name.`); - } - if (seen.has(index.name)) { - throw new Error(`CultCache document type "${definition.type}" declares duplicate index "${index.name}".`); - } - seen.add(index.name); - } - } - else if (definition.indexes) { - for (const name of Object.keys(definition.indexes)) { - if (!name || name.trim().length === 0) { - throw new Error(`CultCache document type "${definition.type}" declares an index with an empty name.`); - } - } - } - if (definition.members) { - const seenSlots = new Set(); - for (const member of definition.members) { - if (!Number.isInteger(member.slot) || member.slot < 0) { - throw new Error(`CultCache document type "${definition.type}" declares an invalid schema member slot.`); - } - if (seenSlots.has(member.slot)) { - throw new Error(`CultCache document type "${definition.type}" declares duplicate schema member slot ${member.slot}.`); - } - seenSlots.add(member.slot); - if (!member.memberName || member.memberName.trim().length === 0) { - throw new Error(`CultCache document type "${definition.type}" declares a schema member with an empty name.`); - } - if (!member.typeName || member.typeName.trim().length === 0) { - throw new Error(`CultCache document type "${definition.type}" declares schema member "${member.memberName}" with an empty type.`); - } - } - } - return Object.freeze({ ...definition }); -} -function defineDocumentRegistry(...definitions) { - return Object.freeze({ - definitions: Object.freeze([...definitions]), - }); -} diff --git a/dist/huginn-eve-dsl.d.ts b/dist/huginn-eve-dsl.d.ts new file mode 100644 index 0000000..4bee7a3 --- /dev/null +++ b/dist/huginn-eve-dsl.d.ts @@ -0,0 +1,3 @@ +import type { CultCacheInspection } from "cultcache-ts/inspection"; +export declare function buildHuginnEveDsl(inspection: CultCacheInspection): string; +//# sourceMappingURL=huginn-eve-dsl.d.ts.map \ No newline at end of file diff --git a/dist/huginn-eve-dsl.d.ts.map b/dist/huginn-eve-dsl.d.ts.map new file mode 100644 index 0000000..d311c37 --- /dev/null +++ b/dist/huginn-eve-dsl.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"huginn-eve-dsl.d.ts","sourceRoot":"","sources":["../src/huginn-eve-dsl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAA0C,MAAM,yBAAyB,CAAC;AAE3G,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,mBAAmB,GAAG,MAAM,CA8BzE"} \ No newline at end of file diff --git a/dist/huginn-eve-dsl.js b/dist/huginn-eve-dsl.js new file mode 100644 index 0000000..2bbb10a --- /dev/null +++ b/dist/huginn-eve-dsl.js @@ -0,0 +1,55 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.buildHuginnEveDsl = buildHuginnEveDsl; +function buildHuginnEveDsl(inspection) { + return [ + "# Huginn emits Eve DSL. Renderers compile and lower it.", + `surface cultcache.huginn.inspector ${quoteDsl("Huginn .cc Inspector")}`, + "version 1", + "", + "collection huginn.catalog.entries", + ...inspection.catalog.map((entry) => `item huginn.catalog.entries ${quoteDsl(renderCatalogEntry(entry))}`), + "", + "collection huginn.records.entries", + ...inspection.records.map((record) => `item huginn.records.entries ${quoteDsl(renderRecord(record))}`), + "", + "card source \"Source Witness\"", + `text ${quoteDsl(`file: ${inspection.filePath}`)}`, + `text ${quoteDsl(`format: ${inspection.format}`)}`, + `text ${quoteDsl(`bytes: ${inspection.fileSizeBytes}`)}`, + "text \".cc bytes are authority; Huginn only emits Eve DSL.\"", + "end", + "", + "card catalog \"Schema Catalog\"", + `text ${quoteDsl(`schemas: ${inspection.catalog.length}`)}`, + "list \"Catalog Entries\" bind huginn.catalog.entries", + "end", + "", + "card records \"Persisted Records\"", + `text ${quoteDsl(`records: ${inspection.records.length}`)}`, + "list \"Record Entries\" bind huginn.records.entries", + "end", + "", + ].join("\n"); +} +function renderCatalogEntry(entry) { + return [ + entry.schemaName, + entry.schemaVersion, + entry.schemaId, + `${entry.members.length} members`, + ].join(" | "); +} +function renderRecord(record) { + const status = record.payloadDecodeError ? `decode error: ${record.payloadDecodeError}` : "decoded"; + return [ + record.key, + record.schemaName, + record.storedAt, + `${record.payloadBytes} bytes`, + status, + ].join(" | "); +} +function quoteDsl(value) { + return JSON.stringify(value); +} diff --git a/dist/index.d.ts b/dist/index.d.ts index 0eb9738..c4ae442 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,7 +1,4 @@ -export { CultCache, CultCacheBuilder } from "./cult-cache"; -export { inspectCultCacheBytes } from "./cult-cache-inspector"; -export { defineDocumentRegistry, defineDocumentType } from "./document"; -export { SingleFileMessagePackBackingStore } from "./single-file-messagepack-backing-store"; -export type { AnyCultCacheDocumentDefinition, CacheBackingStore, CultCacheDocumentAccessor, CultCacheDocumentDefinition, CultCacheDocumentFieldName, CultCacheDocumentFormatter, CultCacheDocumentIndexDefinition, CultCacheDocumentRegistry, CultCacheSchemaCatalogEntry, CultCacheSchemaCatalogMember, CultCacheSchema, CultCacheDocumentValue, CultCacheEnvelope, CultCacheIndexScalar, InferCultCacheSchemaValue, CultCacheStoreRegistration, PushAllOptions, } from "./types"; -export type { CultCacheInspection, InspectedCatalogEntry, InspectedCatalogMember, InspectedRecord, } from "./cult-cache-inspector"; +export { buildHuginnEveDsl } from "./huginn-eve-dsl"; +export { inspectCultCacheBytes } from "cultcache-ts/inspection"; +export type { CultCacheInspection, InspectedCatalogEntry, InspectedCatalogMember, InspectedRecord, } from "cultcache-ts/inspection"; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/index.d.ts.map b/dist/index.d.ts.map index e22edeb..05b291f 100644 --- a/dist/index.d.ts.map +++ b/dist/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,EAAE,iCAAiC,EAAE,MAAM,yCAAyC,CAAC;AAC5F,YAAY,EACV,8BAA8B,EAC9B,iBAAiB,EACjB,yBAAyB,EACzB,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,EAC1B,gCAAgC,EAChC,yBAAyB,EACzB,2BAA2B,EAC3B,4BAA4B,EAC5B,eAAe,EACf,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,yBAAyB,EACzB,0BAA0B,EAC1B,cAAc,GACf,MAAM,SAAS,CAAC;AACjB,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,eAAe,GAChB,MAAM,wBAAwB,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,YAAY,EACV,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,eAAe,GAChB,MAAM,yBAAyB,CAAC"} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index 3a8cee7..c1ce7d8 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,13 +1,7 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.SingleFileMessagePackBackingStore = exports.defineDocumentType = exports.defineDocumentRegistry = exports.inspectCultCacheBytes = exports.CultCacheBuilder = exports.CultCache = void 0; -var cult_cache_1 = require("./cult-cache"); -Object.defineProperty(exports, "CultCache", { enumerable: true, get: function () { return cult_cache_1.CultCache; } }); -Object.defineProperty(exports, "CultCacheBuilder", { enumerable: true, get: function () { return cult_cache_1.CultCacheBuilder; } }); -var cult_cache_inspector_1 = require("./cult-cache-inspector"); -Object.defineProperty(exports, "inspectCultCacheBytes", { enumerable: true, get: function () { return cult_cache_inspector_1.inspectCultCacheBytes; } }); -var document_1 = require("./document"); -Object.defineProperty(exports, "defineDocumentRegistry", { enumerable: true, get: function () { return document_1.defineDocumentRegistry; } }); -Object.defineProperty(exports, "defineDocumentType", { enumerable: true, get: function () { return document_1.defineDocumentType; } }); -var single_file_messagepack_backing_store_1 = require("./single-file-messagepack-backing-store"); -Object.defineProperty(exports, "SingleFileMessagePackBackingStore", { enumerable: true, get: function () { return single_file_messagepack_backing_store_1.SingleFileMessagePackBackingStore; } }); +exports.inspectCultCacheBytes = exports.buildHuginnEveDsl = void 0; +var huginn_eve_dsl_1 = require("./huginn-eve-dsl"); +Object.defineProperty(exports, "buildHuginnEveDsl", { enumerable: true, get: function () { return huginn_eve_dsl_1.buildHuginnEveDsl; } }); +var inspection_1 = require("cultcache-ts/inspection"); +Object.defineProperty(exports, "inspectCultCacheBytes", { enumerable: true, get: function () { return inspection_1.inspectCultCacheBytes; } }); diff --git a/dist/single-file-messagepack-backing-store.d.ts b/dist/single-file-messagepack-backing-store.d.ts deleted file mode 100644 index 6c6518e..0000000 --- a/dist/single-file-messagepack-backing-store.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { CacheBackingStore, CultCacheEnvelope, PushAllOptions } from "./types"; -export declare class SingleFileMessagePackBackingStore implements CacheBackingStore { - #private; - readonly filePath: string; - constructor(filePath: string); - pullAll(): Promise; - push(entry: CultCacheEnvelope): Promise; - delete(entry: CultCacheEnvelope): Promise; - pushAll(entries: CultCacheEnvelope[], options?: PushAllOptions): Promise; -} -//# sourceMappingURL=single-file-messagepack-backing-store.d.ts.map \ No newline at end of file diff --git a/dist/single-file-messagepack-backing-store.d.ts.map b/dist/single-file-messagepack-backing-store.d.ts.map deleted file mode 100644 index 55dce32..0000000 --- a/dist/single-file-messagepack-backing-store.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"single-file-messagepack-backing-store.d.ts","sourceRoot":"","sources":["../src/single-file-messagepack-backing-store.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,iBAAiB,EACjB,iBAAiB,EAGjB,cAAc,EACf,MAAM,SAAS,CAAC;AAsBjB,qBAAa,iCAAkC,YAAW,iBAAiB;;IACzE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAId,QAAQ,EAAE,MAAM;IAItB,OAAO,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IA6DvC,IAAI,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAW7C,MAAM,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAU/C,OAAO,CAAC,OAAO,EAAE,iBAAiB,EAAE,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;CAiDzF"} \ No newline at end of file diff --git a/dist/single-file-messagepack-backing-store.js b/dist/single-file-messagepack-backing-store.js deleted file mode 100644 index e95d270..0000000 --- a/dist/single-file-messagepack-backing-store.js +++ /dev/null @@ -1,311 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SingleFileMessagePackBackingStore = void 0; -const promises_1 = require("node:fs/promises"); -const node_path_1 = require("node:path"); -const msgpack_1 = require("@msgpack/msgpack"); -const zod_1 = require("zod"); -const STORE_FORMAT_VERSION = "cultcache.store.v1"; -const envelopeSchema = zod_1.z.object({ - key: zod_1.z.string().min(1), - type: zod_1.z.string().min(1), - payload: zod_1.z.instanceof(Uint8Array), - storedAt: zod_1.z.string().min(1), - schemaId: zod_1.z.string().min(1).optional(), -}); -const envelopeArraySchema = zod_1.z.array(envelopeSchema); -const legacyEnvelopeArraySchema = zod_1.z.array(zod_1.z.object({ - key: zod_1.z.string().min(1), - type: zod_1.z.string().min(1), - payload: zod_1.z.unknown(), - storedAt: zod_1.z.string().min(1), -})); -class SingleFileMessagePackBackingStore { - filePath; - #writeQueue = Promise.resolve(); - constructor(filePath) { - this.filePath = (0, node_path_1.resolve)(filePath); - } - async pullAll() { - try { - const data = await (0, promises_1.readFile)(this.filePath); - if (data.length === 0) { - return []; - } - const decoded = (0, msgpack_1.decode)(data); - const snapshot = decodeSnapshot(decoded); - if (snapshot) { - return snapshot.records.map((record) => { - const catalogEntry = snapshot.catalogBySchemaId.get(record.schemaId); - if (!catalogEntry) { - throw new Error(`CultCache persisted record "${record.key}" references missing schema id "${record.schemaId}".`); - } - return { - key: record.key, - type: catalogEntry.schemaName, - schemaId: record.schemaId, - catalogEntry, - payload: record.payload, - storedAt: record.storedAt, - }; - }); - } - const legacy = decodeLegacyEnvelopeArray(decoded); - if (legacy) { - let repairedLegacyPayload = false; - const normalized = legacy.map((entry) => { - const payload = normalizePayload(entry.payload); - if (payload !== entry.payload) { - repairedLegacyPayload = true; - } - return { - ...entry, - payload, - }; - }); - const parsed = envelopeArraySchema.parse(normalized); - if (repairedLegacyPayload) { - await this.#writeAll(parsed); - } - return parsed; - } - throw new Error(`CultCache file ${this.filePath} is not a recognized CultCache MessagePack store.`); - } - catch (error) { - const code = error.code; - if (code === "ENOENT") { - return []; - } - throw error; - } - } - async push(entry) { - await this.#enqueue(async () => { - const existing = await this.pullAll(); - const filtered = existing.filter((candidate) => !(candidate.type === entry.type && candidate.key === entry.key)); - filtered.push(entry); - await this.#writeAll(filtered); - }); - } - async delete(entry) { - await this.#enqueue(async () => { - const existing = await this.pullAll(); - const filtered = existing.filter((candidate) => !(candidate.type === entry.type && candidate.key === entry.key)); - await this.#writeAll(filtered); - }); - } - async pushAll(entries, options = {}) { - await this.#enqueue(async () => { - if (options.soft) { - try { - await (0, promises_1.readFile)(this.filePath); - return; - } - catch (error) { - const code = error.code; - if (code !== "ENOENT") { - throw error; - } - } - } - await this.#writeAll(entries); - }); - } - async #enqueue(operation) { - let result; - const next = this.#writeQueue.then(async () => { - result = await operation(); - }); - this.#writeQueue = next.then(() => undefined, () => undefined); - await next; - return result; - } - async #writeAll(entries) { - await (0, promises_1.mkdir)((0, node_path_1.dirname)(this.filePath), { recursive: true }); - const tempPath = `${this.filePath}.tmp-${process.pid}-${Date.now()}-${Math.random() - .toString(36) - .slice(2)}`; - try { - await (0, promises_1.writeFile)(tempPath, (0, msgpack_1.encode)(encodeSnapshot(entries))); - await (0, promises_1.rename)(tempPath, this.filePath); - } - catch (error) { - await (0, promises_1.rm)(tempPath, { force: true }).catch(() => undefined); - throw error; - } - } -} -exports.SingleFileMessagePackBackingStore = SingleFileMessagePackBackingStore; -function encodeSnapshot(entries) { - const catalog = [...catalogEntriesFor(entries).values()] - .sort((left, right) => compareOrdinal(left.schemaName, right.schemaName)); - const records = [...entries] - .sort((left, right) => compareOrdinal(left.key, right.key)) - .map((entry) => [ - entry.key, - schemaIdForEnvelope(entry), - entry.storedAt, - entry.payload, - ]); - return [ - STORE_FORMAT_VERSION, - catalog.map(encodeCatalogEntry), - records, - ]; -} -function catalogEntriesFor(entries) { - const catalog = new Map(); - for (const entry of entries) { - const schemaId = schemaIdForEnvelope(entry); - if (!catalog.has(schemaId)) { - catalog.set(schemaId, entry.catalogEntry ?? { - schemaId, - schemaName: entry.type, - schemaVersion: `${entry.type}.v1`, - contentHash: schemaId, - canonicalSchemaJson: JSON.stringify({ - schemaName: entry.type, - schemaVersion: `${entry.type}.v1`, - members: [], - }), - compatibleSchemaIds: [schemaId], - members: [], - }); - } - } - return catalog; -} -function schemaIdForEnvelope(entry) { - return entry.schemaId ?? entry.type; -} -function encodeCatalogEntry(entry) { - return [ - entry.schemaId, - entry.schemaName, - entry.schemaVersion, - entry.contentHash, - entry.canonicalSchemaJson, - [...(entry.compatibleSchemaIds ?? [entry.schemaId])], - [...(entry.members ?? [])].map(encodeCatalogMember), - ]; -} -function encodeCatalogMember(member) { - return [ - member.slot, - member.memberName, - member.typeName, - member.isReference === true, - member.isMany === true, - member.targetSchemaName ?? null, - member.isName === true, - member.indexAlias ?? null, - ]; -} -function decodeSnapshot(decoded) { - if (!Array.isArray(decoded) || decoded.length === 0 || decoded[0] !== STORE_FORMAT_VERSION) { - return undefined; - } - const catalogRaw = decoded[1]; - const recordsRaw = decoded[2]; - if (!Array.isArray(catalogRaw) || !Array.isArray(recordsRaw)) { - throw new Error("CultCache v1 snapshot must contain a schema catalog and record array."); - } - const catalogBySchemaId = new Map(); - for (const entry of catalogRaw) { - const catalogEntry = decodeCatalogEntry(entry); - catalogBySchemaId.set(catalogEntry.schemaId, catalogEntry); - for (const compatibleSchemaId of catalogEntry.compatibleSchemaIds ?? []) { - catalogBySchemaId.set(compatibleSchemaId, catalogEntry); - } - } - const records = recordsRaw.map(decodeRecord); - return { catalogBySchemaId, records }; -} -function decodeCatalogEntry(value) { - if (!Array.isArray(value)) { - throw new Error("CultCache schema catalog entries must be MessagePack arrays."); - } - const [schemaId = "", schemaName = "", schemaVersion = "", contentHash = "", canonicalSchemaJson = "", compatibleSchemaIds = [], members = [],] = value; - if (!isNonEmptyString(schemaId) || !isNonEmptyString(schemaName)) { - throw new Error("CultCache schema catalog entries must declare schemaId and schemaName."); - } - if (!Array.isArray(compatibleSchemaIds) || !compatibleSchemaIds.every(isNonEmptyString)) { - throw new Error(`CultCache schema catalog entry "${schemaId}" has invalid compatible schema ids.`); - } - if (!Array.isArray(members)) { - throw new Error(`CultCache schema catalog entry "${schemaId}" has invalid members.`); - } - return { - schemaId, - schemaName, - schemaVersion: isNonEmptyString(schemaVersion) ? schemaVersion : `${schemaName}.v1`, - contentHash: isNonEmptyString(contentHash) ? contentHash : schemaId, - canonicalSchemaJson: typeof canonicalSchemaJson === "string" ? canonicalSchemaJson : "", - compatibleSchemaIds, - members: members.map(decodeCatalogMember), - }; -} -function decodeCatalogMember(value) { - if (!Array.isArray(value)) { - throw new Error("CultCache schema catalog members must be MessagePack arrays."); - } - const [slot = -1, memberName = "", typeName = "", isReference = false, isMany = false, targetSchemaName = null, isName = false, indexAlias = null,] = value; - if (!Number.isInteger(slot) || slot < 0 || !isNonEmptyString(memberName) || !isNonEmptyString(typeName)) { - throw new Error("CultCache schema catalog member has invalid slot, name, or type."); - } - return { - slot, - memberName, - typeName, - isReference: isReference === true, - isMany: isMany === true, - targetSchemaName: typeof targetSchemaName === "string" ? targetSchemaName : null, - isName: isName === true, - indexAlias: typeof indexAlias === "string" ? indexAlias : null, - }; -} -function decodeRecord(value) { - if (!Array.isArray(value)) { - throw new Error("CultCache persisted records must be MessagePack arrays."); - } - const [key = "", schemaId = "", storedAt = "", payload = new Uint8Array()] = value; - if (!isNonEmptyString(key) || !isNonEmptyString(schemaId) || !isNonEmptyString(storedAt)) { - throw new Error("CultCache persisted records must declare key, schemaId, and storedAt."); - } - return { - key, - schemaId, - storedAt, - payload: normalizePayload(payload), - }; -} -function decodeLegacyEnvelopeArray(decoded) { - if (!Array.isArray(decoded)) { - return undefined; - } - return legacyEnvelopeArraySchema.parse(decoded); -} -function normalizePayload(payload) { - if (payload instanceof Uint8Array) { - return payload; - } - if (isObject(payload) && - payload.type === "Buffer" && - Array.isArray(payload.data) && - payload.data.every((value) => Number.isInteger(value) && value >= 0 && value <= 255)) { - return Uint8Array.from(payload.data); - } - if (Array.isArray(payload) && payload.every((value) => Number.isInteger(value) && value >= 0 && value <= 255)) { - return Uint8Array.from(payload); - } - return (0, msgpack_1.encode)(payload); -} -function isObject(value) { - return typeof value === "object" && value !== null; -} -function isNonEmptyString(value) { - return typeof value === "string" && value.length > 0; -} -function compareOrdinal(left, right) { - return left < right ? -1 : left > right ? 1 : 0; -} diff --git a/dist/types.d.ts b/dist/types.d.ts deleted file mode 100644 index c111ca5..0000000 --- a/dist/types.d.ts +++ /dev/null @@ -1,76 +0,0 @@ -export type CultCacheIndexScalar = string | number | boolean | bigint; -export interface CultCacheSchema { - parse(input: unknown): TValue; -} -export type InferCultCacheSchemaValue = TSchema extends CultCacheSchema ? TValue : never; -export type CultCacheDocumentFieldName = TValue extends object ? Extract : never; -export type CultCacheDocumentAccessor = CultCacheDocumentFieldName | ((value: TValue) => CultCacheIndexScalar | null | undefined); -export interface CultCacheDocumentFormatter { - encode(value: TValue): Uint8Array; - decode(payload: Uint8Array): TValue; -} -export interface CultCacheSchemaCatalogMember { - slot: number; - memberName: string; - typeName: string; - isReference?: boolean; - isMany?: boolean; - targetSchemaName?: string | null; - isName?: boolean; - indexAlias?: string | null; -} -export interface CultCacheSchemaCatalogEntry { - schemaId: string; - schemaName: string; - schemaVersion: string; - contentHash: string; - canonicalSchemaJson: string; - compatibleSchemaIds?: readonly string[]; - members?: readonly CultCacheSchemaCatalogMember[]; -} -export interface CultCacheDocumentIndexDefinition { - name: string; - accessor: CultCacheDocumentAccessor; -} -export interface CultCacheDocumentDefinition { - type: string; - schema: TSchema; - schemaId?: string; - schemaName?: string; - schemaVersion?: string; - contentHash?: string; - canonicalSchemaJson?: string; - compatibleSchemaIds?: readonly string[]; - members?: readonly CultCacheSchemaCatalogMember[]; - global?: boolean; - name?: CultCacheDocumentAccessor>; - indexes?: Readonly>>> | readonly CultCacheDocumentIndexDefinition>[]; - formatter?: CultCacheDocumentFormatter>; -} -export type CultCacheDocumentValue = InferCultCacheSchemaValue; -export type AnyCultCacheDocumentDefinition = CultCacheDocumentDefinition; -export interface CultCacheDocumentRegistry { - readonly definitions: TDefinitions; -} -export interface CultCacheEnvelope { - key: string; - type: string; - payload: Uint8Array; - storedAt: string; - schemaId?: string; - catalogEntry?: CultCacheSchemaCatalogEntry; -} -export interface PushAllOptions { - soft?: boolean; -} -export interface CacheBackingStore { - pullAll(): Promise; - push(entry: CultCacheEnvelope): Promise; - delete(entry: CultCacheEnvelope): Promise; - pushAll?(entries: CultCacheEnvelope[], options?: PushAllOptions): Promise; -} -export interface CultCacheStoreRegistration { - store: CacheBackingStore; - types: string[]; -} -//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/dist/types.d.ts.map b/dist/types.d.ts.map deleted file mode 100644 index abafae1..0000000 --- a/dist/types.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/types.js b/dist/types.js deleted file mode 100644 index c8ad2e5..0000000 --- a/dist/types.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/electron-builder.json b/electron-builder.json deleted file mode 100644 index 28df4bf..0000000 --- a/electron-builder.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "appId": "com.gamecult.cultcache.inspector", - "productName": "Huginn", - "executableName": "Huginn", - "asar": false, - "icon": "inspector/public/hugin.ico", - "extraMetadata": { - "main": "electron/main.cjs" - }, - "files": [ - "electron/main.cjs", - "dist-inspector/**/*" - ], - "directories": { - "output": "release-inspector" - }, - "win": { - "signAndEditExecutable": false, - "target": ["portable"], - "artifactName": "Huginn-${version}.${ext}" - }, - "mac": { - "target": ["dir"] - }, - "linux": { - "target": ["dir"] - } -} diff --git a/electron/main.cjs b/electron/main.cjs deleted file mode 100644 index 2cf76b6..0000000 --- a/electron/main.cjs +++ /dev/null @@ -1,72 +0,0 @@ -const { app, BrowserWindow } = require("electron"); -const fs = require("node:fs"); -const os = require("node:os"); -const path = require("node:path"); - -const devServer = process.env.CULTCACHE_INSPECTOR_DEV_SERVER; -app.setName("Huginn"); -const logPath = path.join(app.getPath("userData"), "huginn-renderer.log"); - -function writeLog(message) { - const line = `[${new Date().toISOString()}] ${message}${os.EOL}`; - try { - fs.mkdirSync(path.dirname(logPath), { recursive: true }); - fs.appendFileSync(logPath, line); - } catch { - // Logging must never be the reason the inspector refuses to open. - } - console.log(line.trimEnd()); -} - -function createWindow() { - const window = new BrowserWindow({ - width: 1180, - height: 780, - minWidth: 820, - minHeight: 560, - title: "Huginn", - icon: path.join(__dirname, "..", "dist-inspector", "hugin-64.png"), - backgroundColor: "#05090d", - autoHideMenuBar: true, - webPreferences: { - contextIsolation: true, - nodeIntegration: false, - sandbox: true, - }, - }); - - window.webContents.on("console-message", (_event, level, message, line, sourceId) => { - writeLog(`renderer console[${level}] ${sourceId}:${line} ${message}`); - }); - window.webContents.on("did-fail-load", (_event, code, description, validatedUrl) => { - writeLog(`renderer failed load ${code} ${description} ${validatedUrl}`); - }); - window.webContents.on("render-process-gone", (_event, details) => { - writeLog(`renderer process gone ${details.reason} exitCode=${details.exitCode}`); - }); - window.webContents.on("did-finish-load", () => { - writeLog(`renderer loaded ${window.webContents.getURL()}`); - }); - - if (devServer) { - void window.loadURL(devServer); - } else { - void window.loadFile(path.join(__dirname, "..", "dist-inspector", "index.html")); - } -} - -app.whenReady().then(() => { - createWindow(); - - app.on("activate", () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); - } - }); -}); - -app.on("window-all-closed", () => { - if (process.platform !== "darwin") { - app.quit(); - } -}); diff --git a/inspector/index.html b/inspector/index.html deleted file mode 100644 index 73efecc..0000000 --- a/inspector/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Huginn - - -
- - - diff --git a/inspector/public/hugin-256.png b/inspector/public/hugin-256.png deleted file mode 100644 index 4ff5743..0000000 Binary files a/inspector/public/hugin-256.png and /dev/null differ diff --git a/inspector/public/hugin-64.png b/inspector/public/hugin-64.png deleted file mode 100644 index 9437fb0..0000000 Binary files a/inspector/public/hugin-64.png and /dev/null differ diff --git a/inspector/public/hugin.ico b/inspector/public/hugin.ico deleted file mode 100644 index 95bd886..0000000 Binary files a/inspector/public/hugin.ico and /dev/null differ diff --git a/inspector/public/hugin.png b/inspector/public/hugin.png deleted file mode 100644 index bb817ad..0000000 Binary files a/inspector/public/hugin.png and /dev/null differ diff --git a/inspector/public/huginn-fieldpack.png b/inspector/public/huginn-fieldpack.png deleted file mode 100644 index 6a78b8b..0000000 Binary files a/inspector/public/huginn-fieldpack.png and /dev/null differ diff --git a/inspector/public/huginn-surface.png b/inspector/public/huginn-surface.png deleted file mode 100644 index d7b62bd..0000000 Binary files a/inspector/public/huginn-surface.png and /dev/null differ diff --git a/inspector/src/HuginnFieldCanvas.tsx b/inspector/src/HuginnFieldCanvas.tsx deleted file mode 100644 index 08a3881..0000000 --- a/inspector/src/HuginnFieldCanvas.tsx +++ /dev/null @@ -1,1380 +0,0 @@ -import { useEffect, useRef } from "react"; - -type Brush = { - x: number; - y: number; - size: number; - r: number; - g: number; - b: number; - alpha: number; - energy: number; - curve: number; - gx: number; - gy: number; -}; - -type FieldSample = { - width: number; - height: number; - rgba: Uint8ClampedArray; - fieldRgba: Uint8ClampedArray; - luma: Float32Array; - alpha: Float32Array; - analysis: Float32Array; - flowX: Float32Array; - flowY: Float32Array; - fieldStrength: Float32Array; -}; - -type PyramidLevel = { - width: number; - height: number; - values: Float32Array; - energy: Float32Array; - gx: Float32Array; - gy: Float32Array; - curve: Float32Array; -}; - -const FIELD_SIZE = 256; -const MAX_DEPTH = 6; -const MIN_BRUSH_SIZE = 4; -const ENERGY_SPLIT_THRESHOLD = 0.038; -const PARTICLE_COUNT = new URLSearchParams(globalThis.location?.search ?? "").has("smoke") ? 8_192 : 1_048_576; -const PARTICLE_STRIDE_FLOATS = 12; -const MAX_NODE_ENVELOPES = 96; -const STARDUST_MAX_LEVEL = 13; -const STARDUST_COARSE_PARTICLES_PER_CELL = 2; -const STARDUST_FINE_PARTICLES_PER_CELL = 6; - -type NodeEnvelope = { - id: string; - x: number; - y: number; - radius: number; - strength: number; -}; - -type GraphViewportTransform = { - x: number; - y: number; - scale: number; - bounds: { - x: number; - y: number; - width: number; - height: number; - }; -}; - -const gpuParticleComputeShader = /* wgsl */ ` -struct Particle { - position: vec2f, - velocity: vec2f, - color: vec4f, - seed: f32, - size: f32, - life: f32, - pad: f32, -}; - -struct SimUniforms { - time: f32, - dt: f32, - width: f32, - height: f32, - particleCount: f32, - flowGain: f32, - alpha: f32, - detail: f32, - envelopeCount: f32, - nodeGain: f32, - worldX: f32, - worldY: f32, - worldWidth: f32, - worldHeight: f32, - viewX: f32, - viewY: f32, - viewScale: f32, - pad1: f32, -}; - -struct VisibleBand { - level: u32, - minX: u32, - minY: u32, - cellsX: u32, - cellsY: u32, - pairCount: u32, - weight: f32, - particlesPerCell: u32, -}; - -@group(0) @binding(0) var particles: array; -@group(0) @binding(1) var uniforms: SimUniforms; -@group(0) @binding(2) var fieldSampler: sampler; -@group(0) @binding(3) var fieldTexture: texture_2d; -@group(0) @binding(4) var nodeEnvelopes: array; -@group(0) @binding(5) var albedoTexture: texture_2d; - -fn hash32(value: u32) -> u32 { - var x = value; - x = (x ^ 61u) ^ (x >> 16u); - x = x * 9u; - x = x ^ (x >> 4u); - x = x * 0x27d4eb2du; - x = x ^ (x >> 15u); - return x; -} - -fn hashPair(a: u32, b: u32) -> u32 { - var x = hash32(a); - var y = b; - y = y - (y << 6u); - y = y ^ (x >> 17u); - y = y - (y << 9u); - y = y ^ (x << 4u); - y = y - (y << 3u); - y = y ^ (y << 10u); - y = y ^ (y >> 15u); - return x ^ y; -} - -fn randomFirst(a: u32, b: u32) -> f32 { - return f32(hashPair(a, b) & 0x00ffffffu) / 16777216.0; -} - -fn xorshift(value: u32) -> u32 { - var state = value; - state = state ^ (state << 13u); - state = state ^ (state >> 17u); - state = state ^ (state << 5u); - return state; -} - -fn randomValue(state: u32) -> f32 { - return f32(state & 0x00ffffffu) / 16777216.0; -} - -fn fadeCurve(t: f32) -> f32 { - return t * t * t * (t * (6.0 * t - 15.0) + 10.0); -} - -fn grad(seed: u32, p: f32) -> f32 { - if ((hash32(seed) & 1u) == 0u) { - return p; - } - return -p; -} - -fn perlinNoise1D(p: f32, seed: u32) -> f32 { - let pi = floor(p); - let pf = p - pi; - let cell = u32(max(pi, 0.0)); - let w = fadeCurve(pf); - return mix(grad(cell ^ seed, pf), grad((cell + 1u) ^ seed, pf - 1.0), w) * 2.0; -} - -fn tri(value: f32) -> f32 { - return abs(fract(value) - 0.5); -} - -fn tri3(point: vec3f) -> vec3f { - return vec3f( - tri(point.z + tri(point.y)), - tri(point.z + tri(point.x)), - tri(point.y + tri(point.x)) - ); -} - -fn triFlow(point: vec2f, seed: f32) -> vec2f { - let p1 = normalize(tri3(vec3f(point.x, seed, point.y)) * 2.0 - vec3f(1.0)); - let p2 = normalize(tri3(vec3f(point.x, seed, point.y) * 1.6180339) * 2.0 - vec3f(1.0)); - let c = cross(p1, p2); - return normalize(c.xz + vec2f(0.0001, 0.0)); -} - -fn parabola(x: f32, k: f32) -> f32 { - return pow(max(0.0, 4.0 * x * (1.0 - x)), k); -} - -fn samplePacked(uv: vec2f) -> vec4f { - return textureSampleLevel(fieldTexture, fieldSampler, clamp(uv, vec2f(0.001), vec2f(0.999)), 0.0); -} - -fn sampleAlbedo(uv: vec2f) -> vec4f { - return textureSampleLevel(albedoTexture, fieldSampler, clamp(uv, vec2f(0.001), vec2f(0.999)), 0.0); -} - -fn channelGradient(uv: vec2f, channel: u32) -> vec2f { - let step = vec2f(1.0 / 256.0, 1.0 / 256.0) / max(uniforms.detail, 1.0); - let left = samplePacked(uv - vec2f(step.x, 0.0)); - let right = samplePacked(uv + vec2f(step.x, 0.0)); - let up = samplePacked(uv - vec2f(0.0, step.y)); - let down = samplePacked(uv + vec2f(0.0, step.y)); - if (channel == 2u) { - return vec2f(right.b - left.b, down.b - up.b); - } - return vec2f(right.a - left.a, down.a - up.a); -} - -fn visibleUvBounds() -> vec4f { - let viewScale = max(uniforms.viewScale, 0.0001); - let worldMin = (vec2f(0.0, 0.0) - vec2f(uniforms.viewX, uniforms.viewY)) / viewScale; - let worldMax = (vec2f(uniforms.width, uniforms.height) - vec2f(uniforms.viewX, uniforms.viewY)) / viewScale; - let uvMin = clamp((worldMin - vec2f(uniforms.worldX, uniforms.worldY)) / vec2f(uniforms.worldWidth, uniforms.worldHeight), vec2f(0.0), vec2f(1.0)); - let uvMax = clamp((worldMax - vec2f(uniforms.worldX, uniforms.worldY)) / vec2f(uniforms.worldWidth, uniforms.worldHeight), vec2f(0.0), vec2f(1.0)); - return vec4f(min(uvMin, uvMax), max(uvMin, uvMax)); -} - -fn stardustLevelValue() -> f32 { - let screenSide = max(uniforms.worldWidth, uniforms.worldHeight) * max(uniforms.viewScale, 0.0001); - return clamp(log2(max(screenSide / 7.0, 1.0)) + uniforms.detail * 0.72, 4.0, f32(${STARDUST_MAX_LEVEL})); -} - -fn visibleBand(level: u32, particlesPerCell: u32, weight: f32) -> VisibleBand { - let span = 1u << level; - let uv = visibleUvBounds(); - let margin = 1.0 / f32(span); - let minUv = clamp(uv.xy - vec2f(margin), vec2f(0.0), vec2f(1.0)); - let maxUv = clamp(uv.zw + vec2f(margin), vec2f(0.0), vec2f(1.0)); - let minX = min(u32(floor(minUv.x * f32(span))), span - 1u); - let minY = min(u32(floor(minUv.y * f32(span))), span - 1u); - let maxX = min(u32(ceil(maxUv.x * f32(span))), span); - let maxY = min(u32(ceil(maxUv.y * f32(span))), span); - let cellsX = max(1u, maxX - minX); - let cellsY = max(1u, maxY - minY); - let visibleCells = cellsX * cellsY; - return VisibleBand(level, minX, minY, cellsX, cellsY, visibleCells * particlesPerCell, weight, particlesPerCell); -} - -@compute @workgroup_size(128) -fn updateParticles(@builtin(global_invocation_id) id: vec3u) { - let index = id.x; - if (index >= u32(uniforms.particleCount)) { - return; - } - - var particle = particles[index]; - let pairIndex = index / 2u; - let companion = index - pairIndex * 2u; - let pairCount = max(1u, u32(uniforms.particleCount * 0.5)); - let levelValue = stardustLevelValue(); - let coarseLevel = u32(floor(levelValue)); - let fineLevel = min(coarseLevel + 1u, ${STARDUST_MAX_LEVEL}u); - let fineBlend = smoothstep(0.18, 0.86, fract(levelValue)); - let coarseBand = visibleBand(coarseLevel, ${STARDUST_COARSE_PARTICLES_PER_CELL}u, 1.0 - fineBlend * 0.72); - let fineBand = visibleBand(fineLevel, ${STARDUST_FINE_PARTICLES_PER_CELL}u, fineBlend); - let coarsePairs = min(coarseBand.pairCount, max(1u, pairCount / 3u)); - let finePairs = min(fineBand.pairCount, pairCount - coarsePairs); - let activePairs = coarsePairs + finePairs; - if (pairIndex >= activePairs) { - particle.color = vec4f(0.0); - particle.size = 0.0; - particles[index] = particle; - return; - } - - var band = coarseBand; - var localPair = pairIndex; - if (pairIndex >= coarsePairs) { - band = fineBand; - localPair = pairIndex - coarsePairs; - } - - let level = band.level; - let span = 1u << level; - let particleInCell = localPair % band.particlesPerCell; - let cellOrdinal = localPair / band.particlesPerCell; - let cellX = band.minX + (cellOrdinal % band.cellsX); - let cellY = band.minY + (cellOrdinal / band.cellsX); - let cell = vec2f(f32(cellX), f32(cellY)); - let offset = 65535u; - let seedU = hashPair(level * 73856093u + cellX * 19349663u + particleInCell * 119954089u, cellY * 83492791u + companion * 2654435761u); - var rngState = hashPair(cellX + offset + particleInCell * 251u, cellY + offset + level * 509u); - let firstRandom = randomFirst(cellX + offset + particleInCell * 251u, cellY + offset + level * 509u); - rngState = xorshift(rngState); - let period = 5.2 + randomValue(rngState) * 1.8; - rngState = xorshift(rngState); - let jitterX = perlinNoise1D(randomValue(rngState) * 1024.0, seedU ^ 17u); - rngState = xorshift(rngState); - let jitterY = perlinNoise1D(randomValue(rngState) * 1024.0, seedU ^ 29u); - rngState = xorshift(rngState); - let triSeed = randomValue(rngState); - rngState = xorshift(rngState); - let sizeRandom = randomValue(rngState); - let lifetime = fract(uniforms.time / period + firstRandom); - let spacing = 1.0 / max(f32(span), 1.0); - let base = clamp( - (cell + vec2f(0.5)) * spacing + - vec2f(jitterX, jitterY) * spacing * 0.58, - vec2f(0.001), - vec2f(0.999) - ); - let sourceAlbedo = sampleAlbedo(base); - - let startPacked = samplePacked(base); - var packed = startPacked; - let levelDetail = f32(level) / f32(${STARDUST_MAX_LEVEL}u); - let detail = clamp(uniforms.detail, 0.75, 2.0); - let fieldVector = startPacked.rg * 2.0 - vec2f(1.0); - let fieldDirection = normalize(fieldVector + vec2f(0.0001, 0.0)); - let curvatureGradient = channelGradient(base, 2u); - let tangent = normalize(vec2f(-curvatureGradient.y, curvatureGradient.x) + fieldDirection * 0.28 + vec2f(0.0001, 0.0)); - let sourceStructure = startPacked.b; - let sourceEmission = startPacked.a; - let domainNoise = triFlow(base * (14.0 + sourceStructure * 18.0) - vec2f(0.0, uniforms.time * (0.026 + sourceEmission * 0.018)), triSeed); - let fieldBlend = normalize(fieldDirection * (0.56 + sourceEmission * 0.18) + tangent * (0.26 + sourceStructure * 0.36) + domainNoise * (0.13 + sourceStructure * 0.27)); - var nodePush = vec2f(0.0); - var nodeHeat = 0.0; - for (var envelopeIndex = 0u; envelopeIndex < ${MAX_NODE_ENVELOPES}u; envelopeIndex = envelopeIndex + 1u) { - if (f32(envelopeIndex) >= uniforms.envelopeCount) { - break; - } - let envelope = nodeEnvelopes[envelopeIndex]; - let delta = base - envelope.xy; - let radius = max(envelope.z, 0.008); - let normalized = dot(delta, delta) / (radius * radius); - let edge = exp(-4.0); - let gaussian = exp(-4.0 * normalized); - let influence = pow(clamp((gaussian - edge) / max(1.0 - edge, 0.000001), 0.0, 1.0), 0.92) * envelope.w * uniforms.nodeGain; - let away = normalize(delta + vec2f(0.0001, 0.0)); - let swirl = vec2f(-away.y, away.x); - nodePush += (swirl * 0.72 + away * 0.18) * influence; - nodeHeat += influence; - } - - let flowLine = normalize(fieldBlend + nodePush); - var uv = clamp(base - flowLine * lifetime * (uniforms.flowGain / 24.0) * (0.009 + sourceEmission * 0.030 + sourceStructure * 0.016) * spacing, vec2f(0.001), vec2f(0.999)); - packed = samplePacked(uv); - - particle.position = uv; - let advectedFlow = packed.rg * 2.0 - vec2f(1.0); - let advectedShear = length(advectedFlow - fieldVector); - let emission = clamp(sourceEmission * 0.42 + packed.a * 0.78, 0.0, 1.0); - let structure = clamp(sourceStructure * 0.52 + packed.b * 0.62, 0.0, 1.0); - particle.velocity = flowLine * (3.2 + emission * 10.5 + structure * 5.4 + advectedShear * 2.2); - particle.life = lifetime; - - let flowAxis = abs(fieldVector.x - fieldVector.y); - let lifeFade = parabola(lifetime, 2.0); - let phase = lifetime * 6.28318530718 + f32(companion) * 1.57079632679; - let pairFade = pow(clamp(sin(phase) * 0.5 + 0.5, 0.0, 1.0), 1.35); - let heat = clamp(emission * 0.82 + structure * 0.48 + advectedShear * 0.22 + nodeHeat * 0.38 + lifeFade * 0.16, 0.0, 1.0); - let brass = clamp((structure - emission * 0.22) * 1.22 + flowAxis * 0.22, 0.0, 1.0); - let cyan = vec3f(0.07 + emission * 0.24, 0.40 + heat * 0.50, 0.52 + emission * 0.72); - let gold = vec3f(0.96, 0.65 + structure * 0.14, 0.24); - let violet = vec3f(0.40 + sourceAlbedo.r * 0.18, 0.14 + sourceAlbedo.g * 0.08, 0.54 + sourceAlbedo.b * 0.24); - let deep = vec3f(0.012, 0.052, 0.065); - let albedoLift = mix(deep, sourceAlbedo.rgb, clamp(sourceAlbedo.a * (0.64 + emission * 0.30 + structure * 0.16), 0.0, 1.0)); - let fieldTint = mix(mix(cyan, gold, brass * 0.34), violet, clamp(sourceStructure * (1.0 - sourceEmission) * 0.28, 0.0, 0.28)); - let emissiveInk = sourceAlbedo.rgb * (0.25 + emission * 0.72) + fieldTint * (0.20 + structure * 0.22); - let rgb = mix(albedoLift, emissiveInk, clamp(0.16 + heat * 0.28 + sourceAlbedo.a * 0.18, 0.0, 0.74)); - particle.color = vec4f( - rgb, - uniforms.alpha * (0.0022 + emission * 0.013 + structure * 0.006 + advectedShear * 0.003 + nodeHeat * 0.0025) * lifeFade * pairFade * band.weight * (0.72 + levelDetail * 0.28) - ); - particle.size = mix(0.20, 0.98, sizeRandom) * (0.62 + emission * 0.82 + structure * 0.36 + advectedShear * 0.18 + nodeHeat * 0.14) * (0.92 + detail * 0.035) * (0.82 + levelDetail * 0.22) * lifeFade * (0.35 + pairFade * 0.65) * (0.82 + band.weight * 0.18); - particles[index] = particle; -} -`; - -const gpuParticleRenderShader = /* wgsl */ ` -struct Particle { - position: vec2f, - velocity: vec2f, - color: vec4f, - seed: f32, - size: f32, - life: f32, - pad: f32, -}; - -struct SimUniforms { - time: f32, - dt: f32, - width: f32, - height: f32, - particleCount: f32, - flowGain: f32, - alpha: f32, - detail: f32, - envelopeCount: f32, - nodeGain: f32, - worldX: f32, - worldY: f32, - worldWidth: f32, - worldHeight: f32, - viewX: f32, - viewY: f32, - viewScale: f32, - pad1: f32, -}; - -@group(0) @binding(0) var renderParticles: array; -@group(0) @binding(1) var renderUniforms: SimUniforms; - -struct VertexOut { - @builtin(position) position: vec4f, - @location(0) color: vec4f, - @location(1) local: vec2f, -}; - -@vertex -fn vertexMain(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOut { - let corners = array( - vec2f(-1.0, -1.0), - vec2f(1.0, -1.0), - vec2f(-1.0, 1.0), - vec2f(-1.0, 1.0), - vec2f(1.0, -1.0), - vec2f(1.0, 1.0) - ); - let particle = renderParticles[instanceIndex]; - let local = corners[vertexIndex]; - let direction = normalize(particle.velocity + vec2f(0.001, 0.0)); - let tangent = vec2f(-direction.y, direction.x); - let stretch = clamp(length(particle.velocity) / 16.0, 0.0, 1.4); - let ellipse = direction * local.x * particle.size * (1.0 + stretch * 0.62) + tangent * local.y * particle.size * (0.64 + renderUniforms.detail * 0.025); - let world = vec2f(renderUniforms.worldX, renderUniforms.worldY) + particle.position * vec2f(renderUniforms.worldWidth, renderUniforms.worldHeight); - let viewScale = max(renderUniforms.viewScale, 0.05); - let basePosition = vec2f(renderUniforms.viewX, renderUniforms.viewY) + world * viewScale; - let pixelPosition = basePosition + ellipse * sqrt(viewScale); - var out: VertexOut; - out.position = vec4f((pixelPosition.x / renderUniforms.width) * 2.0 - 1.0, 1.0 - (pixelPosition.y / renderUniforms.height) * 2.0, 0.0, 1.0); - out.color = particle.color; - out.local = local; - return out; -} - -@fragment -fn fragmentMain(input: VertexOut) -> @location(0) vec4f { - let radius2 = dot(input.local, input.local); - let falloff = 5.1 + renderUniforms.detail * 0.52; - let edge = exp(-falloff); - let gaussian = exp(-falloff * radius2); - let compact = pow(clamp((gaussian - edge) / max(1.0 - edge, 0.000001), 0.0, 1.0), 0.72); - let core = exp(-radius2 * 18.0) * 0.42; - return vec4f(input.color.rgb * (compact * 1.52 + core), input.color.a * compact); -} -`; - -type HuginnFieldCanvasProps = { - imageUrl: string; - fieldUrl: string; -}; - -export function HuginnFieldCanvas({ imageUrl, fieldUrl }: HuginnFieldCanvasProps) { - const canvasRef = useRef(null); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) { - return; - } - - let cancelled = false; - const render = async () => { - try { - const [image, fieldImage] = await Promise.all([ - loadImage(imageUrl), - loadImage(fieldUrl), - ]); - if (cancelled) { - return; - } - - const sample = sampleImage(image, fieldImage); - const gpuCleanup = await startGpuParticleField(canvas, sample); - if (gpuCleanup) { - console.info("Huginn WebGPU particle field active."); - return gpuCleanup; - } - - console.info("Huginn WebGPU unavailable; using canvas field fallback."); - const pyramid = buildCurvaturePyramid(sample); - const brushes = buildBrushes(sample, pyramid); - const observer = new ResizeObserver(() => drawFieldFallback(canvas, brushes, sample)); - observer.observe(canvas); - drawFieldFallback(canvas, brushes, sample); - - return () => observer.disconnect(); - } catch { - return; - } - }; - - let cleanup: (() => void) | undefined; - void render().then((nextCleanup) => { - cleanup = nextCleanup; - }); - - return () => { - cancelled = true; - cleanup?.(); - }; - }, [fieldUrl, imageUrl]); - - return