@lostgradient/weft 0.23.0 → 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.
Files changed (295) hide show
  1. package/README.md +72 -4
  2. package/dist/cli/codegen-emit-dedup.d.ts +81 -0
  3. package/dist/cli/codegen-emit-dedup.js +45 -0
  4. package/dist/cli/codegen-emit-registry.d.ts +90 -0
  5. package/dist/cli/codegen-emit-registry.js +50 -0
  6. package/dist/cli/codegen-emit.d.ts +26 -39
  7. package/dist/cli/codegen-emit.js +4 -27
  8. package/dist/cli/codegen-validate.d.ts +49 -0
  9. package/dist/cli/codegen-validate.js +186 -0
  10. package/dist/cli/codegen.js +6 -87
  11. package/dist/cli/generated/operation-catalog.snapshot.json +287 -3
  12. package/dist/cli/generated/operation-client.generated.d.ts +324 -10
  13. package/dist/cli/generated/operation-client.generated.js +12 -0
  14. package/dist/cli/output.js +1 -1
  15. package/dist/cli/storage-factory.js +2 -0
  16. package/dist/cli/workflow-commands.js +2 -0
  17. package/dist/cli-main.js +1 -1
  18. package/dist/core/application-payload-digest.d.ts +42 -0
  19. package/dist/core/application-payload-digest.js +76 -0
  20. package/dist/core/application-primitive-abort.d.ts +50 -0
  21. package/dist/core/application-primitive-abort.js +40 -0
  22. package/dist/core/application-primitive-attempt-registry.d.ts +74 -0
  23. package/dist/core/application-primitive-attempt-registry.js +73 -0
  24. package/dist/core/application-primitive-codec.d.ts +36 -0
  25. package/dist/core/application-primitive-codec.js +56 -0
  26. package/dist/core/application-primitive-commit.d.ts +67 -0
  27. package/dist/core/application-primitive-commit.js +66 -0
  28. package/dist/core/application-primitive-guards.d.ts +62 -0
  29. package/dist/core/application-primitive-guards.js +89 -0
  30. package/dist/core/application-primitive-payload.d.ts +29 -0
  31. package/dist/core/application-primitive-payload.js +65 -0
  32. package/dist/core/application-primitive-timing.d.ts +18 -0
  33. package/dist/core/application-primitive-timing.js +22 -0
  34. package/dist/core/atomic-state.js +2 -0
  35. package/dist/core/catalog/codec.d.ts +40 -0
  36. package/dist/core/catalog/codec.js +45 -0
  37. package/dist/core/catalog/errors.d.ts +104 -0
  38. package/dist/core/catalog/errors.js +41 -0
  39. package/dist/core/catalog/index.d.ts +25 -0
  40. package/dist/core/catalog/index.js +15 -0
  41. package/dist/core/catalog/reference-counts.d.ts +105 -0
  42. package/dist/core/catalog/reference-counts.js +28 -0
  43. package/dist/core/catalog/removal.d.ts +52 -0
  44. package/dist/core/catalog/removal.js +14 -0
  45. package/dist/core/catalog/storage-io.d.ts +78 -0
  46. package/dist/core/catalog/storage-io.js +106 -0
  47. package/dist/core/catalog/types.d.ts +108 -0
  48. package/dist/core/catalog/types.js +0 -0
  49. package/dist/core/catalog/workflow-catalog.d.ts +165 -0
  50. package/dist/core/catalog/workflow-catalog.js +198 -0
  51. package/dist/core/compare-codepoint.d.ts +13 -0
  52. package/dist/core/compare-codepoint.js +7 -0
  53. package/dist/core/compression.js +2 -0
  54. package/dist/core/context/attributes.d.ts +1 -1
  55. package/dist/core/context/durable-operations.d.ts +2 -2
  56. package/dist/core/context/index.js +5 -3
  57. package/dist/core/context/state-namespace.d.ts +1 -1
  58. package/dist/core/contract/build.d.ts +64 -0
  59. package/dist/core/contract/build.js +99 -0
  60. package/dist/core/contract/compatibility.d.ts +187 -0
  61. package/dist/core/contract/compatibility.js +37 -0
  62. package/dist/core/contract/failure.d.ts +64 -0
  63. package/dist/core/contract/failure.js +4 -0
  64. package/dist/core/contract/hash.d.ts +92 -0
  65. package/dist/core/contract/hash.js +34 -0
  66. package/dist/core/contract/index.d.ts +21 -0
  67. package/dist/core/contract/index.js +22 -0
  68. package/dist/core/contract/limits.d.ts +74 -0
  69. package/dist/core/contract/limits.js +1 -0
  70. package/dist/core/contract/manifest-parse-schema.d.ts +37 -0
  71. package/dist/core/contract/manifest-parse-schema.js +106 -0
  72. package/dist/core/contract/manifest-parse.d.ts +76 -0
  73. package/dist/core/contract/manifest-parse.js +189 -0
  74. package/dist/core/contract/manifest.d.ts +56 -0
  75. package/dist/core/contract/manifest.js +31 -0
  76. package/dist/core/contract/normalize.d.ts +97 -0
  77. package/dist/core/contract/normalize.js +127 -0
  78. package/dist/core/contract/revision.d.ts +29 -0
  79. package/dist/core/contract/revision.js +5 -0
  80. package/dist/core/contract/types.d.ts +274 -0
  81. package/dist/core/contract/types.js +1 -0
  82. package/dist/core/engine/catalog-activation.d.ts +40 -0
  83. package/dist/core/engine/catalog-activation.js +17 -0
  84. package/dist/core/engine/catalog-events.d.ts +36 -0
  85. package/dist/core/engine/catalog-events.js +15 -0
  86. package/dist/core/engine/catalog-readiness.d.ts +49 -0
  87. package/dist/core/engine/catalog-readiness.js +59 -0
  88. package/dist/core/engine/catalog-removal.d.ts +132 -0
  89. package/dist/core/engine/catalog-removal.js +70 -0
  90. package/dist/core/engine/construction.d.ts +1 -1
  91. package/dist/core/engine/construction.js +4 -2
  92. package/dist/core/engine/disposal.js +9 -0
  93. package/dist/core/engine/engine-workflows-namespace.d.ts +130 -0
  94. package/dist/core/engine/engine-workflows-namespace.js +49 -0
  95. package/dist/core/engine/finalizer-metadata.d.ts +22 -0
  96. package/dist/core/engine/finalizer-metadata.js +12 -0
  97. package/dist/core/engine/handle-result.js +2 -0
  98. package/dist/core/engine/index.d.ts +83 -0
  99. package/dist/core/engine/index.js +61 -0
  100. package/dist/core/engine/internals.d.ts +44 -1
  101. package/dist/core/engine/internals.js +6 -0
  102. package/dist/core/engine/lifecycle/start.js +3 -0
  103. package/dist/core/engine/registration.d.ts +76 -0
  104. package/dist/core/engine/registration.js +12 -6
  105. package/dist/core/engine/source-registration.d.ts +42 -0
  106. package/dist/core/engine/source-registration.js +36 -0
  107. package/dist/core/engine/source-resolution.d.ts +85 -0
  108. package/dist/core/engine/source-resolution.js +119 -0
  109. package/dist/core/engine/validation.js +2 -2
  110. package/dist/core/events/catalog-events.d.ts +117 -0
  111. package/dist/core/events/catalog-events.js +62 -0
  112. package/dist/core/events/event-map.d.ts +6 -0
  113. package/dist/core/events/index.d.ts +1 -0
  114. package/dist/core/events/index.js +1 -0
  115. package/dist/core/mailbox-admission.d.ts +26 -0
  116. package/dist/core/mailbox-admission.js +130 -0
  117. package/dist/core/mailbox-claims.d.ts +251 -0
  118. package/dist/core/mailbox-claims.js +0 -0
  119. package/dist/core/mailbox-codec.d.ts +32 -0
  120. package/dist/core/mailbox-codec.js +221 -0
  121. package/dist/core/mailbox-contract.d.ts +290 -0
  122. package/dist/core/mailbox-contract.js +0 -0
  123. package/dist/core/mailbox-delivery.d.ts +37 -0
  124. package/dist/core/mailbox-delivery.js +193 -0
  125. package/dist/core/mailbox-guards.d.ts +78 -0
  126. package/dist/core/mailbox-guards.js +58 -0
  127. package/dist/core/mailbox-index-codec.d.ts +41 -0
  128. package/dist/core/mailbox-index-codec.js +71 -0
  129. package/dist/core/mailbox-internals.d.ts +156 -0
  130. package/dist/core/mailbox-internals.js +154 -0
  131. package/dist/core/mailbox-maintenance.d.ts +29 -0
  132. package/dist/core/mailbox-maintenance.js +196 -0
  133. package/dist/core/mailbox-settlement.d.ts +69 -0
  134. package/dist/core/mailbox-settlement.js +206 -0
  135. package/dist/core/mailbox-storage.d.ts +117 -0
  136. package/dist/core/mailbox-storage.js +115 -0
  137. package/dist/core/mailbox-transition-helpers.d.ts +70 -0
  138. package/dist/core/mailbox-transition-helpers.js +43 -0
  139. package/dist/core/mailbox-transitions-recovery.d.ts +28 -0
  140. package/dist/core/mailbox-transitions-recovery.js +64 -0
  141. package/dist/core/mailbox-transitions.d.ts +138 -0
  142. package/dist/core/mailbox-transitions.js +180 -0
  143. package/dist/core/mailbox-types.d.ts +406 -0
  144. package/dist/core/mailbox-types.js +16 -0
  145. package/dist/core/mailbox-validation.d.ts +65 -0
  146. package/dist/core/mailbox-validation.js +73 -0
  147. package/dist/core/mailbox-waits.d.ts +50 -0
  148. package/dist/core/mailbox-waits.js +97 -0
  149. package/dist/core/mailbox.d.ts +163 -0
  150. package/dist/core/mailbox.js +211 -0
  151. package/dist/core/outbox-claims.d.ts +307 -0
  152. package/dist/core/outbox-claims.js +0 -0
  153. package/dist/core/outbox-codec.d.ts +24 -0
  154. package/dist/core/outbox-codec.js +257 -0
  155. package/dist/core/outbox-contract.d.ts +346 -0
  156. package/dist/core/outbox-contract.js +0 -0
  157. package/dist/core/outbox-delivery.d.ts +34 -0
  158. package/dist/core/outbox-delivery.js +149 -0
  159. package/dist/core/outbox-drain.d.ts +29 -0
  160. package/dist/core/outbox-drain.js +158 -0
  161. package/dist/core/outbox-enqueue.d.ts +24 -0
  162. package/dist/core/outbox-enqueue.js +115 -0
  163. package/dist/core/outbox-guards.d.ts +76 -0
  164. package/dist/core/outbox-guards.js +60 -0
  165. package/dist/core/outbox-index-codec.d.ts +33 -0
  166. package/dist/core/outbox-index-codec.js +71 -0
  167. package/dist/core/outbox-internals.d.ts +99 -0
  168. package/dist/core/outbox-internals.js +159 -0
  169. package/dist/core/outbox-listing.d.ts +12 -0
  170. package/dist/core/outbox-listing.js +38 -0
  171. package/dist/core/outbox-maintenance.d.ts +28 -0
  172. package/dist/core/outbox-maintenance.js +191 -0
  173. package/dist/core/outbox-operations.d.ts +18 -0
  174. package/dist/core/outbox-operations.js +46 -0
  175. package/dist/core/outbox-runner.d.ts +30 -0
  176. package/dist/core/outbox-runner.js +217 -0
  177. package/dist/core/outbox-settlement.d.ts +57 -0
  178. package/dist/core/outbox-settlement.js +183 -0
  179. package/dist/core/outbox-storage.d.ts +107 -0
  180. package/dist/core/outbox-storage.js +127 -0
  181. package/dist/core/outbox-transition-helpers.d.ts +83 -0
  182. package/dist/core/outbox-transition-helpers.js +63 -0
  183. package/dist/core/outbox-transitions-recovery.d.ts +30 -0
  184. package/dist/core/outbox-transitions-recovery.js +42 -0
  185. package/dist/core/outbox-transitions.d.ts +145 -0
  186. package/dist/core/outbox-transitions.js +236 -0
  187. package/dist/core/outbox-types.d.ts +400 -0
  188. package/dist/core/outbox-types.js +20 -0
  189. package/dist/core/outbox-validation.d.ts +101 -0
  190. package/dist/core/outbox-validation.js +127 -0
  191. package/dist/core/outbox-waits.d.ts +39 -0
  192. package/dist/core/outbox-waits.js +88 -0
  193. package/dist/core/outbox.d.ts +156 -0
  194. package/dist/core/outbox.js +234 -0
  195. package/dist/core/registry-limits.d.ts +49 -0
  196. package/dist/core/registry-limits.js +10 -0
  197. package/dist/core/registry-schema-conversion.d.ts +20 -0
  198. package/dist/core/registry-schema-conversion.js +22 -0
  199. package/dist/core/registry-snapshot.d.ts +91 -42
  200. package/dist/core/registry-snapshot.js +39 -77
  201. package/dist/core/registry-workflow-contract-draft.d.ts +32 -0
  202. package/dist/core/registry-workflow-contract-draft.js +28 -0
  203. package/dist/core/registry-workflow-manifest.d.ts +122 -0
  204. package/dist/core/registry-workflow-manifest.js +92 -0
  205. package/dist/core/source/errors.d.ts +55 -0
  206. package/dist/core/source/errors.js +13 -0
  207. package/dist/core/source/index.d.ts +19 -0
  208. package/dist/core/source/index.js +4 -0
  209. package/dist/core/source/resolvers.d.ts +45 -0
  210. package/dist/core/source/resolvers.js +9 -0
  211. package/dist/core/source/types.d.ts +211 -0
  212. package/dist/core/source/types.js +0 -0
  213. package/dist/core/source/validate.d.ts +91 -0
  214. package/dist/core/source/validate.js +62 -0
  215. package/dist/core/source/workflow-source.d.ts +43 -0
  216. package/dist/core/source/workflow-source.js +16 -0
  217. package/dist/core/types/workflow-registries.d.ts +7 -0
  218. package/dist/core/types/workflow-registry.d.ts +12 -0
  219. package/dist/core/versioning.js +2 -0
  220. package/dist/core/weft-error.d.ts +1 -1
  221. package/dist/core/weft-error.js +9 -1
  222. package/dist/http.js +2 -2
  223. package/dist/index.d.ts +23 -2
  224. package/dist/index.js +50 -0
  225. package/dist/indexeddb.js +1 -1
  226. package/dist/json-schema.js +3 -3
  227. package/dist/mcp/resources.js +2 -0
  228. package/dist/runtime/portable.d.ts +9 -1
  229. package/dist/server/fault-to-json-rpc.js +3 -1
  230. package/dist/server/handler/auth-context-principal.js +4 -0
  231. package/dist/server/json-rpc-http.js +2 -0
  232. package/dist/server/json-rpc-websocket.js +6 -4
  233. package/dist/server/operation-catalog/index.d.ts +1 -1
  234. package/dist/server/operation-catalog/pipeline-helpers.js +2 -0
  235. package/dist/server/operation-catalog/types.d.ts +17 -0
  236. package/dist/server/operation-fault.d.ts +3 -0
  237. package/dist/server/operation-fault.js +3 -1
  238. package/dist/server/operations/activate-workflow-revision.d.ts +30 -0
  239. package/dist/server/operations/activate-workflow-revision.js +69 -0
  240. package/dist/server/operations/fleet-events-subscription.js +1 -1
  241. package/dist/server/operations/fork-workflow.js +1 -1
  242. package/dist/server/operations/get-active-workflow-revision.d.ts +32 -0
  243. package/dist/server/operations/get-active-workflow-revision.js +46 -0
  244. package/dist/server/operations/get-catalog-diagnostics.d.ts +85 -0
  245. package/dist/server/operations/get-catalog-diagnostics.js +57 -0
  246. package/dist/server/operations/get-registry.js +17 -4
  247. package/dist/server/operations/get-workflow-revision.d.ts +25 -0
  248. package/dist/server/operations/get-workflow-revision.js +50 -0
  249. package/dist/server/operations/install-workflow-revision.d.ts +27 -0
  250. package/dist/server/operations/install-workflow-revision.js +46 -0
  251. package/dist/server/operations/list-workflow-revisions.d.ts +20 -0
  252. package/dist/server/operations/list-workflow-revisions.js +40 -0
  253. package/dist/server/operations/start-or-signal-workflow.js +1 -1
  254. package/dist/server/operations/start-workflow.js +1 -1
  255. package/dist/server/operations/static-registrations.js +36 -0
  256. package/dist/server/operations/storage.js +2 -2
  257. package/dist/server/operations/submit-review-decision.js +1 -1
  258. package/dist/server/operations/workflow-catalog-operation-helpers.d.ts +59 -0
  259. package/dist/server/operations/workflow-catalog-operation-helpers.js +135 -0
  260. package/dist/server/runtime/websocket-upgrade.js +2 -0
  261. package/dist/storage/bun-sql.js +49 -6
  262. package/dist/storage/catalog-keys.d.ts +45 -0
  263. package/dist/storage/catalog-keys.js +6 -0
  264. package/dist/storage/compressed-storage.js +1 -1
  265. package/dist/storage/derived-operations.js +1 -1
  266. package/dist/storage/index.d.ts +26 -1
  267. package/dist/storage/interface.d.ts +26 -1
  268. package/dist/storage/interface.js +1 -1
  269. package/dist/storage/key-encoding.d.ts +15 -0
  270. package/dist/storage/key-encoding.js +3 -0
  271. package/dist/storage/key-prefixes.d.ts +1 -1
  272. package/dist/storage/key-prefixes.js +15 -0
  273. package/dist/storage/lmdb.d.ts +26 -1
  274. package/dist/storage/lmdb.js +1 -1
  275. package/dist/storage/mailbox-keys.d.ts +80 -0
  276. package/dist/storage/mailbox-keys.js +14 -0
  277. package/dist/storage/memory.js +1 -1
  278. package/dist/storage/neon.js +2 -2
  279. package/dist/storage/node-sqlite-loader.d.ts +1 -1
  280. package/dist/storage/node-sqlite-loader.js +2 -2
  281. package/dist/storage/node-sqlite.js +51 -8
  282. package/dist/storage/outbox-keys.d.ts +77 -0
  283. package/dist/storage/outbox-keys.js +14 -0
  284. package/dist/storage/postgres.js +2 -2
  285. package/dist/storage/resolve.js +1 -1
  286. package/dist/storage/scoped-storage.js +1 -1
  287. package/dist/storage/storage-configuration.d.ts +6 -0
  288. package/dist/storage/testing.js +1 -1
  289. package/dist/storage/turso.js +2 -2
  290. package/dist/version.d.ts +1 -1
  291. package/dist/version.js +1 -1
  292. package/dist/web-extension.js +1 -1
  293. package/dist/worker/manifest/registry-contract-builder.d.ts +21 -3
  294. package/dist/worker/manifest/registry-contract-builder.js +40 -31
  295. 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&nbsp;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;