/** * Durable I/O for the workflow catalog: restoring catalog state at boot and * writing individual installed-revision entries. * * `restoreWorkflowCatalog` fails closed on any corrupt or unparseable * durable record — a corrupted catalog entry or active pointer is data * corruption in Weft's own durable store, not hostile external input, and * every other fail-closed precedent in this codebase (a corrupt lease epoch, * a corrupt ownership-mode marker) treats that the same way. Silently * dropping it could later resurrect a stale or wrong active revision. * * @module core/catalog/storage-io */ import { type Storage } from '../../storage/interface.ts'; import type { WorkflowRevisionManifest } from '../contract/types.ts'; import type { WorkflowCatalogActivePointer, WorkflowCatalogEntry } from './types.ts'; /** In-memory catalog state hydrated from durable storage. */ export type RestoredWorkflowCatalogState = { entries: Map>; active: Map; }; /** * Restore the durable workflow catalog: every installed `(name, revision)` * entry and every name's active pointer. Every entry record is validated via * {@link parseWorkflowRevisionManifest}; every active-pointer record is * validated via {@link decodeActivePointer}. A corrupt or unparseable record * throws rather than being silently skipped. Restoring an empty store * returns empty maps. */ export declare function restoreWorkflowCatalog(storage: Storage): Promise; /** * Decode and validate one durable catalog-entry record: parse as JSON, * validate as a manifest via {@link parseWorkflowRevisionManifest}, and check * that the decoded `(name, revision)` agrees with the caller-supplied * expectation (normally read from the storage key itself). Fails closed on * any disagreement — shared by {@link restoreWorkflowCatalog}'s per-entry * restore, {@link readCatalogEntry}'s single-key read, and the by-name scan * `WorkflowCatalog.listInstalledRevisions` uses, so the three consumers of * one durable record shape can never validate it three different ways. */ /** * Decode and validate a raw catalog-entry-record byte string against an * expected `(name, revision)` — exported for {@link import('./removal.ts').restoreCatalogEntryFromTombstone}'s * caller (`WorkflowCatalog.restoreFromTombstone`) and the boot-time orphan * sweep (`orphaned-tombstones.ts`) to re-populate the in-memory cache from * a tombstone's bytes, which are byte-identical to the entry bytes this * function already validates for {@link restoreWorkflowCatalog} and * {@link readCatalogEntry}. */ export declare function decodeCatalogEntryRecord(key: string, bytes: Uint8Array, expectedName: string, expectedRevision: string): Promise<{ manifest: WorkflowRevisionManifest; installedAt: number; }>; /** * Read one durable installed-revision record for `(name, revision)`, or * `null` when absent. Fails closed on corruption, exactly matching * {@link restoreWorkflowCatalog}'s per-entry validation — used by * `WorkflowCatalog.install()` to read through the local in-memory cache to * durable storage, which may already hold this `(name, revision)` key * courtesy of a different `WorkflowCatalog` instance/process. */ export declare function readCatalogEntry(storage: Storage, name: string, revision: string): Promise<{ manifest: WorkflowRevisionManifest; installedAt: number; } | null>; /** * Durably scan every installed revision of `name` — the * `catalog-entry::` prefix `WORKFLOW_CATALOG_KEYS.catalogEntryPrefix` * builds. Each record is validated exactly like * {@link restoreWorkflowCatalog}'s per-entry restore (via * {@link decodeCatalogEntryRecord}, shared rather than duplicated); a * corrupt or unparseable entry fails closed rather than being silently * skipped. Returns entries in no particular order — callers that need a * deterministic order (`WorkflowCatalog.listInstalledRevisions`) sort the * result themselves. */ export declare function scanCatalogEntriesForName(storage: Storage, name: string): Promise>; /** Read one name's durable active pointer, or `null` when absent. */ export declare function readActivePointer(storage: Storage, name: string): Promise; /** * A durable install fence (WFT-21, Codex review round 14, P1 item Q7jH): the * `catalog-removal-generation::` bytes a caller observed * BEFORE starting work whose eventual `writeCatalogEntry` call must not * resurrect a revision removed WHILE that work was in flight. Only a * dynamic-source loader (`runSharedSourceLoad`, `core/engine/source-resolution.ts`) * supplies one — it reads the counter immediately before invoking the host * loader, then threads the observed bytes through to `WorkflowCatalog.install()` * once the loader (and validation) finish. `null` means "observed as never * removed." A caller with no prior observation to be stale against (`engine.register()`'s * drain path, a deliberate direct `engine.workflows.install()` reinstall) * omits the fence entirely — see `KEYS.catalogRemovalGeneration`'s own doc * for the full rationale. */ export type CatalogInstallFence = { removalGeneration: Uint8Array | null; }; /** * Read the raw bytes of `(name, revision)`'s durable removal-generation * counter — `null` when the revision has never been removed. Callers that * need to compare (not decode) this value, e.g. `WorkflowCatalog.install()`'s * CAS-loss disambiguation, use {@link catalogRemovalGenerationMatches} * instead of decoding it themselves. */ export declare function readCatalogRemovalGeneration(storage: Storage, name: string, revision: string): Promise; /** * Whether `(name, revision)`'s CURRENT durable removal-generation counter * still reads as `expected` — used by `WorkflowCatalog.install()` after a * fenced `writeCatalogEntry` loses its CAS, to tell a genuine content * conflict (the counter is unchanged; some other writer raced the entry * itself) apart from a stale-load resurrection attempt (the counter * advanced — a removal landed after `expected` was observed). */ export declare function catalogRemovalGenerationMatches(storage: Storage, name: string, revision: string, expected: Uint8Array | null): Promise; /** * Durably write one installed-revision entry, CAS-guarded on BOTH the entry * key being absent (`expectedValue: null`) AND the entry's tombstone key * being absent, and — when `fence` is supplied — additionally on the * `catalog-removal-generation::` counter still reading * `fence.removalGeneration` (WFT-21, item Q7jH; see * {@link CatalogInstallFence}'s own doc). Returns `true` when this write won * the race, `false` when ANY precondition failed — `WorkflowCatalog.install()` * distinguishes the causes itself (re-reading the entry, then the * tombstone, then — when fenced — the removal-generation counter, on * `false`) since a flat boolean cannot: a durable entry already installed * (idempotent-or-conflict, the original condition), a tombstone currently * present for this exact `(name, revision)` (WFT-21, Codex review items * 1-3 — refuse to resurrect a revision `removeCatalogEntry()` is deleting * or has deleted, until its tombstone is resolved), or a removal that * completed (including finalizing its own tombstone away) since a fenced * caller's own observation. * * CAS-protected rather than a plain `put`: "content-addressed by * `(name, revision)`, so racing writers always agree" only holds when * `revision` is content-derived. `buildWorkflowRevisionManifest`'s public * `options.revision` escape hatch lets a caller supply a non-content-derived * revision (e.g. a deploy tag), so two different `WorkflowCatalog` * instances/processes — each with their own, independently-seeded * in-memory cache — could otherwise race a differing-content write to the * same key past each other's in-memory-only conflict check with a plain * `put` (last write wins, silently). */ export declare function writeCatalogEntry(storage: Storage, manifest: WorkflowRevisionManifest, installedAt: number, fence?: CatalogInstallFence): Promise; /** * `WorkflowCatalog.install()`'s cache-hit revalidation (WFT-21, Codex * review round 14, P1 item TYR4): re-reads durable storage rather than * trusting an in-process cache hit outright. A peer's `remove()` + tombstone * resolution can durably delete this exact `(name, revision)` while it * stays cached from an earlier `install()`/`resolveEntry()` call on this * same process. Returns `true` when durable storage still agrees the entry * exists (and matches `manifest`'s content — a mismatch throws * {@link WorkflowCatalogConflictError}, the same conflict `install()`'s own * durable read-through path already throws); `false` when durably absent, * telling the caller to evict its stale cache entry and fall through to the * ordinary not-cached path. */ export declare function revalidateCachedCatalogInstall(storage: Storage, manifest: WorkflowRevisionManifest): Promise; /** * `WorkflowCatalog.install()`'s CAS-loss disambiguation for the "still * durably absent after losing the write race" case. Always throws: a * present tombstone explains the absence directly * ({@link WorkflowRevisionTombstonedError}); a fenced caller whose observed * `catalog-removal-generation::` counter has since advanced * explains it too — a removal completed, tombstone and all, after the * fence was captured (WFT-21, item Q7jH); neither case is a genuine * writer-vs-writer conflict, so both are distinguished from the fallback * {@link WorkflowCatalogConflictError}. */ export declare function throwForAbsentCatalogInstallRace(storage: Storage, manifest: WorkflowRevisionManifest, fence: CatalogInstallFence | undefined): Promise;