@lostgradient/weft 0.23.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -4
- package/dist/cli/codegen-emit-dedup.d.ts +81 -0
- package/dist/cli/codegen-emit-dedup.js +45 -0
- package/dist/cli/codegen-emit-registry.d.ts +90 -0
- package/dist/cli/codegen-emit-registry.js +50 -0
- package/dist/cli/codegen-emit.d.ts +26 -39
- package/dist/cli/codegen-emit.js +4 -27
- package/dist/cli/codegen-validate.d.ts +49 -0
- package/dist/cli/codegen-validate.js +186 -0
- package/dist/cli/codegen.js +6 -87
- package/dist/cli/generated/operation-catalog.snapshot.json +287 -3
- package/dist/cli/generated/operation-client.generated.d.ts +324 -10
- package/dist/cli/generated/operation-client.generated.js +12 -0
- package/dist/cli/output.js +1 -1
- package/dist/cli/storage-factory.js +2 -0
- package/dist/cli/workflow-commands.js +2 -0
- package/dist/cli-main.js +1 -1
- package/dist/core/application-payload-digest.d.ts +42 -0
- package/dist/core/application-payload-digest.js +76 -0
- package/dist/core/application-primitive-abort.d.ts +50 -0
- package/dist/core/application-primitive-abort.js +40 -0
- package/dist/core/application-primitive-attempt-registry.d.ts +74 -0
- package/dist/core/application-primitive-attempt-registry.js +73 -0
- package/dist/core/application-primitive-codec.d.ts +36 -0
- package/dist/core/application-primitive-codec.js +56 -0
- package/dist/core/application-primitive-commit.d.ts +67 -0
- package/dist/core/application-primitive-commit.js +66 -0
- package/dist/core/application-primitive-guards.d.ts +62 -0
- package/dist/core/application-primitive-guards.js +89 -0
- package/dist/core/application-primitive-payload.d.ts +29 -0
- package/dist/core/application-primitive-payload.js +65 -0
- package/dist/core/application-primitive-timing.d.ts +18 -0
- package/dist/core/application-primitive-timing.js +22 -0
- package/dist/core/atomic-state.js +2 -0
- package/dist/core/catalog/codec.d.ts +40 -0
- package/dist/core/catalog/codec.js +45 -0
- package/dist/core/catalog/errors.d.ts +104 -0
- package/dist/core/catalog/errors.js +41 -0
- package/dist/core/catalog/index.d.ts +25 -0
- package/dist/core/catalog/index.js +15 -0
- package/dist/core/catalog/reference-counts.d.ts +105 -0
- package/dist/core/catalog/reference-counts.js +28 -0
- package/dist/core/catalog/removal.d.ts +52 -0
- package/dist/core/catalog/removal.js +14 -0
- package/dist/core/catalog/storage-io.d.ts +78 -0
- package/dist/core/catalog/storage-io.js +106 -0
- package/dist/core/catalog/types.d.ts +108 -0
- package/dist/core/catalog/types.js +0 -0
- package/dist/core/catalog/workflow-catalog.d.ts +165 -0
- package/dist/core/catalog/workflow-catalog.js +198 -0
- package/dist/core/compare-codepoint.d.ts +13 -0
- package/dist/core/compare-codepoint.js +7 -0
- package/dist/core/compression.js +2 -0
- package/dist/core/context/attributes.d.ts +1 -1
- package/dist/core/context/durable-operations.d.ts +2 -2
- package/dist/core/context/index.js +5 -3
- package/dist/core/context/state-namespace.d.ts +1 -1
- package/dist/core/contract/build.d.ts +64 -0
- package/dist/core/contract/build.js +99 -0
- package/dist/core/contract/compatibility.d.ts +187 -0
- package/dist/core/contract/compatibility.js +37 -0
- package/dist/core/contract/failure.d.ts +64 -0
- package/dist/core/contract/failure.js +4 -0
- package/dist/core/contract/hash.d.ts +92 -0
- package/dist/core/contract/hash.js +34 -0
- package/dist/core/contract/index.d.ts +21 -0
- package/dist/core/contract/index.js +22 -0
- package/dist/core/contract/limits.d.ts +74 -0
- package/dist/core/contract/limits.js +1 -0
- package/dist/core/contract/manifest-parse-schema.d.ts +37 -0
- package/dist/core/contract/manifest-parse-schema.js +106 -0
- package/dist/core/contract/manifest-parse.d.ts +76 -0
- package/dist/core/contract/manifest-parse.js +189 -0
- package/dist/core/contract/manifest.d.ts +56 -0
- package/dist/core/contract/manifest.js +31 -0
- package/dist/core/contract/normalize.d.ts +97 -0
- package/dist/core/contract/normalize.js +127 -0
- package/dist/core/contract/revision.d.ts +29 -0
- package/dist/core/contract/revision.js +5 -0
- package/dist/core/contract/types.d.ts +274 -0
- package/dist/core/contract/types.js +1 -0
- package/dist/core/engine/catalog-activation.d.ts +40 -0
- package/dist/core/engine/catalog-activation.js +17 -0
- package/dist/core/engine/catalog-events.d.ts +36 -0
- package/dist/core/engine/catalog-events.js +15 -0
- package/dist/core/engine/catalog-readiness.d.ts +49 -0
- package/dist/core/engine/catalog-readiness.js +59 -0
- package/dist/core/engine/catalog-removal.d.ts +132 -0
- package/dist/core/engine/catalog-removal.js +70 -0
- package/dist/core/engine/construction.d.ts +1 -1
- package/dist/core/engine/construction.js +4 -2
- package/dist/core/engine/disposal.js +9 -0
- package/dist/core/engine/engine-workflows-namespace.d.ts +130 -0
- package/dist/core/engine/engine-workflows-namespace.js +49 -0
- package/dist/core/engine/finalizer-metadata.d.ts +22 -0
- package/dist/core/engine/finalizer-metadata.js +12 -0
- package/dist/core/engine/handle-result.js +2 -0
- package/dist/core/engine/index.d.ts +83 -0
- package/dist/core/engine/index.js +61 -0
- package/dist/core/engine/internals.d.ts +44 -1
- package/dist/core/engine/internals.js +6 -0
- package/dist/core/engine/lifecycle/start.js +3 -0
- package/dist/core/engine/registration.d.ts +76 -0
- package/dist/core/engine/registration.js +12 -6
- package/dist/core/engine/source-registration.d.ts +42 -0
- package/dist/core/engine/source-registration.js +36 -0
- package/dist/core/engine/source-resolution.d.ts +85 -0
- package/dist/core/engine/source-resolution.js +119 -0
- package/dist/core/engine/validation.js +2 -2
- package/dist/core/events/catalog-events.d.ts +117 -0
- package/dist/core/events/catalog-events.js +62 -0
- package/dist/core/events/event-map.d.ts +6 -0
- package/dist/core/events/index.d.ts +1 -0
- package/dist/core/events/index.js +1 -0
- package/dist/core/mailbox-admission.d.ts +26 -0
- package/dist/core/mailbox-admission.js +130 -0
- package/dist/core/mailbox-claims.d.ts +251 -0
- package/dist/core/mailbox-claims.js +0 -0
- package/dist/core/mailbox-codec.d.ts +32 -0
- package/dist/core/mailbox-codec.js +221 -0
- package/dist/core/mailbox-contract.d.ts +290 -0
- package/dist/core/mailbox-contract.js +0 -0
- package/dist/core/mailbox-delivery.d.ts +37 -0
- package/dist/core/mailbox-delivery.js +193 -0
- package/dist/core/mailbox-guards.d.ts +78 -0
- package/dist/core/mailbox-guards.js +58 -0
- package/dist/core/mailbox-index-codec.d.ts +41 -0
- package/dist/core/mailbox-index-codec.js +71 -0
- package/dist/core/mailbox-internals.d.ts +156 -0
- package/dist/core/mailbox-internals.js +154 -0
- package/dist/core/mailbox-maintenance.d.ts +29 -0
- package/dist/core/mailbox-maintenance.js +196 -0
- package/dist/core/mailbox-settlement.d.ts +69 -0
- package/dist/core/mailbox-settlement.js +206 -0
- package/dist/core/mailbox-storage.d.ts +117 -0
- package/dist/core/mailbox-storage.js +115 -0
- package/dist/core/mailbox-transition-helpers.d.ts +70 -0
- package/dist/core/mailbox-transition-helpers.js +43 -0
- package/dist/core/mailbox-transitions-recovery.d.ts +28 -0
- package/dist/core/mailbox-transitions-recovery.js +64 -0
- package/dist/core/mailbox-transitions.d.ts +138 -0
- package/dist/core/mailbox-transitions.js +180 -0
- package/dist/core/mailbox-types.d.ts +406 -0
- package/dist/core/mailbox-types.js +16 -0
- package/dist/core/mailbox-validation.d.ts +65 -0
- package/dist/core/mailbox-validation.js +73 -0
- package/dist/core/mailbox-waits.d.ts +50 -0
- package/dist/core/mailbox-waits.js +97 -0
- package/dist/core/mailbox.d.ts +163 -0
- package/dist/core/mailbox.js +211 -0
- package/dist/core/outbox-claims.d.ts +307 -0
- package/dist/core/outbox-claims.js +0 -0
- package/dist/core/outbox-codec.d.ts +24 -0
- package/dist/core/outbox-codec.js +257 -0
- package/dist/core/outbox-contract.d.ts +346 -0
- package/dist/core/outbox-contract.js +0 -0
- package/dist/core/outbox-delivery.d.ts +34 -0
- package/dist/core/outbox-delivery.js +149 -0
- package/dist/core/outbox-drain.d.ts +29 -0
- package/dist/core/outbox-drain.js +158 -0
- package/dist/core/outbox-enqueue.d.ts +24 -0
- package/dist/core/outbox-enqueue.js +115 -0
- package/dist/core/outbox-guards.d.ts +76 -0
- package/dist/core/outbox-guards.js +60 -0
- package/dist/core/outbox-index-codec.d.ts +33 -0
- package/dist/core/outbox-index-codec.js +71 -0
- package/dist/core/outbox-internals.d.ts +99 -0
- package/dist/core/outbox-internals.js +159 -0
- package/dist/core/outbox-listing.d.ts +12 -0
- package/dist/core/outbox-listing.js +38 -0
- package/dist/core/outbox-maintenance.d.ts +28 -0
- package/dist/core/outbox-maintenance.js +191 -0
- package/dist/core/outbox-operations.d.ts +18 -0
- package/dist/core/outbox-operations.js +46 -0
- package/dist/core/outbox-runner.d.ts +30 -0
- package/dist/core/outbox-runner.js +217 -0
- package/dist/core/outbox-settlement.d.ts +57 -0
- package/dist/core/outbox-settlement.js +183 -0
- package/dist/core/outbox-storage.d.ts +107 -0
- package/dist/core/outbox-storage.js +127 -0
- package/dist/core/outbox-transition-helpers.d.ts +83 -0
- package/dist/core/outbox-transition-helpers.js +63 -0
- package/dist/core/outbox-transitions-recovery.d.ts +30 -0
- package/dist/core/outbox-transitions-recovery.js +42 -0
- package/dist/core/outbox-transitions.d.ts +145 -0
- package/dist/core/outbox-transitions.js +236 -0
- package/dist/core/outbox-types.d.ts +400 -0
- package/dist/core/outbox-types.js +20 -0
- package/dist/core/outbox-validation.d.ts +101 -0
- package/dist/core/outbox-validation.js +127 -0
- package/dist/core/outbox-waits.d.ts +39 -0
- package/dist/core/outbox-waits.js +88 -0
- package/dist/core/outbox.d.ts +156 -0
- package/dist/core/outbox.js +234 -0
- package/dist/core/registry-limits.d.ts +49 -0
- package/dist/core/registry-limits.js +10 -0
- package/dist/core/registry-schema-conversion.d.ts +20 -0
- package/dist/core/registry-schema-conversion.js +22 -0
- package/dist/core/registry-snapshot.d.ts +91 -42
- package/dist/core/registry-snapshot.js +39 -77
- package/dist/core/registry-workflow-contract-draft.d.ts +32 -0
- package/dist/core/registry-workflow-contract-draft.js +28 -0
- package/dist/core/registry-workflow-manifest.d.ts +122 -0
- package/dist/core/registry-workflow-manifest.js +92 -0
- package/dist/core/source/errors.d.ts +55 -0
- package/dist/core/source/errors.js +13 -0
- package/dist/core/source/index.d.ts +19 -0
- package/dist/core/source/index.js +4 -0
- package/dist/core/source/resolvers.d.ts +45 -0
- package/dist/core/source/resolvers.js +9 -0
- package/dist/core/source/types.d.ts +211 -0
- package/dist/core/source/types.js +0 -0
- package/dist/core/source/validate.d.ts +91 -0
- package/dist/core/source/validate.js +62 -0
- package/dist/core/source/workflow-source.d.ts +43 -0
- package/dist/core/source/workflow-source.js +16 -0
- package/dist/core/types/workflow-registries.d.ts +7 -0
- package/dist/core/types/workflow-registry.d.ts +12 -0
- package/dist/core/versioning.js +2 -0
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +9 -1
- package/dist/http.js +2 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +50 -0
- package/dist/indexeddb.js +1 -1
- package/dist/json-schema.js +3 -3
- package/dist/mcp/resources.js +2 -0
- package/dist/runtime/portable.d.ts +9 -1
- package/dist/server/fault-to-json-rpc.js +3 -1
- package/dist/server/handler/auth-context-principal.js +4 -0
- package/dist/server/json-rpc-http.js +2 -0
- package/dist/server/json-rpc-websocket.js +6 -4
- package/dist/server/operation-catalog/index.d.ts +1 -1
- package/dist/server/operation-catalog/pipeline-helpers.js +2 -0
- package/dist/server/operation-catalog/types.d.ts +17 -0
- package/dist/server/operation-fault.d.ts +3 -0
- package/dist/server/operation-fault.js +3 -1
- package/dist/server/operations/activate-workflow-revision.d.ts +30 -0
- package/dist/server/operations/activate-workflow-revision.js +69 -0
- package/dist/server/operations/fleet-events-subscription.js +1 -1
- package/dist/server/operations/fork-workflow.js +1 -1
- package/dist/server/operations/get-active-workflow-revision.d.ts +32 -0
- package/dist/server/operations/get-active-workflow-revision.js +46 -0
- package/dist/server/operations/get-catalog-diagnostics.d.ts +85 -0
- package/dist/server/operations/get-catalog-diagnostics.js +57 -0
- package/dist/server/operations/get-registry.js +17 -4
- package/dist/server/operations/get-workflow-revision.d.ts +25 -0
- package/dist/server/operations/get-workflow-revision.js +50 -0
- package/dist/server/operations/install-workflow-revision.d.ts +27 -0
- package/dist/server/operations/install-workflow-revision.js +46 -0
- package/dist/server/operations/list-workflow-revisions.d.ts +20 -0
- package/dist/server/operations/list-workflow-revisions.js +40 -0
- package/dist/server/operations/start-or-signal-workflow.js +1 -1
- package/dist/server/operations/start-workflow.js +1 -1
- package/dist/server/operations/static-registrations.js +36 -0
- package/dist/server/operations/storage.js +2 -2
- package/dist/server/operations/submit-review-decision.js +1 -1
- package/dist/server/operations/workflow-catalog-operation-helpers.d.ts +59 -0
- package/dist/server/operations/workflow-catalog-operation-helpers.js +135 -0
- package/dist/server/runtime/websocket-upgrade.js +2 -0
- package/dist/storage/bun-sql.js +49 -6
- package/dist/storage/catalog-keys.d.ts +45 -0
- package/dist/storage/catalog-keys.js +6 -0
- package/dist/storage/compressed-storage.js +1 -1
- package/dist/storage/derived-operations.js +1 -1
- package/dist/storage/index.d.ts +26 -1
- package/dist/storage/interface.d.ts +26 -1
- package/dist/storage/interface.js +1 -1
- package/dist/storage/key-encoding.d.ts +15 -0
- package/dist/storage/key-encoding.js +3 -0
- package/dist/storage/key-prefixes.d.ts +1 -1
- package/dist/storage/key-prefixes.js +15 -0
- package/dist/storage/lmdb.d.ts +26 -1
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/mailbox-keys.d.ts +80 -0
- package/dist/storage/mailbox-keys.js +14 -0
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon.js +2 -2
- package/dist/storage/node-sqlite-loader.d.ts +1 -1
- package/dist/storage/node-sqlite.js +49 -6
- package/dist/storage/outbox-keys.d.ts +77 -0
- package/dist/storage/outbox-keys.js +14 -0
- package/dist/storage/postgres.js +2 -2
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/storage-configuration.d.ts +6 -0
- package/dist/storage/testing.js +1 -1
- package/dist/storage/turso.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/web-extension.js +1 -1
- package/dist/worker/manifest/registry-contract-builder.d.ts +21 -3
- package/dist/worker/manifest/registry-contract-builder.js +40 -31
- package/package.json +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable I/O for the workflow catalog: restoring catalog state at boot and
|
|
3
|
+
* writing individual installed-revision entries.
|
|
4
|
+
*
|
|
5
|
+
* `restoreWorkflowCatalog` fails closed on any corrupt or unparseable
|
|
6
|
+
* durable record — a corrupted catalog entry or active pointer is data
|
|
7
|
+
* corruption in Weft's own durable store, not hostile external input, and
|
|
8
|
+
* every other fail-closed precedent in this codebase (a corrupt lease epoch,
|
|
9
|
+
* a corrupt ownership-mode marker) treats that the same way. Silently
|
|
10
|
+
* dropping it could later resurrect a stale or wrong active revision.
|
|
11
|
+
*
|
|
12
|
+
* @module core/catalog/storage-io
|
|
13
|
+
*/
|
|
14
|
+
import { type Storage } from '../../storage/interface.ts';
|
|
15
|
+
import type { WorkflowRevisionManifest } from '../contract/types.ts';
|
|
16
|
+
import type { WorkflowCatalogActivePointer, WorkflowCatalogEntry } from './types.ts';
|
|
17
|
+
/** In-memory catalog state hydrated from durable storage. */
|
|
18
|
+
export type RestoredWorkflowCatalogState = {
|
|
19
|
+
entries: Map<string, Map<string, WorkflowCatalogEntry>>;
|
|
20
|
+
active: Map<string, WorkflowCatalogActivePointer>;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Restore the durable workflow catalog: every installed `(name, revision)`
|
|
24
|
+
* entry and every name's active pointer. Every entry record is validated via
|
|
25
|
+
* {@link parseWorkflowRevisionManifest}; every active-pointer record is
|
|
26
|
+
* validated via {@link decodeActivePointer}. A corrupt or unparseable record
|
|
27
|
+
* throws rather than being silently skipped. Restoring an empty store
|
|
28
|
+
* returns empty maps.
|
|
29
|
+
*/
|
|
30
|
+
export declare function restoreWorkflowCatalog(storage: Storage): Promise<RestoredWorkflowCatalogState>;
|
|
31
|
+
/**
|
|
32
|
+
* Read one durable installed-revision record for `(name, revision)`, or
|
|
33
|
+
* `null` when absent. Fails closed on corruption, exactly matching
|
|
34
|
+
* {@link restoreWorkflowCatalog}'s per-entry validation — used by
|
|
35
|
+
* `WorkflowCatalog.install()` to read through the local in-memory cache to
|
|
36
|
+
* durable storage, which may already hold this `(name, revision)` key
|
|
37
|
+
* courtesy of a different `WorkflowCatalog` instance/process.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readCatalogEntry(storage: Storage, name: string, revision: string): Promise<{
|
|
40
|
+
manifest: WorkflowRevisionManifest;
|
|
41
|
+
installedAt: number;
|
|
42
|
+
} | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Durably scan every installed revision of `name` — the
|
|
45
|
+
* `catalog-entry:<name>:` prefix `WORKFLOW_CATALOG_KEYS.catalogEntryPrefix`
|
|
46
|
+
* builds. Each record is validated exactly like
|
|
47
|
+
* {@link restoreWorkflowCatalog}'s per-entry restore (via
|
|
48
|
+
* {@link decodeCatalogEntryRecord}, shared rather than duplicated); a
|
|
49
|
+
* corrupt or unparseable entry fails closed rather than being silently
|
|
50
|
+
* skipped. Returns entries in no particular order — callers that need a
|
|
51
|
+
* deterministic order (`WorkflowCatalog.listInstalledRevisions`) sort the
|
|
52
|
+
* result themselves.
|
|
53
|
+
*/
|
|
54
|
+
export declare function scanCatalogEntriesForName(storage: Storage, name: string): Promise<Array<{
|
|
55
|
+
manifest: WorkflowRevisionManifest;
|
|
56
|
+
installedAt: number;
|
|
57
|
+
}>>;
|
|
58
|
+
/** Read one name's durable active pointer, or `null` when absent. */
|
|
59
|
+
export declare function readActivePointer(storage: Storage, name: string): Promise<WorkflowCatalogActivePointer | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Durably write one installed-revision entry, CAS-guarded on the key being
|
|
62
|
+
* absent (`expectedValue: null`). Returns `true` when this write won the
|
|
63
|
+
* race, `false` when another writer had already durably installed this
|
|
64
|
+
* exact `(name, revision)` key first — `WorkflowCatalog.install()` re-reads
|
|
65
|
+
* via {@link readCatalogEntry} on `false` to decide whether that concurrent
|
|
66
|
+
* write was byte-identical (idempotent) or a genuine conflict.
|
|
67
|
+
*
|
|
68
|
+
* CAS-protected rather than a plain `put`: "content-addressed by
|
|
69
|
+
* `(name, revision)`, so racing writers always agree" only holds when
|
|
70
|
+
* `revision` is content-derived. `buildWorkflowRevisionManifest`'s public
|
|
71
|
+
* `options.revision` escape hatch lets a caller supply a non-content-derived
|
|
72
|
+
* revision (e.g. a deploy tag), so two different `WorkflowCatalog`
|
|
73
|
+
* instances/processes — each with their own, independently-seeded
|
|
74
|
+
* in-memory cache — could otherwise race a differing-content write to the
|
|
75
|
+
* same key past each other's in-memory-only conflict check with a plain
|
|
76
|
+
* `put` (last write wins, silently).
|
|
77
|
+
*/
|
|
78
|
+
export declare function writeCatalogEntry(storage: Storage, manifest: WorkflowRevisionManifest, installedAt: number): Promise<boolean>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { KEYS, storageConditionalBatch } from "../../storage/interface.js";
|
|
2
|
+
import { tryDecodeStorageKeyComponent } from "../../storage/key-encoding.js";
|
|
3
|
+
import { parseWorkflowRevisionManifest } from "../contract/manifest-parse.js";
|
|
4
|
+
import { decodeActivePointer } from "./codec.js";
|
|
5
|
+
function splitCatalogEntryKey(key) {
|
|
6
|
+
const parts = key.split(":");
|
|
7
|
+
if (parts.length !== 3)
|
|
8
|
+
return null;
|
|
9
|
+
const name = tryDecodeStorageKeyComponent(parts[1] ?? ""), revision = tryDecodeStorageKeyComponent(parts[2] ?? "");
|
|
10
|
+
if (name === null || revision === null)
|
|
11
|
+
return null;
|
|
12
|
+
return { name, revision };
|
|
13
|
+
}
|
|
14
|
+
function splitCatalogActiveKey(key) {
|
|
15
|
+
const parts = key.split(":");
|
|
16
|
+
if (parts.length !== 2)
|
|
17
|
+
return null;
|
|
18
|
+
return tryDecodeStorageKeyComponent(parts[1] ?? "");
|
|
19
|
+
}
|
|
20
|
+
function parseCatalogEntryRecord(bytes) {
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
28
|
+
return null;
|
|
29
|
+
const record = parsed;
|
|
30
|
+
if (typeof record.installedAt !== "number")
|
|
31
|
+
return null;
|
|
32
|
+
return { manifest: record.manifest, installedAt: record.installedAt };
|
|
33
|
+
}
|
|
34
|
+
function failClosed(kind, key, reason) {
|
|
35
|
+
throw Error(`The store's ${kind} ("${key}") exists but ${reason}. Treating it as absent would risk resurrecting a stale or wrong active workflow revision, so the workflow catalog fails closed instead. Resolve by operator repair: inspect the stored bytes and, only if certain no other engine relies on them, delete the key so it can be re-established.`);
|
|
36
|
+
}
|
|
37
|
+
export async function restoreWorkflowCatalog(storage) {
|
|
38
|
+
const entries = new Map, active = new Map;
|
|
39
|
+
for await (const [key, bytes] of storage.scan("catalog-entry:"))
|
|
40
|
+
await restoreOneCatalogEntry(entries, key, bytes);
|
|
41
|
+
for await (const [key, bytes] of storage.scan("catalog-active:"))
|
|
42
|
+
restoreOneActivePointer(active, key, bytes);
|
|
43
|
+
return { entries, active };
|
|
44
|
+
}
|
|
45
|
+
async function decodeCatalogEntryRecord(key, bytes, expectedName, expectedRevision) {
|
|
46
|
+
const parsedRecord = parseCatalogEntryRecord(bytes);
|
|
47
|
+
if (parsedRecord === null)
|
|
48
|
+
failClosed("workflow catalog entry", key, "could not be parsed as JSON");
|
|
49
|
+
const parsed = await parseWorkflowRevisionManifest(parsedRecord.manifest);
|
|
50
|
+
if (!parsed.ok)
|
|
51
|
+
failClosed("workflow catalog entry", key, "does not decode as a valid installed-revision record");
|
|
52
|
+
const manifest = parsed.manifest;
|
|
53
|
+
if (manifest.name !== expectedName || manifest.revision !== expectedRevision)
|
|
54
|
+
failClosed("workflow catalog entry", key, "contains a manifest whose (name, revision) disagrees with its storage key");
|
|
55
|
+
return { manifest, installedAt: parsedRecord.installedAt };
|
|
56
|
+
}
|
|
57
|
+
async function restoreOneCatalogEntry(entries, key, bytes) {
|
|
58
|
+
const split = splitCatalogEntryKey(key);
|
|
59
|
+
if (split === null)
|
|
60
|
+
failClosed("workflow catalog entry key", key, "does not match the expected catalog-entry:<name>:<revision> shape");
|
|
61
|
+
const { manifest, installedAt } = await decodeCatalogEntryRecord(key, bytes, split.name, split.revision);
|
|
62
|
+
let byName = entries.get(split.name);
|
|
63
|
+
if (byName === void 0) {
|
|
64
|
+
byName = new Map;
|
|
65
|
+
entries.set(split.name, byName);
|
|
66
|
+
}
|
|
67
|
+
byName.set(split.revision, { manifest, installedAt });
|
|
68
|
+
}
|
|
69
|
+
function restoreOneActivePointer(active, key, bytes) {
|
|
70
|
+
const name = splitCatalogActiveKey(key);
|
|
71
|
+
if (name === null)
|
|
72
|
+
failClosed("workflow catalog active pointer key", key, "does not match the expected catalog-active:<name> shape");
|
|
73
|
+
const pointer = decodeActivePointer(bytes);
|
|
74
|
+
if (pointer === null)
|
|
75
|
+
failClosed("workflow catalog active pointer", key, "does not decode as a valid { revision, generation, activatedAt } record");
|
|
76
|
+
active.set(name, pointer);
|
|
77
|
+
}
|
|
78
|
+
export async function readCatalogEntry(storage, name, revision) {
|
|
79
|
+
const key = KEYS.catalogEntry(name, revision), bytes = await storage.get(key);
|
|
80
|
+
if (bytes === null)
|
|
81
|
+
return null;
|
|
82
|
+
return decodeCatalogEntryRecord(key, bytes, name, revision);
|
|
83
|
+
}
|
|
84
|
+
export async function scanCatalogEntriesForName(storage, name) {
|
|
85
|
+
const results = [];
|
|
86
|
+
for await (const [key, bytes] of storage.scan(KEYS.catalogEntryPrefix(name))) {
|
|
87
|
+
const split = splitCatalogEntryKey(key);
|
|
88
|
+
if (split === null)
|
|
89
|
+
failClosed("workflow catalog entry key", key, "does not match the expected catalog-entry:<name>:<revision> shape");
|
|
90
|
+
results.push(await decodeCatalogEntryRecord(key, bytes, split.name, split.revision));
|
|
91
|
+
}
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
export async function readActivePointer(storage, name) {
|
|
95
|
+
const bytes = await storage.get(KEYS.catalogActive(name));
|
|
96
|
+
if (bytes === null)
|
|
97
|
+
return null;
|
|
98
|
+
const pointer = decodeActivePointer(bytes);
|
|
99
|
+
if (pointer === null)
|
|
100
|
+
failClosed("workflow catalog active pointer", KEYS.catalogActive(name), "does not decode as a valid { revision, generation, activatedAt } record");
|
|
101
|
+
return pointer;
|
|
102
|
+
}
|
|
103
|
+
export async function writeCatalogEntry(storage, manifest, installedAt) {
|
|
104
|
+
const bytes = new TextEncoder().encode(JSON.stringify({ manifest, installedAt })), key = KEYS.catalogEntry(manifest.name, manifest.revision);
|
|
105
|
+
return storageConditionalBatch(storage, [{ key, expectedValue: null }], [{ type: "put", key, value: bytes }]);
|
|
106
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory and wire types for the durable workflow catalog (WFT-9/WFT-10).
|
|
3
|
+
*
|
|
4
|
+
* `WorkflowCatalog` keys entries with nested `Map<string, Map<string, ...>>`
|
|
5
|
+
* structures — never delimiter-joined strings — so a workflow `name` or
|
|
6
|
+
* `revision` equal to `'__proto__'`, `'toString'`, or containing a colon is
|
|
7
|
+
* always safe to store and look up.
|
|
8
|
+
*
|
|
9
|
+
* @module core/catalog/types
|
|
10
|
+
*/
|
|
11
|
+
import type { WorkflowCompatibilityVerdict } from '../contract/compatibility.ts';
|
|
12
|
+
import type { WorkflowRevisionManifest } from '../contract/types.ts';
|
|
13
|
+
import type { RegisteredWorkflowDefinition } from '../types/workflow-registry.ts';
|
|
14
|
+
/**
|
|
15
|
+
* One installed `(name, revision)` catalog entry. `definition` is populated
|
|
16
|
+
* only when this process itself installed the entry (via
|
|
17
|
+
* `engine.register()`'s drain path) — an entry hydrated from durable storage
|
|
18
|
+
* at boot, for a revision this process has not (yet) registered, carries no
|
|
19
|
+
* live `definition` because a `RegisteredWorkflowDefinition` cannot be
|
|
20
|
+
* persisted (it holds function references). Consumers that only need
|
|
21
|
+
* identity (`resolveActive`, registry-snapshot's `activeRevisions`) never
|
|
22
|
+
* touch `definition`.
|
|
23
|
+
*/
|
|
24
|
+
export type WorkflowCatalogEntry = Readonly<{
|
|
25
|
+
manifest: WorkflowRevisionManifest;
|
|
26
|
+
definition?: RegisteredWorkflowDefinition;
|
|
27
|
+
installedAt: number;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* The durable `{ revision, generation, activatedAt }` pointer for one
|
|
31
|
+
* workflow name — what `engine.workflows.getActive()` returns.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { Engine } from '@lostgradient/weft';
|
|
36
|
+
* import type { WorkflowCatalogActivePointer } from '@lostgradient/weft';
|
|
37
|
+
*
|
|
38
|
+
* const engine = new Engine();
|
|
39
|
+
* const pointer: WorkflowCatalogActivePointer | null = await engine.workflows.getActive('checkout');
|
|
40
|
+
* console.log(pointer?.generation ?? null);
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export type WorkflowCatalogActivePointer = Readonly<{
|
|
44
|
+
revision: string;
|
|
45
|
+
generation: number;
|
|
46
|
+
activatedAt: number;
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* Public-facing view of one installed `(name, revision)` catalog entry —
|
|
50
|
+
* {@link WorkflowCatalogEntry} minus `definition`, which can carry a live
|
|
51
|
+
* function reference and must never reach a public return value.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { Engine } from '@lostgradient/weft';
|
|
56
|
+
* import type { WorkflowRevisionRecord } from '@lostgradient/weft';
|
|
57
|
+
*
|
|
58
|
+
* const engine = new Engine();
|
|
59
|
+
* const record: WorkflowRevisionRecord | null = await engine.workflows.getRevision(
|
|
60
|
+
* 'checkout',
|
|
61
|
+
* 'some-revision',
|
|
62
|
+
* );
|
|
63
|
+
* console.log(record?.manifest.revision ?? null);
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export type WorkflowRevisionRecord = Readonly<{
|
|
67
|
+
manifest: WorkflowRevisionManifest;
|
|
68
|
+
installedAt: number;
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* Result of {@link import('./workflow-catalog.ts').WorkflowCatalog.activateCandidate}.
|
|
72
|
+
* A candidate either applies (bumping the durable generation) or is refused
|
|
73
|
+
* with a specific, machine-readable reason — never silently ignored.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* import { Engine } from '@lostgradient/weft';
|
|
78
|
+
* import type { WorkflowCatalogActivationResult } from '@lostgradient/weft';
|
|
79
|
+
*
|
|
80
|
+
* const engine = new Engine();
|
|
81
|
+
* const active = await engine.workflows.getActive('checkout');
|
|
82
|
+
* const result: WorkflowCatalogActivationResult = await engine.workflows.activate(
|
|
83
|
+
* 'checkout',
|
|
84
|
+
* active?.revision ?? '',
|
|
85
|
+
* active === null ? {} : { expectedGeneration: active.generation },
|
|
86
|
+
* );
|
|
87
|
+
* console.log(result.applied);
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export type WorkflowCatalogActivationResult = Readonly<{
|
|
91
|
+
applied: true;
|
|
92
|
+
pointer: WorkflowCatalogActivePointer;
|
|
93
|
+
}> | Readonly<{
|
|
94
|
+
applied: false;
|
|
95
|
+
reason: 'incompatible';
|
|
96
|
+
verdict: WorkflowCompatibilityVerdict;
|
|
97
|
+
}> | Readonly<{
|
|
98
|
+
applied: false;
|
|
99
|
+
reason: 'stale-generation';
|
|
100
|
+
currentGeneration: number;
|
|
101
|
+
}> | Readonly<{
|
|
102
|
+
applied: false;
|
|
103
|
+
reason: 'conflict';
|
|
104
|
+
}> | Readonly<{
|
|
105
|
+
applied: false;
|
|
106
|
+
reason: 'expected-generation-required';
|
|
107
|
+
currentGeneration: number;
|
|
108
|
+
}>;
|
|
File without changes
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `WorkflowCatalog` — the immutable-entry, durable workflow catalog
|
|
3
|
+
* (WFT-9/WFT-10).
|
|
4
|
+
*
|
|
5
|
+
* In-memory state is `Map<string, Map<string, WorkflowCatalogEntry>>` for
|
|
6
|
+
* installed entries and `Map<string, WorkflowCatalogActivePointer>` for each
|
|
7
|
+
* name's active pointer — nested maps, never delimiter-joined keys, so a
|
|
8
|
+
* workflow `name` or `revision` equal to `'__proto__'`/`'toString'` or
|
|
9
|
+
* containing a colon is always handled correctly.
|
|
10
|
+
*
|
|
11
|
+
* Two activation entry points exist, matching WFT-9/WFT-10's distinct
|
|
12
|
+
* producers:
|
|
13
|
+
*
|
|
14
|
+
* - {@link WorkflowCatalog.activateRegistered} — unconditional, used
|
|
15
|
+
* exclusively by `engine.register()`'s drain path
|
|
16
|
+
* (`core/engine/catalog-readiness.ts`). Never consults
|
|
17
|
+
* `checkWorkflowCompatibility`; always wins via a bounded 5-attempt CAS
|
|
18
|
+
* retry loop. Registering a different version of an already-registered
|
|
19
|
+
* workflow must never hard-fail construction or registration (the
|
|
20
|
+
* `version-mismatch-recovery.test.ts` precedent), which is why this path
|
|
21
|
+
* is unconditional. "Unconditional" is about bypassing
|
|
22
|
+
* `checkWorkflowCompatibility`, not immunity to failure in general: under
|
|
23
|
+
* sustained contention the bounded 5-attempt CAS retry can still exhaust
|
|
24
|
+
* and throw {@link WorkflowCatalogActivationConflictError}, which
|
|
25
|
+
* propagates out of `ensureWorkflowCatalogReady` and fails
|
|
26
|
+
* `Engine.create()` itself (safely — `Engine.create()`'s `try`/`catch`
|
|
27
|
+
* disposes the half-booted engine before rethrowing).
|
|
28
|
+
* - {@link WorkflowCatalog.activateCandidate} — the guarded primitive:
|
|
29
|
+
* `checkWorkflowCompatibility`-gated, single-shot CAS (no retry — the
|
|
30
|
+
* caller decides whether to re-read and retry), refuses on incompatibility
|
|
31
|
+
* or a stale expected generation. As of WFT-11, reachable by external
|
|
32
|
+
* callers via `engine.workflows.activate()` — the first production
|
|
33
|
+
* caller. Because a multi-writer caller can activate a revision this
|
|
34
|
+
* process's `#entries` cache never observed, the compatibility check
|
|
35
|
+
* reads through to durable storage rather than trusting the cache.
|
|
36
|
+
* Reused as-is by later dynamic-loading work (WFT-13+).
|
|
37
|
+
*
|
|
38
|
+
* @module core/catalog/workflow-catalog
|
|
39
|
+
*/
|
|
40
|
+
import { type Storage } from '../../storage/interface.ts';
|
|
41
|
+
import { type WorkflowCompatibilityPolicy } from '../contract/compatibility.ts';
|
|
42
|
+
import type { WorkflowRevisionManifest } from '../contract/types.ts';
|
|
43
|
+
import type { RegisteredWorkflowDefinition } from '../types/workflow-registry.ts';
|
|
44
|
+
import { type WorkflowCatalogRemovalOutcome } from './removal.ts';
|
|
45
|
+
import { type RestoredWorkflowCatalogState } from './storage-io.ts';
|
|
46
|
+
import type { WorkflowCatalogActivationResult, WorkflowCatalogActivePointer, WorkflowCatalogEntry, WorkflowRevisionRecord } from './types.ts';
|
|
47
|
+
/** Options accepted by {@link WorkflowCatalog.activateCandidate}. */
|
|
48
|
+
export type ActivateCandidateOptions = {
|
|
49
|
+
/** The generation this caller last observed; refused with `stale-generation` if it disagrees with the durable pointer. */
|
|
50
|
+
expectedGeneration?: number;
|
|
51
|
+
/** Compatibility policy; defaults to {@link DEFAULT_WORKFLOW_COMPATIBILITY_POLICY}. */
|
|
52
|
+
policy?: WorkflowCompatibilityPolicy;
|
|
53
|
+
};
|
|
54
|
+
export declare class WorkflowCatalog {
|
|
55
|
+
#private;
|
|
56
|
+
constructor(storage: Storage, seed?: RestoredWorkflowCatalogState);
|
|
57
|
+
/** Look up one installed `(name, revision)` entry, or `undefined` when not installed. */
|
|
58
|
+
getEntry(name: string, revision: string): WorkflowCatalogEntry | undefined;
|
|
59
|
+
/** Every installed revision of `name`, in no particular order. */
|
|
60
|
+
listRevisions(name: string): readonly WorkflowCatalogEntry[];
|
|
61
|
+
/** The current active pointer for `name`, or `undefined` when never activated. */
|
|
62
|
+
resolveActive(name: string): WorkflowCatalogActivePointer | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Resolve one installed `(name, revision)` entry as a public
|
|
65
|
+
* {@link WorkflowRevisionRecord} — cache hit first, else a durable
|
|
66
|
+
* read-through via {@link readCatalogEntry} (adopted into the local cache
|
|
67
|
+
* on hit, exactly like `install()`'s own read-through), else `undefined`
|
|
68
|
+
* when truly absent. No TOCTOU gap against a concurrent `install()` on
|
|
69
|
+
* this same instance: JS is single-threaded and neither this method nor
|
|
70
|
+
* `install()`'s fast (already-cached) path yields between the cache read
|
|
71
|
+
* and its use.
|
|
72
|
+
*/
|
|
73
|
+
resolveEntry(name: string, revision: string): Promise<WorkflowRevisionRecord | undefined>;
|
|
74
|
+
/**
|
|
75
|
+
* Every durably installed revision of `name`, sorted by {@link compareCodepoint}
|
|
76
|
+
* on `revision` for a deterministic order — never `localeCompare`, per
|
|
77
|
+
* this codebase's determinism rule. Durable scan via
|
|
78
|
+
* {@link scanCatalogEntriesForName}, validated the same fail-closed way
|
|
79
|
+
* {@link restoreWorkflowCatalog} validates every entry it restores.
|
|
80
|
+
* Returns an empty array for an unknown name rather than throwing.
|
|
81
|
+
*/
|
|
82
|
+
listInstalledRevisions(name: string): Promise<readonly WorkflowRevisionRecord[]>;
|
|
83
|
+
/**
|
|
84
|
+
* Whether `(name, revision)` is already durably installed — checked
|
|
85
|
+
* against this process's in-memory cache first, then, on a cache miss,
|
|
86
|
+
* durable storage itself (a different process may have installed it).
|
|
87
|
+
* Used by `catalog-events.ts`'s installed/activated/draining dispatch
|
|
88
|
+
* helper to decide whether an activation call is installing genuinely
|
|
89
|
+
* new content, since `install()`'s own cache-hit branch does not
|
|
90
|
+
* distinguish "already known to this process" from "just installed a
|
|
91
|
+
* moment ago by this same call."
|
|
92
|
+
*/
|
|
93
|
+
hasInstalled(name: string, revision: string): Promise<boolean>;
|
|
94
|
+
/**
|
|
95
|
+
* Durably resolve `name`'s active pointer — always reads through to
|
|
96
|
+
* durable storage rather than trusting the in-memory `#active` cache.
|
|
97
|
+
* Deliberately NOT cache-first like {@link hasInstalled}: `hasInstalled`'s
|
|
98
|
+
* cache-hit short-circuit can only go stale in the SAFE direction (a
|
|
99
|
+
* different process's `remove()` durably deleting an entry this
|
|
100
|
+
* process's `#entries` still holds still lets `catalog.remove()`'s own
|
|
101
|
+
* durable re-read refuse with `'not-found'` rather than double-deleting),
|
|
102
|
+
* whereas a name's active pointer has no such one-directional guarantee —
|
|
103
|
+
* a second engine/process can durably move it (e.g. via
|
|
104
|
+
* `activateCandidate`, or a second engine holding the ADR 0002
|
|
105
|
+
* workflow-lease) to a revision this process never installed at all, so a
|
|
106
|
+
* stale cache HIT here can misreport a durably-active revision as
|
|
107
|
+
* inactive. Used by removal and diagnostics
|
|
108
|
+
* (`core/engine/catalog-removal.ts`) so a durably-active revision is
|
|
109
|
+
* never misreported as inactive/removable; `resolveActive` stays the
|
|
110
|
+
* cheap, synchronous, best-effort accessor for in-process callers (e.g.
|
|
111
|
+
* `reserveInFlightStart`) that only care about this process's own view.
|
|
112
|
+
*/
|
|
113
|
+
resolveActiveDurable(name: string): Promise<WorkflowCatalogActivePointer | undefined>;
|
|
114
|
+
/**
|
|
115
|
+
* Durably remove the installed `(name, revision)` entry — delegates the
|
|
116
|
+
* CAS mechanics to {@link removeCatalogEntry}. On success, evicts the
|
|
117
|
+
* entry from this process's in-memory `#entries` cache too, so a
|
|
118
|
+
* subsequent `getEntry`/`listRevisions` call in this same process never
|
|
119
|
+
* observes a removed revision. Refuses (`'active'`) when `revision` is
|
|
120
|
+
* currently the active pointer for `name` — a structural invariant this
|
|
121
|
+
* method itself enforces, independent of any reference-count decision a
|
|
122
|
+
* caller layers on top (see `core/engine/catalog-removal.ts`).
|
|
123
|
+
*/
|
|
124
|
+
remove(name: string, revision: string): Promise<WorkflowCatalogRemovalOutcome>;
|
|
125
|
+
/**
|
|
126
|
+
* Install `manifest` (paired with `definition`, when this process holds
|
|
127
|
+
* one). Idempotent on a byte-identical reinstall for the same
|
|
128
|
+
* `(name, revision)` key; throws {@link WorkflowCatalogConflictError} when
|
|
129
|
+
* an existing entry for that key has different manifest content —
|
|
130
|
+
* checked against BOTH this process's in-memory cache and, on a cache
|
|
131
|
+
* miss, durable storage itself (a different `WorkflowCatalog`
|
|
132
|
+
* instance/process may already have installed this exact key). The
|
|
133
|
+
* durable write is CAS-guarded ({@link writeCatalogEntry}) rather than a
|
|
134
|
+
* plain `put`, so two processes racing to install genuinely different
|
|
135
|
+
* content under the same key — possible whenever `revision` is an
|
|
136
|
+
* explicit, non-content-derived caller value rather than the default
|
|
137
|
+
* content hash — cannot silently last-write-win each other; the loser
|
|
138
|
+
* re-reads and resolves through the same idempotent/conflict check.
|
|
139
|
+
*
|
|
140
|
+
* Defensively re-validates `name` against the wire-safe name grammar even
|
|
141
|
+
* though `engine.register()`'s existing `validateWorkflowOrActivityName`
|
|
142
|
+
* already guarantees this for the only current producer — `install()` is
|
|
143
|
+
* deliberately safe for a future producer that is not `engine.register()`.
|
|
144
|
+
*/
|
|
145
|
+
install(manifest: WorkflowRevisionManifest, definition?: RegisteredWorkflowDefinition): Promise<WorkflowCatalogEntry>;
|
|
146
|
+
/**
|
|
147
|
+
* Unconditionally activate `manifest` for `name`, installing it first if
|
|
148
|
+
* needed. Used exclusively by `engine.register()`'s drain path — never
|
|
149
|
+
* consults `checkWorkflowCompatibility`. Reactivating the currently active
|
|
150
|
+
* revision is a no-op (generation unchanged); activating a different
|
|
151
|
+
* revision bumps the generation by exactly 1. Retries the CAS write up to
|
|
152
|
+
* {@link MAX_ACTIVATE_REGISTERED_ATTEMPTS} times under contention, throwing
|
|
153
|
+
* {@link WorkflowCatalogActivationConflictError} on exhaustion.
|
|
154
|
+
*/
|
|
155
|
+
activateRegistered(name: string, manifest: WorkflowRevisionManifest, definition: RegisteredWorkflowDefinition): Promise<WorkflowCatalogActivePointer>;
|
|
156
|
+
/**
|
|
157
|
+
* The guarded activation primitive: reads the currently-active manifest
|
|
158
|
+
* for `name` (absence is treated as automatically compatible — first
|
|
159
|
+
* activation), checks `checkWorkflowCompatibility`, and refuses rather
|
|
160
|
+
* than applies when incompatible or when `expectedGeneration` disagrees
|
|
161
|
+
* with the durably-read generation. Single-shot CAS write — no retry; the
|
|
162
|
+
* caller decides whether to re-read and retry.
|
|
163
|
+
*/
|
|
164
|
+
activateCandidate(name: string, candidateManifest: WorkflowRevisionManifest, options?: ActivateCandidateOptions): Promise<WorkflowCatalogActivationResult>;
|
|
165
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {
|
|
2
|
+
KEYS,
|
|
3
|
+
requireStorageCapability,
|
|
4
|
+
storageConditionalBatch
|
|
5
|
+
} from "../../storage/interface.js";
|
|
6
|
+
import { compareCodepoint } from "../compare-codepoint.js";
|
|
7
|
+
import {
|
|
8
|
+
checkWorkflowCompatibility,
|
|
9
|
+
DEFAULT_WORKFLOW_COMPATIBILITY_POLICY
|
|
10
|
+
} from "../contract/compatibility.js";
|
|
11
|
+
import { validateWorkflowOrActivityName } from "../types/name-grammar.js";
|
|
12
|
+
import { encodeActivePointer, manifestsAreByteIdentical } from "./codec.js";
|
|
13
|
+
import {
|
|
14
|
+
WorkflowCatalogActivationConflictError,
|
|
15
|
+
WorkflowCatalogActiveEntryMissingError,
|
|
16
|
+
WorkflowCatalogConflictError
|
|
17
|
+
} from "./errors.js";
|
|
18
|
+
import { removeCatalogEntry } from "./removal.js";
|
|
19
|
+
import {
|
|
20
|
+
readActivePointer,
|
|
21
|
+
readCatalogEntry,
|
|
22
|
+
scanCatalogEntriesForName,
|
|
23
|
+
writeCatalogEntry
|
|
24
|
+
} from "./storage-io.js";
|
|
25
|
+
const MAX_ACTIVATE_REGISTERED_ATTEMPTS = 5;
|
|
26
|
+
|
|
27
|
+
export class WorkflowCatalog {
|
|
28
|
+
#storage;
|
|
29
|
+
#entries;
|
|
30
|
+
#active;
|
|
31
|
+
constructor(storage, seed) {
|
|
32
|
+
this.#storage = storage;
|
|
33
|
+
this.#entries = seed?.entries ?? new Map;
|
|
34
|
+
this.#active = seed?.active ?? new Map;
|
|
35
|
+
}
|
|
36
|
+
getEntry(name, revision) {
|
|
37
|
+
return this.#entries.get(name)?.get(revision);
|
|
38
|
+
}
|
|
39
|
+
listRevisions(name) {
|
|
40
|
+
const byName = this.#entries.get(name);
|
|
41
|
+
return byName === void 0 ? [] : [...byName.values()];
|
|
42
|
+
}
|
|
43
|
+
resolveActive(name) {
|
|
44
|
+
return this.#active.get(name);
|
|
45
|
+
}
|
|
46
|
+
async resolveEntry(name, revision) {
|
|
47
|
+
const cached = this.getEntry(name, revision);
|
|
48
|
+
if (cached !== void 0)
|
|
49
|
+
return { manifest: cached.manifest, installedAt: cached.installedAt };
|
|
50
|
+
const durable = await readCatalogEntry(this.#storage, name, revision);
|
|
51
|
+
if (durable === null)
|
|
52
|
+
return;
|
|
53
|
+
this.#cacheEntry(name, revision, durable);
|
|
54
|
+
return durable;
|
|
55
|
+
}
|
|
56
|
+
async listInstalledRevisions(name) {
|
|
57
|
+
return (await scanCatalogEntriesForName(this.#storage, name)).toSorted((a, b) => compareCodepoint(a.manifest.revision, b.manifest.revision));
|
|
58
|
+
}
|
|
59
|
+
async hasInstalled(name, revision) {
|
|
60
|
+
if (this.getEntry(name, revision) !== void 0)
|
|
61
|
+
return !0;
|
|
62
|
+
return await readCatalogEntry(this.#storage, name, revision) !== null;
|
|
63
|
+
}
|
|
64
|
+
async resolveActiveDurable(name) {
|
|
65
|
+
return await readActivePointer(this.#storage, name) ?? void 0;
|
|
66
|
+
}
|
|
67
|
+
async remove(name, revision) {
|
|
68
|
+
requireStorageCapability(this.#storage, "conditionalBatch", "workflow catalog removal");
|
|
69
|
+
const result = await removeCatalogEntry(this.#storage, name, revision);
|
|
70
|
+
if (result.outcome === "removed")
|
|
71
|
+
this.#entries.get(name)?.delete(revision);
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
async install(manifest, definition) {
|
|
75
|
+
validateWorkflowOrActivityName(manifest.name, "workflow");
|
|
76
|
+
const existing = this.getEntry(manifest.name, manifest.revision);
|
|
77
|
+
if (existing !== void 0) {
|
|
78
|
+
if (!manifestsAreByteIdentical(existing.manifest, manifest))
|
|
79
|
+
throw new WorkflowCatalogConflictError(manifest.name, manifest.revision);
|
|
80
|
+
return existing;
|
|
81
|
+
}
|
|
82
|
+
const durable = await readCatalogEntry(this.#storage, manifest.name, manifest.revision);
|
|
83
|
+
if (durable !== null)
|
|
84
|
+
return this.#adoptDurableEntry(manifest, durable, definition);
|
|
85
|
+
const installedAt = Date.now();
|
|
86
|
+
if (!await writeCatalogEntry(this.#storage, manifest, installedAt)) {
|
|
87
|
+
const raced = await readCatalogEntry(this.#storage, manifest.name, manifest.revision);
|
|
88
|
+
if (raced === null)
|
|
89
|
+
throw new WorkflowCatalogConflictError(manifest.name, manifest.revision);
|
|
90
|
+
return this.#adoptDurableEntry(manifest, raced, definition);
|
|
91
|
+
}
|
|
92
|
+
return this.#cacheEntry(manifest.name, manifest.revision, {
|
|
93
|
+
manifest,
|
|
94
|
+
installedAt,
|
|
95
|
+
...definition === void 0 ? {} : { definition }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
#adoptDurableEntry(manifest, durable, definition) {
|
|
99
|
+
if (!manifestsAreByteIdentical(durable.manifest, manifest))
|
|
100
|
+
throw new WorkflowCatalogConflictError(manifest.name, manifest.revision);
|
|
101
|
+
return this.#cacheEntry(manifest.name, manifest.revision, {
|
|
102
|
+
manifest: durable.manifest,
|
|
103
|
+
installedAt: durable.installedAt,
|
|
104
|
+
...definition === void 0 ? {} : { definition }
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
#cacheEntry(name, revision, entry) {
|
|
108
|
+
let byName = this.#entries.get(name);
|
|
109
|
+
if (byName === void 0) {
|
|
110
|
+
byName = new Map;
|
|
111
|
+
this.#entries.set(name, byName);
|
|
112
|
+
}
|
|
113
|
+
byName.set(revision, entry);
|
|
114
|
+
return entry;
|
|
115
|
+
}
|
|
116
|
+
async activateRegistered(name, manifest, definition) {
|
|
117
|
+
requireStorageCapability(this.#storage, "conditionalBatch", "workflow catalog activation");
|
|
118
|
+
await this.install(manifest, definition);
|
|
119
|
+
for (let attempt = 1;attempt <= MAX_ACTIVATE_REGISTERED_ATTEMPTS; attempt++) {
|
|
120
|
+
const current = await readActivePointer(this.#storage, name);
|
|
121
|
+
if (current !== null && current.revision === manifest.revision) {
|
|
122
|
+
this.#active.set(name, current);
|
|
123
|
+
return current;
|
|
124
|
+
}
|
|
125
|
+
const nextGeneration = current === null ? 1 : current.generation + 1, nextPointer = {
|
|
126
|
+
revision: manifest.revision,
|
|
127
|
+
generation: nextGeneration,
|
|
128
|
+
activatedAt: Date.now()
|
|
129
|
+
};
|
|
130
|
+
if (await storageConditionalBatch(this.#storage, [
|
|
131
|
+
{
|
|
132
|
+
key: KEYS.catalogActive(name),
|
|
133
|
+
expectedValue: current === null ? null : encodeActivePointer(current)
|
|
134
|
+
}
|
|
135
|
+
], [{ type: "put", key: KEYS.catalogActive(name), value: encodeActivePointer(nextPointer) }])) {
|
|
136
|
+
this.#active.set(name, nextPointer);
|
|
137
|
+
return nextPointer;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw new WorkflowCatalogActivationConflictError(name, MAX_ACTIVATE_REGISTERED_ATTEMPTS);
|
|
141
|
+
}
|
|
142
|
+
async activateCandidate(name, candidateManifest, options) {
|
|
143
|
+
requireStorageCapability(this.#storage, "conditionalBatch", "workflow catalog activation");
|
|
144
|
+
await this.install(candidateManifest);
|
|
145
|
+
const currentPointer = await readActivePointer(this.#storage, name), refusal = await this.#refuseIncompatibleOrStaleCandidate(name, candidateManifest, currentPointer, options);
|
|
146
|
+
if (refusal !== void 0)
|
|
147
|
+
return refusal;
|
|
148
|
+
const nextGeneration = currentPointer === null ? 1 : currentPointer.generation + 1, nextPointer = {
|
|
149
|
+
revision: candidateManifest.revision,
|
|
150
|
+
generation: nextGeneration,
|
|
151
|
+
activatedAt: Date.now()
|
|
152
|
+
};
|
|
153
|
+
if (!await storageConditionalBatch(this.#storage, [
|
|
154
|
+
{
|
|
155
|
+
key: KEYS.catalogActive(name),
|
|
156
|
+
expectedValue: currentPointer === null ? null : encodeActivePointer(currentPointer)
|
|
157
|
+
}
|
|
158
|
+
], [{ type: "put", key: KEYS.catalogActive(name), value: encodeActivePointer(nextPointer) }]))
|
|
159
|
+
return { applied: !1, reason: "conflict" };
|
|
160
|
+
this.#active.set(name, nextPointer);
|
|
161
|
+
return { applied: !0, pointer: nextPointer };
|
|
162
|
+
}
|
|
163
|
+
async#refuseIncompatibleOrStaleCandidate(name, candidateManifest, currentPointer, options) {
|
|
164
|
+
if (currentPointer === null)
|
|
165
|
+
return this.#refuseStaleFirstActivation(options);
|
|
166
|
+
const generationRefusal = this.#refuseMissingOrStaleGeneration(currentPointer, options);
|
|
167
|
+
if (generationRefusal !== void 0)
|
|
168
|
+
return generationRefusal;
|
|
169
|
+
return this.#refuseIncompatibleCandidate(name, candidateManifest, currentPointer, options);
|
|
170
|
+
}
|
|
171
|
+
#refuseStaleFirstActivation(options) {
|
|
172
|
+
if (options?.expectedGeneration !== void 0 && options.expectedGeneration !== 0)
|
|
173
|
+
return { applied: !1, reason: "stale-generation", currentGeneration: 0 };
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
#refuseMissingOrStaleGeneration(currentPointer, options) {
|
|
177
|
+
if (options?.expectedGeneration === void 0)
|
|
178
|
+
return {
|
|
179
|
+
applied: !1,
|
|
180
|
+
reason: "expected-generation-required",
|
|
181
|
+
currentGeneration: currentPointer.generation
|
|
182
|
+
};
|
|
183
|
+
if (options.expectedGeneration !== currentPointer.generation)
|
|
184
|
+
return {
|
|
185
|
+
applied: !1,
|
|
186
|
+
reason: "stale-generation",
|
|
187
|
+
currentGeneration: currentPointer.generation
|
|
188
|
+
};
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
async#refuseIncompatibleCandidate(name, candidateManifest, currentPointer, options) {
|
|
192
|
+
const currentEntry = await this.resolveEntry(name, currentPointer.revision);
|
|
193
|
+
if (currentEntry === void 0)
|
|
194
|
+
throw new WorkflowCatalogActiveEntryMissingError(name, currentPointer.revision);
|
|
195
|
+
const verdict = checkWorkflowCompatibility(currentEntry.manifest, candidateManifest, options?.policy ?? DEFAULT_WORKFLOW_COMPATIBILITY_POLICY);
|
|
196
|
+
return verdict.compatible ? void 0 : { applied: !1, reason: "incompatible", verdict };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Order two strings by codepoint rather than `localeCompare` (project rule:
|
|
3
|
+
* deterministic comparisons in runtime logic). A standalone leaf module so
|
|
4
|
+
* every codepoint sort across the registry-snapshot family of modules —
|
|
5
|
+
* `registry-snapshot.ts`'s `sortedWorkflows`/`sortedActivities` and
|
|
6
|
+
* `registry-workflow-manifest.ts`'s scoped-activity sort — shares one
|
|
7
|
+
* implementation of the unreachable-in-isolation tie branch, rather than
|
|
8
|
+
* each defining its own uncovered copy, without creating an import cycle
|
|
9
|
+
* between those two modules.
|
|
10
|
+
*
|
|
11
|
+
* @module core/compare-codepoint
|
|
12
|
+
*/
|
|
13
|
+
export declare function compareCodepoint(a: string, b: string): number;
|