@statewalker/cellb 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +323 -2
- package/dist/adapters/files-backup.d.ts +16 -0
- package/dist/adapters/files-backup.js +32 -0
- package/dist/adapters/files-backup.js.map +1 -0
- package/dist/adapters/files-changelog.d.ts +33 -0
- package/dist/adapters/files-changelog.js +96 -0
- package/dist/adapters/files-changelog.js.map +1 -0
- package/dist/adapters/files-ltx.d.ts +35 -0
- package/dist/adapters/files-ltx.js +9 -0
- package/dist/adapters/files-ltx.js.map +1 -0
- package/dist/adapters/ltx-vfs.d.ts +21 -0
- package/dist/adapters/ltx-vfs.js +286 -0
- package/dist/adapters/ltx-vfs.js.map +1 -0
- package/dist/adapters/node-sqlite.d.ts +45 -0
- package/dist/adapters/node-sqlite.js +254 -0
- package/dist/adapters/node-sqlite.js.map +1 -0
- package/dist/adapters/wasm-page-hook.d.ts +26 -0
- package/dist/adapters/wasm-page-hook.js +55 -0
- package/dist/adapters/wasm-page-hook.js.map +1 -0
- package/dist/adapters/wasm-session.d.ts +11 -0
- package/dist/adapters/wasm-session.js +76 -0
- package/dist/adapters/wasm-session.js.map +1 -0
- package/dist/adapters/wasm-sqlite.d.ts +89 -0
- package/dist/adapters/wasm-sqlite.js +258 -0
- package/dist/adapters/wasm-sqlite.js.map +1 -0
- package/dist/bindings/assets.d.ts +26 -0
- package/dist/bindings/assets.js +260 -0
- package/dist/bindings/assets.js.map +1 -0
- package/dist/bindings/cache.d.ts +16 -0
- package/dist/bindings/cache.js +34 -0
- package/dist/bindings/cache.js.map +1 -0
- package/dist/bindings/celld-kv.d.ts +26 -0
- package/dist/bindings/celld-kv.js +81 -0
- package/dist/bindings/celld-kv.js.map +1 -0
- package/dist/bindings/celld-queues.d.ts +56 -0
- package/dist/bindings/celld-queues.js +223 -0
- package/dist/bindings/celld-queues.js.map +1 -0
- package/dist/bindings/celld-workflows.d.ts +22 -0
- package/dist/bindings/celld-workflows.js +54 -0
- package/dist/bindings/celld-workflows.js.map +1 -0
- package/dist/bindings/cron.d.ts +27 -0
- package/dist/bindings/cron.js +232 -0
- package/dist/bindings/cron.js.map +1 -0
- package/dist/bindings/d1-migrations.d.ts +22 -0
- package/dist/bindings/d1-migrations.js +32 -0
- package/dist/bindings/d1-migrations.js.map +1 -0
- package/dist/bindings/html-rewriter.d.ts +8 -0
- package/dist/bindings/html-rewriter.js +75 -0
- package/dist/bindings/html-rewriter.js.map +1 -0
- package/dist/bindings/r2-celld.d.ts +3 -0
- package/dist/bindings/r2-celld.js +113 -0
- package/dist/bindings/r2-celld.js.map +1 -0
- package/dist/bindings/r2.d.ts +236 -0
- package/dist/bindings/r2.js +419 -0
- package/dist/bindings/r2.js.map +1 -0
- package/dist/bindings/worker-loader.d.ts +83 -0
- package/dist/bindings/worker-loader.js +183 -0
- package/dist/bindings/worker-loader.js.map +1 -0
- package/dist/bindings/workers-ai.d.ts +39 -0
- package/dist/bindings/workers-ai.js +175 -0
- package/dist/bindings/workers-ai.js.map +1 -0
- package/dist/browser.d.ts +7 -0
- package/dist/browser.js +8 -0
- package/dist/browser.js.map +1 -0
- package/dist/bucket/celld-cells.d.ts +86 -0
- package/dist/bucket/celld-cells.js +273 -0
- package/dist/bucket/celld-cells.js.map +1 -0
- package/dist/bucket/celld-format.d.ts +10 -0
- package/dist/bucket/celld-format.js +27 -0
- package/dist/bucket/celld-format.js.map +1 -0
- package/dist/bucket/idb-object-store.d.ts +9 -0
- package/dist/bucket/idb-object-store.js +73 -0
- package/dist/bucket/idb-object-store.js.map +1 -0
- package/dist/bucket/ltx-log.d.ts +34 -0
- package/dist/bucket/ltx-log.js +289 -0
- package/dist/bucket/ltx-log.js.map +1 -0
- package/dist/bucket/object-store.d.ts +51 -0
- package/dist/bucket/object-store.js +114 -0
- package/dist/bucket/object-store.js.map +1 -0
- package/dist/bucket/s3-object-store.d.ts +25 -0
- package/dist/bucket/s3-object-store.js +151 -0
- package/dist/bucket/s3-object-store.js.map +1 -0
- package/dist/bucket/sqlite-object-store.d.ts +10 -0
- package/dist/bucket/sqlite-object-store.js +107 -0
- package/dist/bucket/sqlite-object-store.js.map +1 -0
- package/dist/core/alarms.d.ts +27 -0
- package/dist/core/alarms.js +88 -0
- package/dist/core/alarms.js.map +1 -0
- package/dist/core/async-context.d.ts +34 -0
- package/dist/core/async-context.js +50 -0
- package/dist/core/async-context.js.map +1 -0
- package/dist/core/celld-schema.d.ts +14 -0
- package/dist/core/celld-schema.js +34 -0
- package/dist/core/celld-schema.js.map +1 -0
- package/dist/core/durable-object.d.ts +28 -0
- package/dist/core/durable-object.js +31 -0
- package/dist/core/durable-object.js.map +1 -0
- package/dist/core/gates.d.ts +33 -0
- package/dist/core/gates.js +89 -0
- package/dist/core/gates.js.map +1 -0
- package/dist/core/handles.d.ts +90 -0
- package/dist/core/handles.js +224 -0
- package/dist/core/handles.js.map +1 -0
- package/dist/core/id.d.ts +19 -0
- package/dist/core/id.js +64 -0
- package/dist/core/id.js.map +1 -0
- package/dist/core/namespace.d.ts +38 -0
- package/dist/core/namespace.js +141 -0
- package/dist/core/namespace.js.map +1 -0
- package/dist/core/object-host.d.ts +91 -0
- package/dist/core/object-host.js +616 -0
- package/dist/core/object-host.js.map +1 -0
- package/dist/core/request-body.d.ts +9 -0
- package/dist/core/request-body.js +55 -0
- package/dist/core/request-body.js.map +1 -0
- package/dist/core/runtime.d.ts +121 -0
- package/dist/core/runtime.js +366 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/core/shard.d.ts +7 -0
- package/dist/core/shard.js +10 -0
- package/dist/core/shard.js.map +1 -0
- package/dist/core/sql-split.d.ts +15 -0
- package/dist/core/sql-split.js +101 -0
- package/dist/core/sql-split.js.map +1 -0
- package/dist/core/sql-storage.d.ts +53 -0
- package/dist/core/sql-storage.js +407 -0
- package/dist/core/sql-storage.js.map +1 -0
- package/dist/core/state.d.ts +44 -0
- package/dist/core/state.js +93 -0
- package/dist/core/state.js.map +1 -0
- package/dist/core/storage.d.ts +103 -0
- package/dist/core/storage.js +351 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/core/worker-rejections.d.ts +21 -0
- package/dist/core/worker-rejections.js +55 -0
- package/dist/core/worker-rejections.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/isolate/browser.d.ts +16 -0
- package/dist/isolate/browser.js +37 -0
- package/dist/isolate/browser.js.map +1 -0
- package/dist/isolate/host.d.ts +24 -0
- package/dist/isolate/host.js +74 -0
- package/dist/isolate/host.js.map +1 -0
- package/dist/isolate/isolate-worker.d.ts +1 -0
- package/dist/isolate/isolate-worker.js +53 -0
- package/dist/isolate/isolate-worker.js.map +1 -0
- package/dist/isolate/isolate.d.ts +10 -0
- package/dist/isolate/isolate.js +169 -0
- package/dist/isolate/isolate.js.map +1 -0
- package/dist/isolate/node-entry.d.ts +1 -0
- package/dist/isolate/node-entry.js +12 -0
- package/dist/isolate/node-entry.js.map +1 -0
- package/dist/isolate/protocol.d.ts +66 -0
- package/dist/isolate/protocol.js +205 -0
- package/dist/isolate/protocol.js.map +1 -0
- package/dist/ltx/crc64.d.ts +6 -0
- package/dist/ltx/crc64.js +46 -0
- package/dist/ltx/crc64.js.map +1 -0
- package/dist/ltx/ltx-page-server.d.ts +65 -0
- package/dist/ltx/ltx-page-server.js +309 -0
- package/dist/ltx/ltx-page-server.js.map +1 -0
- package/dist/ltx/ltx-pages.d.ts +81 -0
- package/dist/ltx/ltx-pages.js +315 -0
- package/dist/ltx/ltx-pages.js.map +1 -0
- package/dist/ltx/ltx-store-worker.d.ts +1 -0
- package/dist/ltx/ltx-store-worker.js +38 -0
- package/dist/ltx/ltx-store-worker.js.map +1 -0
- package/dist/ltx/ltx.d.ts +45 -0
- package/dist/ltx/ltx.js +203 -0
- package/dist/ltx/ltx.js.map +1 -0
- package/dist/ltx/lz4.d.ts +9 -0
- package/dist/ltx/lz4.js +156 -0
- package/dist/ltx/lz4.js.map +1 -0
- package/dist/ltx/opfs-sync-files.d.ts +23 -0
- package/dist/ltx/opfs-sync-files.js +191 -0
- package/dist/ltx/opfs-sync-files.js.map +1 -0
- package/dist/ltx/xxh32.d.ts +1 -0
- package/dist/ltx/xxh32.js +43 -0
- package/dist/ltx/xxh32.js.map +1 -0
- package/dist/miniflare-compat/blob-service.d.ts +13 -0
- package/dist/miniflare-compat/blob-service.js +66 -0
- package/dist/miniflare-compat/blob-service.js.map +1 -0
- package/dist/miniflare-compat/kv-binding.d.ts +44 -0
- package/dist/miniflare-compat/kv-binding.js +99 -0
- package/dist/miniflare-compat/kv-binding.js.map +1 -0
- package/dist/miniflare-compat/shims/assert.d.ts +15 -0
- package/dist/miniflare-compat/shims/assert.js +24 -0
- package/dist/miniflare-compat/shims/assert.js.map +1 -0
- package/dist/miniflare-compat/shims/workerd-cloudflare.d.ts +7 -0
- package/dist/miniflare-compat/shims/workerd-cloudflare.js +6 -0
- package/dist/miniflare-compat/shims/workerd-cloudflare.js.map +1 -0
- package/dist/miniflare-compat/shims/workerd-globals.d.ts +1 -0
- package/dist/miniflare-compat/shims/workerd-globals.js +30 -0
- package/dist/miniflare-compat/shims/workerd-globals.js.map +1 -0
- package/dist/miniflare-compat/shims/workerd-internal.d.ts +20 -0
- package/dist/miniflare-compat/shims/workerd-internal.js +23 -0
- package/dist/miniflare-compat/shims/workerd-internal.js.map +1 -0
- package/dist/miniflare-compat/shims/workerd-performance.d.ts +4 -0
- package/dist/miniflare-compat/shims/workerd-performance.js +17 -0
- package/dist/miniflare-compat/shims/workerd-performance.js.map +1 -0
- package/dist/miniflare-compat/simulator-fetcher.d.ts +9 -0
- package/dist/miniflare-compat/simulator-fetcher.js +14 -0
- package/dist/miniflare-compat/simulator-fetcher.js.map +1 -0
- package/dist/miniflare-compat/vendored.d.ts +15 -0
- package/dist/miniflare-compat/vendored.js +27 -0
- package/dist/miniflare-compat/vendored.js.map +1 -0
- package/dist/node-compat/async_hooks.d.ts +22 -0
- package/dist/node-compat/async_hooks.js +176 -0
- package/dist/node-compat/async_hooks.js.map +1 -0
- package/dist/node-compat/browser-registry.d.ts +3 -0
- package/dist/node-compat/browser-registry.js +39 -0
- package/dist/node-compat/browser-registry.js.map +1 -0
- package/dist/node-compat/crypto.d.ts +60 -0
- package/dist/node-compat/crypto.js +143 -0
- package/dist/node-compat/crypto.js.map +1 -0
- package/dist/node-compat/diagnostics_channel.d.ts +27 -0
- package/dist/node-compat/diagnostics_channel.js +46 -0
- package/dist/node-compat/diagnostics_channel.js.map +1 -0
- package/dist/node-compat/fs.d.ts +35 -0
- package/dist/node-compat/fs.js +40 -0
- package/dist/node-compat/fs.js.map +1 -0
- package/dist/node-compat/globals.d.ts +3 -0
- package/dist/node-compat/globals.js +4 -0
- package/dist/node-compat/globals.js.map +1 -0
- package/dist/node-compat/install.d.ts +5 -0
- package/dist/node-compat/install.js +12 -0
- package/dist/node-compat/install.js.map +1 -0
- package/dist/node-compat/module.d.ts +12 -0
- package/dist/node-compat/module.js +36 -0
- package/dist/node-compat/module.js.map +1 -0
- package/dist/node-compat/modules.d.ts +8 -0
- package/dist/node-compat/modules.js +23 -0
- package/dist/node-compat/modules.js.map +1 -0
- package/dist/node-compat/os.d.ts +36 -0
- package/dist/node-compat/os.js +36 -0
- package/dist/node-compat/os.js.map +1 -0
- package/dist/node-compat/process.d.ts +14 -0
- package/dist/node-compat/process.js +16 -0
- package/dist/node-compat/process.js.map +1 -0
- package/dist/node-compat/shims.d.ts +4 -0
- package/dist/node.d.ts +5 -0
- package/dist/node.js +6 -0
- package/dist/node.js.map +1 -0
- package/dist/peer/celld-peer.d.ts +53 -0
- package/dist/peer/celld-peer.js +555 -0
- package/dist/peer/celld-peer.js.map +1 -0
- package/dist/ports.d.ts +183 -0
- package/dist/ports.js +10 -0
- package/dist/ports.js.map +1 -0
- package/dist/project/browser-bundle.d.ts +17 -0
- package/dist/project/browser-bundle.js +125 -0
- package/dist/project/browser-bundle.js.map +1 -0
- package/dist/project/browser-cookies.d.ts +12 -0
- package/dist/project/browser-cookies.js +90 -0
- package/dist/project/browser-cookies.js.map +1 -0
- package/dist/project/browser-host.d.ts +173 -0
- package/dist/project/browser-host.js +200 -0
- package/dist/project/browser-host.js.map +1 -0
- package/dist/project/browser-modules.d.ts +17 -0
- package/dist/project/browser-modules.js +89 -0
- package/dist/project/browser-modules.js.map +1 -0
- package/dist/project/browser-shards.d.ts +62 -0
- package/dist/project/browser-shards.js +210 -0
- package/dist/project/browser-shards.js.map +1 -0
- package/dist/project/browser-tabs.d.ts +103 -0
- package/dist/project/browser-tabs.js +418 -0
- package/dist/project/browser-tabs.js.map +1 -0
- package/dist/project/cloudflare-email.d.ts +8 -0
- package/dist/project/cloudflare-email.js +11 -0
- package/dist/project/cloudflare-email.js.map +1 -0
- package/dist/project/cloudflare-workers.d.ts +12 -0
- package/dist/project/cloudflare-workers.js +16 -0
- package/dist/project/cloudflare-workers.js.map +1 -0
- package/dist/project/host-modules.d.ts +4 -0
- package/dist/project/host-modules.js +40 -0
- package/dist/project/host-modules.js.map +1 -0
- package/dist/project/jsonc.d.ts +2 -0
- package/dist/project/jsonc.js +27 -0
- package/dist/project/jsonc.js.map +1 -0
- package/dist/project/lower-await.d.ts +2 -0
- package/dist/project/lower-await.js +151 -0
- package/dist/project/lower-await.js.map +1 -0
- package/dist/project/node-project.d.ts +38 -0
- package/dist/project/node-project.js +373 -0
- package/dist/project/node-project.js.map +1 -0
- package/dist/project/own-module.d.ts +6 -0
- package/dist/project/own-module.js +13 -0
- package/dist/project/own-module.js.map +1 -0
- package/dist/project/scheduler.d.ts +2 -0
- package/dist/project/scheduler.js +3 -0
- package/dist/project/scheduler.js.map +1 -0
- package/dist/project/site.d.ts +74 -0
- package/dist/project/site.js +206 -0
- package/dist/project/site.js.map +1 -0
- package/dist/project/virtual-modules.d.ts +3 -0
- package/dist/project/worker-scope.d.ts +27 -0
- package/dist/project/worker-scope.js +46 -0
- package/dist/project/worker-scope.js.map +1 -0
- package/dist/project/workerd-message-channel.d.ts +13 -0
- package/dist/project/workerd-message-channel.js +137 -0
- package/dist/project/workerd-message-channel.js.map +1 -0
- package/dist/project/workers-global-names.d.ts +6 -0
- package/dist/project/workers-global-names.js +15 -0
- package/dist/project/workers-global-names.js.map +1 -0
- package/dist/project/workers-globals-module.d.ts +8 -0
- package/dist/project/workers-globals-module.js +9 -0
- package/dist/project/workers-globals-module.js.map +1 -0
- package/dist/project/workers-globals.d.ts +9 -0
- package/dist/project/workers-globals.js +19 -0
- package/dist/project/workers-globals.js.map +1 -0
- package/dist/project/wrangler.d.ts +116 -0
- package/dist/project/wrangler.js +342 -0
- package/dist/project/wrangler.js.map +1 -0
- package/dist/router/cell-router.d.ts +72 -0
- package/dist/router/cell-router.js +247 -0
- package/dist/router/cell-router.js.map +1 -0
- package/dist/router/shard-worker.d.ts +44 -0
- package/dist/router/shard-worker.js +163 -0
- package/dist/router/shard-worker.js.map +1 -0
- package/dist/transport/cells-http.d.ts +25 -0
- package/dist/transport/cells-http.js +141 -0
- package/dist/transport/cells-http.js.map +1 -0
- package/dist/transport/cells-link.d.ts +27 -0
- package/dist/transport/cells-link.js +71 -0
- package/dist/transport/cells-link.js.map +1 -0
- package/dist/transport/port.d.ts +26 -0
- package/dist/transport/port.js +100 -0
- package/dist/transport/port.js.map +1 -0
- package/dist/v8/codec.d.ts +6 -0
- package/dist/v8/codec.js +9 -0
- package/dist/v8/codec.js.map +1 -0
- package/dist/v8/serialize.d.ts +10 -0
- package/dist/v8/serialize.js +601 -0
- package/dist/v8/serialize.js.map +1 -0
- package/dist/ws/client.d.ts +25 -0
- package/dist/ws/client.js +203 -0
- package/dist/ws/client.js.map +1 -0
- package/dist/ws/control.d.ts +48 -0
- package/dist/ws/control.js +44 -0
- package/dist/ws/control.js.map +1 -0
- package/dist/ws/frames.d.ts +38 -0
- package/dist/ws/frames.js +39 -0
- package/dist/ws/frames.js.map +1 -0
- package/dist/ws/server.d.ts +99 -0
- package/dist/ws/server.js +295 -0
- package/dist/ws/server.js.map +1 -0
- package/dist/ws/sw-connector.d.ts +10 -0
- package/dist/ws/sw-connector.js +25 -0
- package/dist/ws/sw-connector.js.map +1 -0
- package/dist/ws/upgrade.d.ts +28 -0
- package/dist/ws/upgrade.js +75 -0
- package/dist/ws/upgrade.js.map +1 -0
- package/package.json +111 -3
- package/vendor/celld/LICENSE +202 -0
- package/vendor/celld/kv.js +1131 -0
- package/vendor/celld/queues.js +1656 -0
- package/vendor/celld/workflows.js +1610 -0
- package/vendor/html-rewriter-wasm/html_rewriter.js +925 -0
- package/vendor/html-rewriter-wasm/html_rewriter_bg.wasm +0 -0
- package/vendor/miniflare/d1-api.js +738 -0
- package/vendor/miniflare/d1-database.js +6978 -0
- package/vendor/miniflare/kv-namespace.js +3061 -0
|
@@ -0,0 +1,1610 @@
|
|
|
1
|
+
// GENERATED by scripts/vendor-celld.mjs from denoland/celld@8702f6d186c77698dbfb30cf29e3b1922a88f008 crates/celld/js/harness.js
|
|
2
|
+
// (Apache-2.0): the Workflows section, unchanged, wrapped as workflowService(host). Do not edit.
|
|
3
|
+
export function workflowService(host) {
|
|
4
|
+
const { __cell, __cf, __sc_encode, __als_get, __als_set, __alarm_set, __alarm_delete,
|
|
5
|
+
__registerWaitUntil, __ctxRun } = host;
|
|
6
|
+
const __isolate_condemn = host.condemn ?? (() => {});
|
|
7
|
+
const classes = {};
|
|
8
|
+
const __runtimeClass = (name, cls) => { classes[name] = cls; };
|
|
9
|
+
const __celld = {};
|
|
10
|
+
// ---- Workflows ----------------------------------------------------------
|
|
11
|
+
// A workflow instance is one cell of the runtime-supplied `__Workflow` class,
|
|
12
|
+
// named `<workflow_name>/<instance_id>` through getByName, so it inherits
|
|
13
|
+
// ownership, fencing, LTX replication and the output gate from the cell it
|
|
14
|
+
// already is. Execution is replay: `run()` re-executes from the top on every
|
|
15
|
+
// resume and only steps are memoized, in `ctx.storage` KV under `__wf.*`.
|
|
16
|
+
// This half is the server; `__makeWorkflow` below is the client the binding
|
|
17
|
+
// hands to a Worker. The public contract is in docs/cloudflare-compat.md.
|
|
18
|
+
|
|
19
|
+
// Upstream's limit for a step return, an event payload, and the params. The
|
|
20
|
+
// error is distinct and names the step, because "too large" and "the isolate
|
|
21
|
+
// died" must not read the same (the D1 row-cap precedent).
|
|
22
|
+
const __WF_VALUE_CAP = 1 << 20;
|
|
23
|
+
// Workflow names, instance ids, and event types all share this charset.
|
|
24
|
+
// `/` is outside it, which is what makes the `<name>/<id>` cell-name join
|
|
25
|
+
// unambiguous.
|
|
26
|
+
const __WF_NAME_RE = /^[a-zA-Z0-9_][a-zA-Z0-9-_]*$/;
|
|
27
|
+
const __wfError = (message) => new Error("WORKFLOW_ERROR: " + message);
|
|
28
|
+
// The suspension primitive: a blocked step returns a promise that never
|
|
29
|
+
// settles in this invocation, so user code simply stops progressing. A
|
|
30
|
+
// sentinel thrown through user code was rejected because `Promise.all` in
|
|
31
|
+
// `run()` would observe it as a rejection and user try/catch would eat it.
|
|
32
|
+
const __wfNever = new Promise(() => {});
|
|
33
|
+
const __wfTerminal = (status) =>
|
|
34
|
+
status === "complete" || status === "errored" || status === "terminated";
|
|
35
|
+
// Upstream's hard bounds: 10,000 retries per step, 365 days for any sleep or
|
|
36
|
+
// event wait, one second under a waitForEvent timeout. Enforced loudly at the
|
|
37
|
+
// call site -- a config outside them would otherwise misbehave only after
|
|
38
|
+
// the instance is minutes or days into its run.
|
|
39
|
+
const __WF_RETRY_LIMIT_CAP = 10000;
|
|
40
|
+
const __WF_STEP_NAME_LIMIT = 256;
|
|
41
|
+
const __WF_MAX_WAIT_MS = 365 * 86400000;
|
|
42
|
+
const __WF_MIN_EVENT_TIMEOUT_MS = 1000;
|
|
43
|
+
// celld has no account plans, so it exposes the larger published retention
|
|
44
|
+
// ceiling as one stable application limit. Both an omitted policy and an
|
|
45
|
+
// omitted member of a partial policy use this value.
|
|
46
|
+
const __WF_RETENTION_MAX_MS = 30 * 86400000;
|
|
47
|
+
const __WF_LOCATION_HINTS = new Set([
|
|
48
|
+
"wnam", "enam", "sam", "weur", "eeur", "apac", "apac-ne", "apac-se",
|
|
49
|
+
"oc", "afr", "me",
|
|
50
|
+
]);
|
|
51
|
+
// How long run() can stay pending on non-step work while no step runs and
|
|
52
|
+
// none is blocked before the instance fails. Upstream permits un-stepped
|
|
53
|
+
// awaits between steps (they are merely non-durable), so erroring at the
|
|
54
|
+
// first quiescent macrotask would kill any run() whose external await
|
|
55
|
+
// outlives one turn of the loop. Sixty seconds is long enough for any
|
|
56
|
+
// sane un-stepped await and short enough that a truly never-settling
|
|
57
|
+
// promise fails the instance instead of wedging the alarm handler.
|
|
58
|
+
const __WF_STALL_GRACE_MS = 60000;
|
|
59
|
+
const __WF_UNITS = {
|
|
60
|
+
second: 1000,
|
|
61
|
+
minute: 60000,
|
|
62
|
+
hour: 3600000,
|
|
63
|
+
day: 86400000,
|
|
64
|
+
week: 604800000,
|
|
65
|
+
// Upstream does not document its month/year arithmetic; fixed 30/365-day
|
|
66
|
+
// units are used so one expression cannot mean two durations depending on
|
|
67
|
+
// when it runs.
|
|
68
|
+
month: 30 * 86400000,
|
|
69
|
+
year: 365 * 86400000,
|
|
70
|
+
};
|
|
71
|
+
const __wfDuration = (value, what) => {
|
|
72
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
|
|
73
|
+
if (typeof value !== "string") {
|
|
74
|
+
throw __wfError(
|
|
75
|
+
`invalid duration for ${what}: ${JSON.stringify(value)}; use milliseconds ` +
|
|
76
|
+
'or "<n> <second|minute|hour|day|week|month|year>[s]"',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const match = /^\s*(\d+(?:\.\d+)?)\s+(second|minute|hour|day|week|month|year)s?\s*$/
|
|
80
|
+
.exec(value);
|
|
81
|
+
if (!match) {
|
|
82
|
+
throw __wfError(
|
|
83
|
+
`invalid duration for ${what}: ${JSON.stringify(value)}; use milliseconds ` +
|
|
84
|
+
'or "<n> <second|minute|hour|day|week|month|year>[s]"',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return Number(match[1]) * __WF_UNITS[match[2]];
|
|
88
|
+
};
|
|
89
|
+
const __wfRetentionDuration = (value, what) => {
|
|
90
|
+
const durationMs = __wfDuration(value, what);
|
|
91
|
+
if (durationMs > __WF_RETENTION_MAX_MS) {
|
|
92
|
+
throw __wfError(
|
|
93
|
+
`${what} is ${durationMs} ms, above celld's 30-day retention limit`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return durationMs;
|
|
97
|
+
};
|
|
98
|
+
const __wfOwnObject = (value) =>
|
|
99
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
100
|
+
const __wfUnknownKey = (value, allowed) =>
|
|
101
|
+
Object.keys(value).find((key) => !allowed.includes(key));
|
|
102
|
+
const __wfStepName = (name) => {
|
|
103
|
+
// The pinned workers-sdk validator has no minimum length. An empty name is
|
|
104
|
+
// therefore valid, although rejecting it looks safer at first glance. A
|
|
105
|
+
// non-empty check made celld reject Worker code that the upstream runtime
|
|
106
|
+
// accepts, so keep only the upstream length and control-character bounds.
|
|
107
|
+
if (
|
|
108
|
+
typeof name !== "string" || name.length > __WF_STEP_NAME_LIMIT ||
|
|
109
|
+
/[\x00-\x1f]/.test(name)
|
|
110
|
+
) {
|
|
111
|
+
throw __wfError(
|
|
112
|
+
`invalid step name ${JSON.stringify(name)}: use at most ` +
|
|
113
|
+
`${__WF_STEP_NAME_LIMIT} characters and no control characters`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const __wfEventType = (type) => {
|
|
118
|
+
if (typeof type !== "string" || type.length > 100 || !__WF_NAME_RE.test(type)) {
|
|
119
|
+
throw __wfError(
|
|
120
|
+
`invalid event type ${JSON.stringify(type)}: use at most 100 letters, ` +
|
|
121
|
+
"digits, hyphens, or underscores",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const __wfSleepDuration = (duration, name) => {
|
|
126
|
+
const durationMs = __wfDuration(
|
|
127
|
+
duration,
|
|
128
|
+
`the duration of sleep ${JSON.stringify(name)}`,
|
|
129
|
+
);
|
|
130
|
+
if (durationMs > __WF_MAX_WAIT_MS) {
|
|
131
|
+
throw __wfError(
|
|
132
|
+
`sleep ${JSON.stringify(name)} is ${durationMs} ms, above the upstream ` +
|
|
133
|
+
"limit of 365 days",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return durationMs;
|
|
137
|
+
};
|
|
138
|
+
const __wfSleepUntil = (timestamp, name) => {
|
|
139
|
+
const at = timestamp instanceof Date ? timestamp.getTime() : timestamp;
|
|
140
|
+
if (typeof at !== "number" || !Number.isFinite(at)) {
|
|
141
|
+
throw __wfError(
|
|
142
|
+
`invalid sleepUntil timestamp for ${JSON.stringify(name)}: use a Date ` +
|
|
143
|
+
"or a UNIX timestamp in milliseconds",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const delay = at - Date.now();
|
|
147
|
+
if (delay > __WF_MAX_WAIT_MS) {
|
|
148
|
+
throw __wfError(
|
|
149
|
+
`sleepUntil ${JSON.stringify(name)} is ${delay} ms away, above the ` +
|
|
150
|
+
"upstream limit of 365 days",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return at;
|
|
154
|
+
};
|
|
155
|
+
const __wfWaitOptions = (options, name) => {
|
|
156
|
+
if (!__wfOwnObject(options)) {
|
|
157
|
+
throw __wfError(`invalid waitForEvent options for ${JSON.stringify(name)}`);
|
|
158
|
+
}
|
|
159
|
+
__wfEventType(options.type);
|
|
160
|
+
const timeout = options.timeout ?? "24 hours";
|
|
161
|
+
const timeoutMs = __wfDuration(
|
|
162
|
+
timeout,
|
|
163
|
+
`the timeout of waitForEvent ${JSON.stringify(name)}`,
|
|
164
|
+
);
|
|
165
|
+
if (timeoutMs < __WF_MIN_EVENT_TIMEOUT_MS || timeoutMs > __WF_MAX_WAIT_MS) {
|
|
166
|
+
throw __wfError(
|
|
167
|
+
`the timeout of waitForEvent ${JSON.stringify(name)} is ${timeoutMs} ms; ` +
|
|
168
|
+
"upstream allows 1 second to 365 days",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return { type: options.type, timeout, timeoutMs };
|
|
172
|
+
};
|
|
173
|
+
const __wfEventOptions = (options) => {
|
|
174
|
+
if (!__wfOwnObject(options)) {
|
|
175
|
+
throw __wfError("sendEvent() needs an event object");
|
|
176
|
+
}
|
|
177
|
+
__wfEventType(options.type);
|
|
178
|
+
if (options.payload !== undefined) __wfCheckValue(options.payload, "the event payload");
|
|
179
|
+
return { type: options.type, payload: options.payload };
|
|
180
|
+
};
|
|
181
|
+
const __wfRetentionOptions = (retention, what) => {
|
|
182
|
+
if (retention === undefined) {
|
|
183
|
+
return {
|
|
184
|
+
successMs: __WF_RETENTION_MAX_MS,
|
|
185
|
+
errorMs: __WF_RETENTION_MAX_MS,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (!__wfOwnObject(retention)) {
|
|
189
|
+
throw __wfError(`${what} option retention must be an object`);
|
|
190
|
+
}
|
|
191
|
+
const unknown = __wfUnknownKey(retention, ["successRetention", "errorRetention"]);
|
|
192
|
+
if (unknown !== undefined) {
|
|
193
|
+
throw __wfError(
|
|
194
|
+
`${what} option retention has an unknown field ${JSON.stringify(unknown)}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
successMs: retention.successRetention === undefined
|
|
199
|
+
? __WF_RETENTION_MAX_MS
|
|
200
|
+
: __wfRetentionDuration(
|
|
201
|
+
retention.successRetention,
|
|
202
|
+
`${what} successRetention`,
|
|
203
|
+
),
|
|
204
|
+
errorMs: retention.errorRetention === undefined
|
|
205
|
+
? __WF_RETENTION_MAX_MS
|
|
206
|
+
: __wfRetentionDuration(retention.errorRetention, `${what} errorRetention`),
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
const __wfLocationHint = (hint, what) => {
|
|
210
|
+
if (hint !== undefined && !__WF_LOCATION_HINTS.has(hint)) {
|
|
211
|
+
throw __wfError(
|
|
212
|
+
`${what} has invalid locationHint ${JSON.stringify(hint)}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return hint;
|
|
216
|
+
};
|
|
217
|
+
const __wfRestartOptions = (options) => {
|
|
218
|
+
if (options === undefined) return {};
|
|
219
|
+
if (!__wfOwnObject(options)) {
|
|
220
|
+
throw __wfError("restart() options must be an object");
|
|
221
|
+
}
|
|
222
|
+
const unknown = __wfUnknownKey(options, ["from"]);
|
|
223
|
+
if (unknown !== undefined) {
|
|
224
|
+
throw __wfError(`restart() has an unknown option ${JSON.stringify(unknown)}`);
|
|
225
|
+
}
|
|
226
|
+
if (options.from === undefined) return {};
|
|
227
|
+
const from = options.from;
|
|
228
|
+
if (!__wfOwnObject(from)) {
|
|
229
|
+
throw __wfError("restart() option from must be an object");
|
|
230
|
+
}
|
|
231
|
+
const fromUnknown = __wfUnknownKey(from, ["name", "count", "type"]);
|
|
232
|
+
if (fromUnknown !== undefined) {
|
|
233
|
+
throw __wfError(
|
|
234
|
+
`restart() option from has an unknown field ${JSON.stringify(fromUnknown)}`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
__wfStepName(from.name);
|
|
238
|
+
const count = from.count ?? 1;
|
|
239
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
240
|
+
throw __wfError("restart() option from.count must be a positive integer");
|
|
241
|
+
}
|
|
242
|
+
const type = from.type ?? "do";
|
|
243
|
+
if (!["do", "sleep", "waitForEvent"].includes(type)) {
|
|
244
|
+
throw __wfError(
|
|
245
|
+
'restart() option from.type must be "do", "sleep", or "waitForEvent"',
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return { from: { name: from.name, count, type } };
|
|
249
|
+
};
|
|
250
|
+
const __wfStepConfig = (config, name) => {
|
|
251
|
+
if (!__wfOwnObject(config)) {
|
|
252
|
+
throw __wfError(`invalid config for step ${JSON.stringify(name)}: use an object`);
|
|
253
|
+
}
|
|
254
|
+
let unknown = __wfUnknownKey(config, ["retries", "timeout", "sensitive"]);
|
|
255
|
+
if (unknown !== undefined) {
|
|
256
|
+
throw __wfError(
|
|
257
|
+
`invalid config for step ${JSON.stringify(name)}: unknown option ` +
|
|
258
|
+
JSON.stringify(unknown),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
const retries = config.retries;
|
|
262
|
+
if (retries !== undefined) {
|
|
263
|
+
if (!__wfOwnObject(retries)) {
|
|
264
|
+
throw __wfError(`invalid retries config for step ${JSON.stringify(name)}`);
|
|
265
|
+
}
|
|
266
|
+
unknown = __wfUnknownKey(retries, ["limit", "delay", "backoff"]);
|
|
267
|
+
if (unknown !== undefined) {
|
|
268
|
+
throw __wfError(
|
|
269
|
+
`invalid retries config for step ${JSON.stringify(name)}: unknown option ` +
|
|
270
|
+
JSON.stringify(unknown),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (
|
|
274
|
+
typeof retries.limit !== "number" || !Number.isFinite(retries.limit) ||
|
|
275
|
+
retries.limit < 0
|
|
276
|
+
) {
|
|
277
|
+
throw __wfError(
|
|
278
|
+
`invalid retries config for step ${JSON.stringify(name)}: ` +
|
|
279
|
+
"limit must be a non-negative number",
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (retries.limit > __WF_RETRY_LIMIT_CAP) {
|
|
283
|
+
throw __wfError(
|
|
284
|
+
`invalid retries config for step ${JSON.stringify(name)}: limit is above ` +
|
|
285
|
+
String(__WF_RETRY_LIMIT_CAP),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if (typeof retries.delay !== "function") {
|
|
289
|
+
__wfDuration(retries.delay, `the retries config delay of step ${JSON.stringify(name)}`);
|
|
290
|
+
}
|
|
291
|
+
if (
|
|
292
|
+
retries.backoff !== undefined &&
|
|
293
|
+
!["constant", "linear", "exponential"].includes(retries.backoff)
|
|
294
|
+
) {
|
|
295
|
+
throw __wfError(
|
|
296
|
+
`invalid retries config for step ${JSON.stringify(name)}: ` +
|
|
297
|
+
"backoff must be constant, linear, or exponential",
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (config.timeout !== undefined) {
|
|
302
|
+
const timeout = __wfDuration(
|
|
303
|
+
config.timeout,
|
|
304
|
+
`the config timeout of step ${JSON.stringify(name)}`,
|
|
305
|
+
);
|
|
306
|
+
if (timeout === 0) {
|
|
307
|
+
throw __wfError(
|
|
308
|
+
`invalid config timeout for step ${JSON.stringify(name)}: use a value above zero`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (config.sensitive !== undefined && config.sensitive !== "output") {
|
|
313
|
+
throw __wfError(
|
|
314
|
+
`invalid config sensitive value for step ${JSON.stringify(name)}: use "output"`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
...config,
|
|
319
|
+
...(retries === undefined ? {} : { retries: { ...retries } }),
|
|
320
|
+
};
|
|
321
|
+
};
|
|
322
|
+
// Serialization is checked with the same encoder storage and RPC use, so a
|
|
323
|
+
// value that passes here cannot fail later at the isolate boundary. The
|
|
324
|
+
// thrown errors are permanent: retrying cannot make a value cloneable or
|
|
325
|
+
// shrink it.
|
|
326
|
+
const __wfEncodeValue = (value, what) => {
|
|
327
|
+
try {
|
|
328
|
+
return __sc_encode(value);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
const failure = __wfError(
|
|
331
|
+
`${what} is not serializable: ${String(error && error.message || error)}. ` +
|
|
332
|
+
"It must survive structured clone; a ReadableStream return is not implemented in celld.",
|
|
333
|
+
);
|
|
334
|
+
failure.__wfPermanent = true;
|
|
335
|
+
throw failure;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const __wfCheckEncodedValue = (bytes, what) => {
|
|
339
|
+
if (bytes.byteLength > __WF_VALUE_CAP) {
|
|
340
|
+
const failure = __wfError(
|
|
341
|
+
`${what} is ${bytes.byteLength} bytes, above the 1 MiB limit; ` +
|
|
342
|
+
"store large output externally and return a reference",
|
|
343
|
+
);
|
|
344
|
+
failure.__wfPermanent = true;
|
|
345
|
+
throw failure;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
const __wfCheckValue = (value, what) =>
|
|
349
|
+
__wfCheckEncodedValue(__wfEncodeValue(value, what), what);
|
|
350
|
+
const __wfErrorRecord = (error) => ({
|
|
351
|
+
name: String(error && error.name || "Error"),
|
|
352
|
+
message: String(error && error.message || error),
|
|
353
|
+
});
|
|
354
|
+
// A permanently failed step rejects on every replay, so user try/catch
|
|
355
|
+
// around step.do keeps working after a resume, not only on the attempt that
|
|
356
|
+
// failed live.
|
|
357
|
+
const __wfReviveError = (record) => {
|
|
358
|
+
const error = new Error(record.message);
|
|
359
|
+
error.name = record.name;
|
|
360
|
+
return error;
|
|
361
|
+
};
|
|
362
|
+
const __wfLedgerPrefix = (generation) => `__wf.${generation}.`;
|
|
363
|
+
const __wfEventPrefix = (generation) =>
|
|
364
|
+
__wfLedgerPrefix(generation) + "event.";
|
|
365
|
+
const __wfEventKey = (generation, sequence) =>
|
|
366
|
+
__wfEventPrefix(generation) + String(sequence).padStart(9, "0");
|
|
367
|
+
const __wfRetentionMs = (meta, status) =>
|
|
368
|
+
status === "complete"
|
|
369
|
+
? (meta.retention?.successMs ?? __WF_RETENTION_MAX_MS)
|
|
370
|
+
: (meta.retention?.errorMs ?? __WF_RETENTION_MAX_MS);
|
|
371
|
+
const __wfFinishRetention = (meta, transaction) => {
|
|
372
|
+
meta.endedMs = Date.now();
|
|
373
|
+
meta.expiresMs = meta.endedMs + __wfRetentionMs(meta, meta.status);
|
|
374
|
+
transaction.setAlarm(meta.expiresMs);
|
|
375
|
+
};
|
|
376
|
+
// A ledger record replays only through the kind of call that wrote it. A
|
|
377
|
+
// `do` renamed into a `sleep`, or two calls reordered under one name, would
|
|
378
|
+
// otherwise read fields the other kind never wrote and continue on
|
|
379
|
+
// undefined -- a deadline of NaN, a value of nothing -- instead of failing.
|
|
380
|
+
const __wfKindCheck = (record, kind, name) => {
|
|
381
|
+
if (record === undefined || record.kind === kind) return record;
|
|
382
|
+
const failure = __wfError(
|
|
383
|
+
`step ${JSON.stringify(name)} replays as ${kind} but its ledger record ` +
|
|
384
|
+
`was written by ${record.kind}; a step keeps one kind across replays`,
|
|
385
|
+
);
|
|
386
|
+
failure.__wfPermanent = true;
|
|
387
|
+
throw failure;
|
|
388
|
+
};
|
|
389
|
+
// Each step callback runs under an async-context frame naming its step (the
|
|
390
|
+
// same CPED-backed frame __ctxRun uses, so it survives the callback's
|
|
391
|
+
// awaits). A step started inside another step's callback would wedge
|
|
392
|
+
// quiescence: the outer callback cannot settle while it awaits the blocked
|
|
393
|
+
// inner step, so `running` never reaches zero and the instance can neither
|
|
394
|
+
// suspend nor finish. The frame is what lets the inner call refuse loudly
|
|
395
|
+
// at the nesting site instead.
|
|
396
|
+
const __wfStepKey = Symbol("celld.wfStep");
|
|
397
|
+
const __wfEnclosingStep = () => {
|
|
398
|
+
const frame = __als_get();
|
|
399
|
+
return frame === undefined ? undefined : frame.get(__wfStepKey);
|
|
400
|
+
};
|
|
401
|
+
const __wfEnterStep = (name, fn) => {
|
|
402
|
+
const prior = __als_get();
|
|
403
|
+
const frame = new Map(prior);
|
|
404
|
+
frame.set(__wfStepKey, name);
|
|
405
|
+
__als_set(frame);
|
|
406
|
+
try {
|
|
407
|
+
return fn();
|
|
408
|
+
} finally {
|
|
409
|
+
__als_set(prior);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// Race an attempt against its deadline. The abandoned callback keeps
|
|
414
|
+
// running -- there is no cancellation to send it -- but its result is
|
|
415
|
+
// discarded and the timeout counts as a failed attempt, as upstream.
|
|
416
|
+
const __wfTimeout = (promise, ms, name, attempt) =>
|
|
417
|
+
new Promise((resolve, reject) => {
|
|
418
|
+
const timer = setTimeout(
|
|
419
|
+
() =>
|
|
420
|
+
reject(new Error(
|
|
421
|
+
`step ${JSON.stringify(name)} timed out after ${ms} ms (attempt ${attempt})`,
|
|
422
|
+
)),
|
|
423
|
+
ms,
|
|
424
|
+
);
|
|
425
|
+
promise.then(
|
|
426
|
+
(value) => { clearTimeout(timer); resolve(value); },
|
|
427
|
+
(error) => { clearTimeout(timer); reject(error); },
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Storage failures belong to the engine even when workflow code catches the
|
|
432
|
+
// rejected step promise. Record them outside the Error object, so user code
|
|
433
|
+
// cannot suppress a retry or forge one by returning a marked exception.
|
|
434
|
+
const __wfTrackStorageFailures = (storage, driver) => {
|
|
435
|
+
const tracked = Object.create(storage);
|
|
436
|
+
for (const name of ["get", "put", "transactionSync"]) {
|
|
437
|
+
Object.defineProperty(tracked, name, {
|
|
438
|
+
value(...args) {
|
|
439
|
+
try {
|
|
440
|
+
const result = storage[name](...args);
|
|
441
|
+
if (result !== null && typeof result === "object" &&
|
|
442
|
+
typeof result.then === "function") {
|
|
443
|
+
return result.catch((error) => {
|
|
444
|
+
driver.storageFailure ??= error;
|
|
445
|
+
throw error;
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return result;
|
|
449
|
+
} catch (error) {
|
|
450
|
+
driver.storageFailure ??= error;
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return tracked;
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// The WorkflowStep the driver hands to run(). Each method resolves its step
|
|
460
|
+
// record by (name, occurrence count) -- the count is assigned synchronously
|
|
461
|
+
// at call time, so `Promise.all` of two same-named steps gets deterministic
|
|
462
|
+
// distinct keys in call order, and a repeated name in a loop cannot collide.
|
|
463
|
+
const __wfMakeStep = (driver) => {
|
|
464
|
+
const storage = driver.storage;
|
|
465
|
+
// The shared step frame. `body` resolves to {value} or {block: at, kind};
|
|
466
|
+
// a blocked step leaves its promise unsettled forever in this invocation.
|
|
467
|
+
// The running counter covers the whole body, ledger reads included, so
|
|
468
|
+
// quiescence cannot fire between a step's read and its decision.
|
|
469
|
+
const frame = (name, body) => {
|
|
470
|
+
// A stale replay -- one whose invocation already suspended -- can wake
|
|
471
|
+
// later if user code raced a step against an un-stepped await. It goes
|
|
472
|
+
// quiet instead of executing an attempt beside the fresh replay.
|
|
473
|
+
if (driver.finished) return __wfNever;
|
|
474
|
+
const enclosing = __wfEnclosingStep();
|
|
475
|
+
if (enclosing !== undefined) {
|
|
476
|
+
const failure = __wfError(
|
|
477
|
+
`step ${JSON.stringify(name)} cannot start inside the callback of ` +
|
|
478
|
+
`step ${JSON.stringify(enclosing)}; a step callback must not use ` +
|
|
479
|
+
"the step API",
|
|
480
|
+
);
|
|
481
|
+
failure.__wfPermanent = true;
|
|
482
|
+
return Promise.reject(failure);
|
|
483
|
+
}
|
|
484
|
+
const count = (driver.counts.get(name) ?? 0) + 1;
|
|
485
|
+
driver.counts.set(name, count);
|
|
486
|
+
const ordinal = ++driver.ordinal;
|
|
487
|
+
const key = driver.ledgerPrefix + `step.${count}.${name}`;
|
|
488
|
+
// Step activity restarts the stall clock: a pending grace timer was
|
|
489
|
+
// armed against a run() that looked stuck, and this call is progress.
|
|
490
|
+
driver.clearStall();
|
|
491
|
+
driver.running += 1;
|
|
492
|
+
return new Promise((resolve, reject) => {
|
|
493
|
+
(async () => {
|
|
494
|
+
const meta = await storage.get("__wf.meta");
|
|
495
|
+
if (
|
|
496
|
+
meta === undefined || meta.generation !== driver.generation ||
|
|
497
|
+
__wfTerminal(meta.status) || meta.status === "paused" ||
|
|
498
|
+
meta.status === "waitingForPause"
|
|
499
|
+
) {
|
|
500
|
+
return { pause: true };
|
|
501
|
+
}
|
|
502
|
+
return await body(key, count, ordinal);
|
|
503
|
+
})().then(
|
|
504
|
+
(result) => {
|
|
505
|
+
driver.running -= 1;
|
|
506
|
+
driver.clearStall();
|
|
507
|
+
if (result.pause === true) {
|
|
508
|
+
driver.pauseRequested = true;
|
|
509
|
+
driver.check();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (result.block !== undefined) {
|
|
513
|
+
if (!driver.finished) {
|
|
514
|
+
driver.blocked.push({ at: result.block, kind: result.kind });
|
|
515
|
+
}
|
|
516
|
+
driver.check();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
driver.check();
|
|
520
|
+
resolve(result.value);
|
|
521
|
+
},
|
|
522
|
+
(error) => {
|
|
523
|
+
driver.running -= 1;
|
|
524
|
+
driver.clearStall();
|
|
525
|
+
driver.check();
|
|
526
|
+
reject(error);
|
|
527
|
+
},
|
|
528
|
+
);
|
|
529
|
+
});
|
|
530
|
+
};
|
|
531
|
+
const doStep = (name, configOrCallback, callbackOrRollback, maybeRollback) => {
|
|
532
|
+
const hasConfig = typeof configOrCallback !== "function";
|
|
533
|
+
let config = hasConfig ? (configOrCallback ?? {}) : {};
|
|
534
|
+
const callback = hasConfig ? callbackOrRollback : configOrCallback;
|
|
535
|
+
const rollback = hasConfig ? maybeRollback : callbackOrRollback;
|
|
536
|
+
try {
|
|
537
|
+
__wfStepName(name);
|
|
538
|
+
config = __wfStepConfig(config, name);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
return Promise.reject(error);
|
|
541
|
+
}
|
|
542
|
+
if (rollback !== undefined) {
|
|
543
|
+
return Promise.reject(__wfError(
|
|
544
|
+
"step rollbackOptions are not implemented in celld; remove the rollback handler",
|
|
545
|
+
));
|
|
546
|
+
}
|
|
547
|
+
if (config.sensitive !== undefined) {
|
|
548
|
+
return Promise.reject(__wfError(
|
|
549
|
+
"sensitive step output is not implemented in celld; remove `sensitive`",
|
|
550
|
+
));
|
|
551
|
+
}
|
|
552
|
+
if (typeof callback !== "function") {
|
|
553
|
+
return Promise.reject(__wfError("step.do needs a callback function"));
|
|
554
|
+
}
|
|
555
|
+
return frame(name, async (key, count, ordinal) => {
|
|
556
|
+
const record = __wfKindCheck(await storage.get(key), "do", name);
|
|
557
|
+
if (record !== undefined && record.status === "completed") {
|
|
558
|
+
return { value: record.value };
|
|
559
|
+
}
|
|
560
|
+
if (record !== undefined && record.status === "failed") {
|
|
561
|
+
throw __wfReviveError(record.error);
|
|
562
|
+
}
|
|
563
|
+
// A pending retry whose backoff has not elapsed blocks without
|
|
564
|
+
// executing; the deadline was persisted when the attempt failed, so a
|
|
565
|
+
// replay cannot reset the backoff.
|
|
566
|
+
if (record !== undefined && record.nextAt > Date.now()) {
|
|
567
|
+
return { block: record.nextAt, kind: "retry" };
|
|
568
|
+
}
|
|
569
|
+
const attempt = (record === undefined ? 0 : record.attempt) + 1;
|
|
570
|
+
const historyOrdinal = record?.ordinal ?? ordinal;
|
|
571
|
+
const retries = config.retries ??
|
|
572
|
+
{ limit: 5, delay: 10000, backoff: "exponential" };
|
|
573
|
+
const timeout = config.timeout ?? "10 minutes";
|
|
574
|
+
const timeoutMs = __wfDuration(
|
|
575
|
+
timeout,
|
|
576
|
+
`the timeout of step ${JSON.stringify(name)}`,
|
|
577
|
+
);
|
|
578
|
+
// Upstream omits a dynamic delay from the step context. Copying the
|
|
579
|
+
// config verbatim would expose the callback to itself as retry policy,
|
|
580
|
+
// while callers expect only the serializable limit and backoff fields.
|
|
581
|
+
const contextRetries = typeof retries.delay === "function"
|
|
582
|
+
? { limit: retries.limit, ...(retries.backoff === undefined
|
|
583
|
+
? {}
|
|
584
|
+
: { backoff: retries.backoff }) }
|
|
585
|
+
: retries;
|
|
586
|
+
const context = {
|
|
587
|
+
step: { name, count },
|
|
588
|
+
attempt,
|
|
589
|
+
config: { retries: contextRetries, timeout },
|
|
590
|
+
};
|
|
591
|
+
try {
|
|
592
|
+
// Persist the history entry before the callback starts. A concurrent
|
|
593
|
+
// selected restart can therefore name an in-flight occurrence, and
|
|
594
|
+
// its new generation fences the callback's eventual old-ledger write.
|
|
595
|
+
await storage.put(key, {
|
|
596
|
+
kind: "do",
|
|
597
|
+
status: "running",
|
|
598
|
+
attempt: attempt - 1,
|
|
599
|
+
ordinal: historyOrdinal,
|
|
600
|
+
});
|
|
601
|
+
const value = await __wfTimeout(
|
|
602
|
+
__wfEnterStep(name, async () => callback(context)),
|
|
603
|
+
timeoutMs,
|
|
604
|
+
name,
|
|
605
|
+
attempt,
|
|
606
|
+
);
|
|
607
|
+
if (value !== undefined) {
|
|
608
|
+
__wfCheckValue(value, `the return value of step ${JSON.stringify(name)}`);
|
|
609
|
+
}
|
|
610
|
+
await storage.put(key, {
|
|
611
|
+
kind: "do",
|
|
612
|
+
status: "completed",
|
|
613
|
+
value,
|
|
614
|
+
ordinal: historyOrdinal,
|
|
615
|
+
});
|
|
616
|
+
return { value };
|
|
617
|
+
} catch (error) {
|
|
618
|
+
// NonRetryableError skips retries by contract; a value-cap or
|
|
619
|
+
// serialization failure is marked permanent above because another
|
|
620
|
+
// attempt returns the same value.
|
|
621
|
+
const permanent = error instanceof NonRetryableError ||
|
|
622
|
+
(error !== null && typeof error === "object" &&
|
|
623
|
+
(error.name === "NonRetryableError" || error.__wfPermanent === true)) ||
|
|
624
|
+
attempt > retries.limit;
|
|
625
|
+
if (permanent) {
|
|
626
|
+
await storage.put(key, {
|
|
627
|
+
kind: "do",
|
|
628
|
+
status: "failed",
|
|
629
|
+
error: __wfErrorRecord(error),
|
|
630
|
+
ordinal: historyOrdinal,
|
|
631
|
+
});
|
|
632
|
+
throw error;
|
|
633
|
+
}
|
|
634
|
+
// Upstream documents the backoff names, not the arithmetic; the
|
|
635
|
+
// simplest reading is used and recorded in the design page. When
|
|
636
|
+
// `retries` is given without `backoff`, celld uses constant -- the
|
|
637
|
+
// documented exponential default is the whole-config-omitted case.
|
|
638
|
+
const dynamic = typeof retries.delay === "function";
|
|
639
|
+
const delay = dynamic
|
|
640
|
+
? await __wfEnterStep(name, () => retries.delay({ ctx: context, error }))
|
|
641
|
+
: retries.delay;
|
|
642
|
+
const base = __wfDuration(
|
|
643
|
+
delay,
|
|
644
|
+
`the retry delay of step ${JSON.stringify(name)}`,
|
|
645
|
+
);
|
|
646
|
+
// A delay callback replaces the fixed base duration, not the backoff
|
|
647
|
+
// policy. Skipping this factor makes a dynamic exponential or linear
|
|
648
|
+
// policy retry earlier than the same policy with a fixed base.
|
|
649
|
+
const factor = retries.backoff === "exponential"
|
|
650
|
+
? 2 ** (attempt - 1)
|
|
651
|
+
: retries.backoff === "linear"
|
|
652
|
+
? attempt
|
|
653
|
+
: 1;
|
|
654
|
+
const nextAt = Date.now() + base * factor;
|
|
655
|
+
await storage.put(key, {
|
|
656
|
+
kind: "do",
|
|
657
|
+
status: "retrying",
|
|
658
|
+
attempt,
|
|
659
|
+
nextAt,
|
|
660
|
+
ordinal: historyOrdinal,
|
|
661
|
+
});
|
|
662
|
+
return { block: nextAt, kind: "retry" };
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
};
|
|
666
|
+
const sleepUntilDeadline = (name, key, ordinal, deadline) => (async () => {
|
|
667
|
+
const record = __wfKindCheck(await storage.get(key), "sleep", name);
|
|
668
|
+
if (record !== undefined && record.status === "completed") {
|
|
669
|
+
return { value: undefined };
|
|
670
|
+
}
|
|
671
|
+
const historyOrdinal = record?.ordinal ?? ordinal;
|
|
672
|
+
let at = record === undefined ? undefined : record.deadline;
|
|
673
|
+
if (at === undefined) {
|
|
674
|
+
// The absolute deadline is computed once and persisted. Recomputing
|
|
675
|
+
// from "now" on replay would extend the sleep by however long the
|
|
676
|
+
// instance was down, so a crash could postpone a deadline forever.
|
|
677
|
+
at = deadline();
|
|
678
|
+
if (at - Date.now() > __WF_MAX_WAIT_MS) {
|
|
679
|
+
throw __wfError(
|
|
680
|
+
`sleep ${JSON.stringify(name)} ends ${at - Date.now()} ms from now, ` +
|
|
681
|
+
"above the upstream limit of 365 days",
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
await storage.put(key, {
|
|
685
|
+
kind: "sleep",
|
|
686
|
+
status: "sleeping",
|
|
687
|
+
deadline: at,
|
|
688
|
+
ordinal: historyOrdinal,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
if (at <= Date.now()) {
|
|
692
|
+
await storage.put(key, {
|
|
693
|
+
kind: "sleep",
|
|
694
|
+
status: "completed",
|
|
695
|
+
ordinal: historyOrdinal,
|
|
696
|
+
});
|
|
697
|
+
return { value: undefined };
|
|
698
|
+
}
|
|
699
|
+
return { block: at, kind: "sleep" };
|
|
700
|
+
})();
|
|
701
|
+
return {
|
|
702
|
+
do: doStep,
|
|
703
|
+
sleep: (name, duration) => {
|
|
704
|
+
let durationMs;
|
|
705
|
+
try {
|
|
706
|
+
__wfStepName(name);
|
|
707
|
+
durationMs = __wfSleepDuration(duration, name);
|
|
708
|
+
} catch (error) {
|
|
709
|
+
return Promise.reject(error);
|
|
710
|
+
}
|
|
711
|
+
return frame(name, (key, _count, ordinal) =>
|
|
712
|
+
sleepUntilDeadline(
|
|
713
|
+
name,
|
|
714
|
+
key,
|
|
715
|
+
ordinal,
|
|
716
|
+
() => Date.now() + durationMs,
|
|
717
|
+
));
|
|
718
|
+
},
|
|
719
|
+
sleepUntil: (name, timestamp) => {
|
|
720
|
+
let at;
|
|
721
|
+
try {
|
|
722
|
+
__wfStepName(name);
|
|
723
|
+
at = __wfSleepUntil(timestamp, name);
|
|
724
|
+
} catch (error) {
|
|
725
|
+
return Promise.reject(error);
|
|
726
|
+
}
|
|
727
|
+
return frame(name, (key, _count, ordinal) =>
|
|
728
|
+
sleepUntilDeadline(name, key, ordinal, () => {
|
|
729
|
+
// A replay uses the persisted deadline after it has passed. Check
|
|
730
|
+
// the moving future bound only while the deadline is first installed,
|
|
731
|
+
// or every successful sleep would fail on the resume that completes it.
|
|
732
|
+
if (at - Date.now() < 0) {
|
|
733
|
+
throw __wfError(
|
|
734
|
+
`sleepUntil ${JSON.stringify(name)} needs a future timestamp`,
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
return at;
|
|
738
|
+
}));
|
|
739
|
+
},
|
|
740
|
+
waitForEvent: (name, options) => {
|
|
741
|
+
let checked;
|
|
742
|
+
try {
|
|
743
|
+
__wfStepName(name);
|
|
744
|
+
checked = __wfWaitOptions(options, name);
|
|
745
|
+
} catch (error) {
|
|
746
|
+
return Promise.reject(error);
|
|
747
|
+
}
|
|
748
|
+
return frame(name, async (key, _count, ordinal) => {
|
|
749
|
+
let record = __wfKindCheck(await storage.get(key), "event", name);
|
|
750
|
+
if (record !== undefined && record.status === "completed") {
|
|
751
|
+
return { value: record.value };
|
|
752
|
+
}
|
|
753
|
+
if (record !== undefined && record.status === "failed") {
|
|
754
|
+
throw __wfReviveError(record.error);
|
|
755
|
+
}
|
|
756
|
+
if (record === undefined) {
|
|
757
|
+
record = {
|
|
758
|
+
kind: "event",
|
|
759
|
+
status: "waiting",
|
|
760
|
+
type: checked.type,
|
|
761
|
+
deadline: Date.now() + checked.timeoutMs,
|
|
762
|
+
ordinal,
|
|
763
|
+
};
|
|
764
|
+
await storage.put(key, record);
|
|
765
|
+
}
|
|
766
|
+
// The deadline is checked before the buffer: an event that arrived
|
|
767
|
+
// after the persisted deadline must time the step out, not succeed
|
|
768
|
+
// because the replay that noticed it ran late.
|
|
769
|
+
if (record.deadline <= Date.now()) {
|
|
770
|
+
const error = new Error(
|
|
771
|
+
`waitForEvent ${JSON.stringify(name)} timed out waiting for an event ` +
|
|
772
|
+
`of type ${JSON.stringify(record.type)}`,
|
|
773
|
+
);
|
|
774
|
+
await storage.put(key, {
|
|
775
|
+
kind: "event",
|
|
776
|
+
status: "failed",
|
|
777
|
+
error: __wfErrorRecord(error),
|
|
778
|
+
ordinal: record.ordinal,
|
|
779
|
+
});
|
|
780
|
+
throw error;
|
|
781
|
+
}
|
|
782
|
+
// Delivered events persist until a matching step consumes one, in
|
|
783
|
+
// arrival order -- the zero-padded sequence key makes list order
|
|
784
|
+
// arrival order -- so an event sent before the step is reached is
|
|
785
|
+
// buffered, not lost.
|
|
786
|
+
const consumed = storage.transactionSync(() => {
|
|
787
|
+
const current = __wfKindCheck(storage.kv.get(key), "event", name);
|
|
788
|
+
if (current !== undefined && current.status === "completed") {
|
|
789
|
+
return { value: current.value };
|
|
790
|
+
}
|
|
791
|
+
if (current !== undefined && current.status === "failed") {
|
|
792
|
+
throw __wfReviveError(current.error);
|
|
793
|
+
}
|
|
794
|
+
for (const [eventKey, event] of storage.kv.list({
|
|
795
|
+
prefix: __wfEventPrefix(driver.generation),
|
|
796
|
+
})) {
|
|
797
|
+
if (event.type !== current.type) continue;
|
|
798
|
+
const value = {
|
|
799
|
+
payload: event.payload,
|
|
800
|
+
timestamp: new Date(event.timestampMs),
|
|
801
|
+
type: event.type,
|
|
802
|
+
};
|
|
803
|
+
// The delete and completed ledger record are one SQLite commit.
|
|
804
|
+
// A crash can therefore expose both or neither, so an
|
|
805
|
+
// acknowledged event cannot disappear between them.
|
|
806
|
+
storage.kv.delete(eventKey);
|
|
807
|
+
/*__CELLD_TEST_WORKFLOW_EVENT_CONSUMED__*/
|
|
808
|
+
storage.kv.put(key, {
|
|
809
|
+
kind: "event",
|
|
810
|
+
status: "completed",
|
|
811
|
+
value,
|
|
812
|
+
ordinal: current.ordinal,
|
|
813
|
+
});
|
|
814
|
+
return { value };
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
if (consumed !== undefined) return consumed;
|
|
818
|
+
return { block: record.deadline, kind: "event" };
|
|
819
|
+
});
|
|
820
|
+
},
|
|
821
|
+
};
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
const __WorkflowCell = (() => {
|
|
825
|
+
class StorageTransaction {
|
|
826
|
+
constructor(storage) {
|
|
827
|
+
this.kv = storage.kv;
|
|
828
|
+
this._scope = storage._scope;
|
|
829
|
+
}
|
|
830
|
+
setAlarm(t) {
|
|
831
|
+
__alarm_set(this._scope, t instanceof Date ? t.getTime() : Number(t));
|
|
832
|
+
}
|
|
833
|
+
deleteAlarm() {
|
|
834
|
+
__alarm_delete(this._scope);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const transactionSync = (storage, callback) =>
|
|
838
|
+
storage.transactionSync(() => callback(new StorageTransaction(storage)));
|
|
839
|
+
const clearInstance = (transaction) => {
|
|
840
|
+
for (const [key] of transaction.kv.list({ prefix: "__wf." })) {
|
|
841
|
+
transaction.kv.delete(key);
|
|
842
|
+
}
|
|
843
|
+
transaction.deleteAlarm();
|
|
844
|
+
};
|
|
845
|
+
const expireTerminal = (transaction, meta, now) => {
|
|
846
|
+
if (!__wfTerminal(meta.status)) return false;
|
|
847
|
+
if (meta.expiresMs === undefined) {
|
|
848
|
+
meta.endedMs = now;
|
|
849
|
+
meta.expiresMs = now + __wfRetentionMs(meta, meta.status);
|
|
850
|
+
transaction.kv.put("__wf.meta", meta);
|
|
851
|
+
}
|
|
852
|
+
if (meta.expiresMs > now) return false;
|
|
853
|
+
clearInstance(transaction);
|
|
854
|
+
return true;
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
// The adapter stays in this closure because Workflow needs synchronous
|
|
858
|
+
// alarm writes inside transactionSync(). The public async alarm API runs
|
|
859
|
+
// after that commit, while a global helper would let application code
|
|
860
|
+
// split metadata from its wake or invoke the raw operation after commit.
|
|
861
|
+
return class __WorkflowCell {
|
|
862
|
+
constructor(state) {
|
|
863
|
+
this._state = state;
|
|
864
|
+
}
|
|
865
|
+
async __wfCreate({
|
|
866
|
+
workflowName,
|
|
867
|
+
instanceId,
|
|
868
|
+
params,
|
|
869
|
+
retention,
|
|
870
|
+
locationHint,
|
|
871
|
+
skipExisting = false,
|
|
872
|
+
}) {
|
|
873
|
+
const storage = this._state.storage;
|
|
874
|
+
if (params !== undefined) __wfCheckValue(params, "the workflow params");
|
|
875
|
+
const result = transactionSync(storage, (transaction) => {
|
|
876
|
+
let meta = transaction.kv.get("__wf.meta");
|
|
877
|
+
if (meta !== undefined && expireTerminal(transaction, meta, Date.now())) {
|
|
878
|
+
meta = undefined;
|
|
879
|
+
}
|
|
880
|
+
if (meta !== undefined && skipExisting) return { id: instanceId, created: false };
|
|
881
|
+
if (meta !== undefined) {
|
|
882
|
+
throw __wfError(
|
|
883
|
+
`instance ${JSON.stringify(instanceId)} already exists with status ` +
|
|
884
|
+
JSON.stringify(meta.status),
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
const generation = crypto.randomUUID();
|
|
888
|
+
transaction.kv.put("__wf.meta", {
|
|
889
|
+
workflowName,
|
|
890
|
+
instanceId,
|
|
891
|
+
params,
|
|
892
|
+
retention,
|
|
893
|
+
...(locationHint === undefined ? {} : { locationHint }),
|
|
894
|
+
generation,
|
|
895
|
+
createdMs: Date.now(),
|
|
896
|
+
status: "queued",
|
|
897
|
+
});
|
|
898
|
+
// The metadata and alarm are one SQLite commit. A committed creation
|
|
899
|
+
// therefore always has the wake that can start or recover its drive.
|
|
900
|
+
transaction.setAlarm(Date.now());
|
|
901
|
+
/*__CELLD_TEST_WORKFLOW_META_CREATED__*/
|
|
902
|
+
return { id: instanceId, created: true };
|
|
903
|
+
});
|
|
904
|
+
return result;
|
|
905
|
+
}
|
|
906
|
+
async __wfStatus() {
|
|
907
|
+
const meta = transactionSync(this._state.storage, (transaction) => {
|
|
908
|
+
const current = transaction.kv.get("__wf.meta");
|
|
909
|
+
if (current === undefined) return undefined;
|
|
910
|
+
return expireTerminal(transaction, current, Date.now()) ? undefined : current;
|
|
911
|
+
});
|
|
912
|
+
// A cell exists on first address like any cell, so an absent ledger is
|
|
913
|
+
// the only honest "no such instance" signal. Inventing an instance here
|
|
914
|
+
// would turn every typo into an empty workflow.
|
|
915
|
+
if (meta === undefined) throw __wfError("instance does not exist");
|
|
916
|
+
const status = { status: meta.status, rollback: null };
|
|
917
|
+
if (meta.error !== undefined) status.error = meta.error;
|
|
918
|
+
if (meta.output !== undefined) status.output = meta.output;
|
|
919
|
+
return status;
|
|
920
|
+
}
|
|
921
|
+
async __wfDelete() {
|
|
922
|
+
const storage = this._state.storage;
|
|
923
|
+
const deleted = transactionSync(storage, (transaction) => {
|
|
924
|
+
const meta = transaction.kv.get("__wf.meta");
|
|
925
|
+
if (meta === undefined) return false;
|
|
926
|
+
if (expireTerminal(transaction, meta, Date.now())) return false;
|
|
927
|
+
clearInstance(transaction);
|
|
928
|
+
return true;
|
|
929
|
+
});
|
|
930
|
+
// Throw after the transaction commits. An expiry discovered here must
|
|
931
|
+
// keep its cleanup even though delete() reports that the instance is gone.
|
|
932
|
+
if (!deleted) throw __wfError("instance does not exist");
|
|
933
|
+
// A callback can still be awaiting application I/O when deletion lands.
|
|
934
|
+
// Reset the object after the deletion commit so that callback cannot
|
|
935
|
+
// recreate an old-generation ledger entry after delete() reports success.
|
|
936
|
+
this._state.abort(new Error("WORKFLOW_INSTANCE_DELETED"));
|
|
937
|
+
}
|
|
938
|
+
async __wfPause() {
|
|
939
|
+
const storage = this._state.storage;
|
|
940
|
+
transactionSync(storage, (transaction) => {
|
|
941
|
+
const meta = transaction.kv.get("__wf.meta");
|
|
942
|
+
if (meta === undefined) throw __wfError("instance does not exist");
|
|
943
|
+
if (__wfTerminal(meta.status) || meta.status === "paused" ||
|
|
944
|
+
meta.status === "waitingForPause") return;
|
|
945
|
+
if (meta.status === "running") {
|
|
946
|
+
meta.status = "waitingForPause";
|
|
947
|
+
// A drive lost after this commit must re-enter and finish the pause.
|
|
948
|
+
transaction.setAlarm(Date.now());
|
|
949
|
+
} else {
|
|
950
|
+
meta.status = "paused";
|
|
951
|
+
meta.pausedMs = Date.now();
|
|
952
|
+
transaction.deleteAlarm();
|
|
953
|
+
}
|
|
954
|
+
transaction.kv.put("__wf.meta", meta);
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
async __wfResume() {
|
|
958
|
+
const storage = this._state.storage;
|
|
959
|
+
transactionSync(storage, (transaction) => {
|
|
960
|
+
const meta = transaction.kv.get("__wf.meta");
|
|
961
|
+
if (meta === undefined) throw __wfError("instance does not exist");
|
|
962
|
+
if (meta.status === "waitingForPause") {
|
|
963
|
+
meta.status = "running";
|
|
964
|
+
transaction.kv.put("__wf.meta", meta);
|
|
965
|
+
transaction.setAlarm(Date.now());
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (meta.status !== "paused") {
|
|
969
|
+
throw __wfError(
|
|
970
|
+
`cannot resume an instance with status ${JSON.stringify(meta.status)}`,
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
const offset = Math.max(0, Date.now() - (meta.pausedMs ?? Date.now()));
|
|
974
|
+
for (const [key, record] of transaction.kv.list({
|
|
975
|
+
prefix: __wfLedgerPrefix(meta.generation) + "step.",
|
|
976
|
+
})) {
|
|
977
|
+
if (record.status === "retrying") record.nextAt += offset;
|
|
978
|
+
if (record.status === "sleeping" || record.status === "waiting") {
|
|
979
|
+
record.deadline += offset;
|
|
980
|
+
}
|
|
981
|
+
transaction.kv.put(key, record);
|
|
982
|
+
}
|
|
983
|
+
delete meta.pausedMs;
|
|
984
|
+
meta.status = "queued";
|
|
985
|
+
transaction.kv.put("__wf.meta", meta);
|
|
986
|
+
transaction.setAlarm(Date.now());
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
async __wfRestart(options) {
|
|
990
|
+
const checked = __wfRestartOptions(options);
|
|
991
|
+
const storage = this._state.storage;
|
|
992
|
+
transactionSync(storage, (transaction) => {
|
|
993
|
+
const meta = transaction.kv.get("__wf.meta");
|
|
994
|
+
if (meta === undefined) throw __wfError("instance does not exist");
|
|
995
|
+
const oldPrefix = __wfLedgerPrefix(meta.generation);
|
|
996
|
+
let targetOrdinal;
|
|
997
|
+
if (checked.from !== undefined) {
|
|
998
|
+
const expectedKind = checked.from.type === "waitForEvent"
|
|
999
|
+
? "event"
|
|
1000
|
+
: checked.from.type;
|
|
1001
|
+
let occurrence = 0;
|
|
1002
|
+
const history = [...transaction.kv.list({ prefix: oldPrefix + "step." })]
|
|
1003
|
+
.filter(([, record]) => Number.isInteger(record.ordinal))
|
|
1004
|
+
.sort((left, right) => left[1].ordinal - right[1].ordinal);
|
|
1005
|
+
// The ledger key is `<prefix>step.<per-name-count>.<name>`. Derive
|
|
1006
|
+
// the name from it because a record stores replay state, not identity.
|
|
1007
|
+
for (const [key, record] of history) {
|
|
1008
|
+
const marker = oldPrefix + "step.";
|
|
1009
|
+
const suffix = key.slice(marker.length);
|
|
1010
|
+
const dot = suffix.indexOf(".");
|
|
1011
|
+
const name = dot < 0 ? "" : suffix.slice(dot + 1);
|
|
1012
|
+
if (record.kind !== expectedKind || name !== checked.from.name) continue;
|
|
1013
|
+
occurrence += 1;
|
|
1014
|
+
if (occurrence === checked.from.count) {
|
|
1015
|
+
targetOrdinal = record.ordinal;
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
if (targetOrdinal === undefined) {
|
|
1020
|
+
throw __wfError(
|
|
1021
|
+
`restart() could not find ${checked.from.type} step ` +
|
|
1022
|
+
`${JSON.stringify(checked.from.name)} occurrence ${checked.from.count}`,
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const generation = crypto.randomUUID();
|
|
1027
|
+
const newPrefix = __wfLedgerPrefix(generation);
|
|
1028
|
+
if (targetOrdinal !== undefined) {
|
|
1029
|
+
for (const [key, record] of transaction.kv.list({ prefix: oldPrefix + "step." })) {
|
|
1030
|
+
if (Number.isInteger(record.ordinal) && record.ordinal < targetOrdinal) {
|
|
1031
|
+
transaction.kv.put(newPrefix + key.slice(oldPrefix.length), record);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
for (const [key] of transaction.kv.list({ prefix: oldPrefix })) {
|
|
1036
|
+
transaction.kv.delete(key);
|
|
1037
|
+
}
|
|
1038
|
+
meta.generation = generation;
|
|
1039
|
+
meta.status = "queued";
|
|
1040
|
+
delete meta.pausedMs;
|
|
1041
|
+
delete meta.output;
|
|
1042
|
+
delete meta.error;
|
|
1043
|
+
transaction.kv.put("__wf.meta", meta);
|
|
1044
|
+
transaction.setAlarm(Date.now());
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
async __wfTerminate() {
|
|
1048
|
+
const storage = this._state.storage;
|
|
1049
|
+
transactionSync(storage, (transaction) => {
|
|
1050
|
+
const meta = transaction.kv.get("__wf.meta");
|
|
1051
|
+
if (meta === undefined) throw __wfError("instance does not exist");
|
|
1052
|
+
if (__wfTerminal(meta.status)) {
|
|
1053
|
+
throw __wfError(
|
|
1054
|
+
`cannot terminate an instance with status ${JSON.stringify(meta.status)}`,
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
meta.status = "terminated";
|
|
1058
|
+
__wfFinishRetention(meta, transaction);
|
|
1059
|
+
transaction.kv.put("__wf.meta", meta);
|
|
1060
|
+
/*__CELLD_TEST_WORKFLOW_TERMINAL_ALARM_SET__*/
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
async __wfSendEvent(options) {
|
|
1064
|
+
const storage = this._state.storage;
|
|
1065
|
+
// The binding validates first to avoid an unnecessary cell call. Repeat
|
|
1066
|
+
// the check here because this RPC owns the durable event invariant: no
|
|
1067
|
+
// current or future internal caller can bypass validation before storage.
|
|
1068
|
+
const { type, payload } = __wfEventOptions(options);
|
|
1069
|
+
transactionSync(storage, (transaction) => {
|
|
1070
|
+
const meta = transaction.kv.get("__wf.meta");
|
|
1071
|
+
if (meta === undefined) throw __wfError("instance does not exist");
|
|
1072
|
+
if (__wfTerminal(meta.status)) {
|
|
1073
|
+
throw __wfError(
|
|
1074
|
+
`cannot send an event to an instance with status ${JSON.stringify(meta.status)}`,
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
const sequenceKey = __wfLedgerPrefix(meta.generation) + "eventSeq";
|
|
1078
|
+
const sequence = transaction.kv.get(sequenceKey) ?? 0;
|
|
1079
|
+
transaction.kv.put(sequenceKey, sequence + 1);
|
|
1080
|
+
transaction.kv.put(__wfEventKey(meta.generation, sequence), {
|
|
1081
|
+
type,
|
|
1082
|
+
payload,
|
|
1083
|
+
timestampMs: Date.now(),
|
|
1084
|
+
});
|
|
1085
|
+
// Allocation, the event body, and the nudge are one commit. Parallel
|
|
1086
|
+
// writers cannot reuse a sequence and a crash cannot expose a partial
|
|
1087
|
+
// acknowledged send.
|
|
1088
|
+
if (meta.status !== "paused" && meta.status !== "waitingForPause") {
|
|
1089
|
+
transaction.setAlarm(Date.now());
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
async alarm() {
|
|
1094
|
+
const storage = this._state.storage;
|
|
1095
|
+
// One drive at a time. A sendEvent nudge (setAlarm(now)) landing
|
|
1096
|
+
// mid-fire replaces the firing alarm in the core, and the replacement
|
|
1097
|
+
// can dispatch a second alarm() into this isolate while the first drive
|
|
1098
|
+
// still awaits user code. Two concurrent replays would both see the
|
|
1099
|
+
// same incomplete steps and double-execute their callbacks. The guard
|
|
1100
|
+
// is isolate-local and that is sufficient: every drive for a cell runs
|
|
1101
|
+
// in its owner's isolate, and a takeover starts a fresh isolate with no
|
|
1102
|
+
// drive in flight. The nudge is not lost -- the in-flight drive folds
|
|
1103
|
+
// `_wfNudge` into the minimum it re-arms, and if it suspended before
|
|
1104
|
+
// seeing the flag, the re-arm below starts a fresh drive immediately.
|
|
1105
|
+
// A failed drive is not re-raised here: the engine already retries the
|
|
1106
|
+
// alarm that fired it.
|
|
1107
|
+
if (this._wfDriving !== undefined) {
|
|
1108
|
+
this._wfNudge = Date.now();
|
|
1109
|
+
await this._wfDriving.catch(() => {});
|
|
1110
|
+
if (this._wfNudge !== undefined) {
|
|
1111
|
+
this._wfNudge = undefined;
|
|
1112
|
+
const meta = await storage.get("__wf.meta");
|
|
1113
|
+
if (
|
|
1114
|
+
meta !== undefined && !__wfTerminal(meta.status) &&
|
|
1115
|
+
meta.status !== "paused" && meta.status !== "waitingForPause"
|
|
1116
|
+
) {
|
|
1117
|
+
await storage.setAlarm(Date.now());
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const meta = await storage.get("__wf.meta");
|
|
1123
|
+
// A terminal instance's pending alarm can still fire. It owns retention:
|
|
1124
|
+
// remove expired state, or restore the expiry wake after a stale nudge.
|
|
1125
|
+
if (meta === undefined) {
|
|
1126
|
+
await storage.deleteAlarm();
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (__wfTerminal(meta.status)) {
|
|
1130
|
+
transactionSync(storage, (transaction) => {
|
|
1131
|
+
const current = transaction.kv.get("__wf.meta");
|
|
1132
|
+
if (current === undefined) {
|
|
1133
|
+
transaction.deleteAlarm();
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (!expireTerminal(transaction, current, Date.now())) {
|
|
1137
|
+
transaction.setAlarm(current.expiresMs);
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (meta.status === "paused") {
|
|
1143
|
+
await storage.deleteAlarm();
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (meta.status === "waitingForPause") {
|
|
1147
|
+
transactionSync(storage, (transaction) => {
|
|
1148
|
+
const current = transaction.kv.get("__wf.meta");
|
|
1149
|
+
if (current === undefined || current.generation !== meta.generation ||
|
|
1150
|
+
current.status !== "waitingForPause") return;
|
|
1151
|
+
current.status = "paused";
|
|
1152
|
+
current.pausedMs = Date.now();
|
|
1153
|
+
transaction.kv.put("__wf.meta", current);
|
|
1154
|
+
transaction.deleteAlarm();
|
|
1155
|
+
});
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
// The fired alarm stays armed while the drive runs. The engine consumes
|
|
1159
|
+
// it only when this handler returns cleanly, and retries it when the
|
|
1160
|
+
// handler fails -- that retry is the crash safety net. Deleting it here
|
|
1161
|
+
// would commit the deletion durably mid-drive (durability is per SQLite
|
|
1162
|
+
// commit, and replication ships frames per commit), so a SIGKILL before
|
|
1163
|
+
// the suspend-time re-arm would strand a "running" instance with no
|
|
1164
|
+
// alarm and no wake entry. The suspend path re-arms explicitly, and a
|
|
1165
|
+
// terminal drive deletes.
|
|
1166
|
+
const driving = this._drive(meta);
|
|
1167
|
+
this._wfDriving = driving;
|
|
1168
|
+
try {
|
|
1169
|
+
await driving;
|
|
1170
|
+
} finally {
|
|
1171
|
+
this._wfDriving = undefined;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
async _drive(meta) {
|
|
1175
|
+
const storage = this._state.storage;
|
|
1176
|
+
// The drive owns `status`, `output`, and `error`; every other piece of
|
|
1177
|
+
// instance state belongs to the RPCs (sendEvent's sequence lives in its
|
|
1178
|
+
// own key for the same reason). The gate is open at every await, so a
|
|
1179
|
+
// terminate can land mid-drive: re-reading before each write, and
|
|
1180
|
+
// writing nothing once the status is terminal, is what makes "terminate
|
|
1181
|
+
// wins the race" true rather than asserted. Writing back the meta this
|
|
1182
|
+
// drive entered with would resurrect a terminated instance as "waiting"
|
|
1183
|
+
// and re-arm the alarm its terminate deleted.
|
|
1184
|
+
const settle = (mutate) => transactionSync(storage, (transaction) => {
|
|
1185
|
+
const current = transaction.kv.get("__wf.meta");
|
|
1186
|
+
if (current === undefined || __wfTerminal(current.status) ||
|
|
1187
|
+
current.generation !== meta.generation) return false;
|
|
1188
|
+
mutate(current, transaction);
|
|
1189
|
+
transaction.kv.put("__wf.meta", current);
|
|
1190
|
+
return true;
|
|
1191
|
+
});
|
|
1192
|
+
const className = __cell.workflows[meta.workflowName];
|
|
1193
|
+
const cls = className === undefined ? undefined : __cf.exports[className];
|
|
1194
|
+
if (typeof cls !== "function" ||
|
|
1195
|
+
typeof (cls.prototype && cls.prototype.run) !== "function") {
|
|
1196
|
+
// Deploy cannot see inside the bundle, so a missing export fails
|
|
1197
|
+
// here, loudly, with the class and the config key named.
|
|
1198
|
+
settle((current, transaction) => {
|
|
1199
|
+
current.status = "errored";
|
|
1200
|
+
current.error = {
|
|
1201
|
+
name: "Error",
|
|
1202
|
+
message: className === undefined
|
|
1203
|
+
? `workflow ${JSON.stringify(meta.workflowName)} is not declared ` +
|
|
1204
|
+
"in this deployment's `workflows`"
|
|
1205
|
+
: `workflow class ${JSON.stringify(className)} is not a module ` +
|
|
1206
|
+
"export with a run() method; export it and extend " +
|
|
1207
|
+
"WorkflowEntrypoint",
|
|
1208
|
+
};
|
|
1209
|
+
__wfFinishRetention(current, transaction);
|
|
1210
|
+
/*__CELLD_TEST_WORKFLOW_TERMINAL_ALARM_SET__*/
|
|
1211
|
+
});
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
const driver = {
|
|
1215
|
+
storage: undefined,
|
|
1216
|
+
storageFailure: undefined,
|
|
1217
|
+
generation: meta.generation,
|
|
1218
|
+
ledgerPrefix: __wfLedgerPrefix(meta.generation),
|
|
1219
|
+
counts: new Map(),
|
|
1220
|
+
ordinal: 0,
|
|
1221
|
+
running: 0,
|
|
1222
|
+
blocked: [],
|
|
1223
|
+
pauseRequested: false,
|
|
1224
|
+
finished: false,
|
|
1225
|
+
checkQueued: false,
|
|
1226
|
+
stallTimer: undefined,
|
|
1227
|
+
clearStall() {
|
|
1228
|
+
if (this.stallTimer !== undefined) {
|
|
1229
|
+
clearTimeout(this.stallTimer);
|
|
1230
|
+
this.stallTimer = undefined;
|
|
1231
|
+
}
|
|
1232
|
+
},
|
|
1233
|
+
};
|
|
1234
|
+
driver.storage = __wfTrackStorageFailures(storage, driver);
|
|
1235
|
+
const outcome = new Promise((resolve) => {
|
|
1236
|
+
driver.finish = (result) => {
|
|
1237
|
+
if (driver.finished) return;
|
|
1238
|
+
driver.finished = true;
|
|
1239
|
+
driver.clearStall();
|
|
1240
|
+
resolve(result);
|
|
1241
|
+
};
|
|
1242
|
+
});
|
|
1243
|
+
// Quiescence: when a step is blocked, no step callback is executing,
|
|
1244
|
+
// and a macrotask has passed, nothing can progress, so the invocation
|
|
1245
|
+
// suspends. The macrotask lets every microtask chain a completed step
|
|
1246
|
+
// unblocked drain first; checking on the spot would suspend an instance
|
|
1247
|
+
// whose next step is one `await` away from starting.
|
|
1248
|
+
driver.check = () => {
|
|
1249
|
+
if (driver.checkQueued || driver.finished) return;
|
|
1250
|
+
driver.checkQueued = true;
|
|
1251
|
+
setTimeout(() => {
|
|
1252
|
+
driver.checkQueued = false;
|
|
1253
|
+
if (driver.finished || driver.running !== 0) return;
|
|
1254
|
+
if (driver.pauseRequested) {
|
|
1255
|
+
driver.finish({ kind: "pause" });
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
if (driver.blocked.length > 0) {
|
|
1259
|
+
driver.finish({ kind: "suspend" });
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
// No step runs, none is blocked, and run() has not settled: it
|
|
1263
|
+
// awaits something that is not a step. Upstream permits that -- an
|
|
1264
|
+
// un-stepped `await fetch()` between steps is legal, merely
|
|
1265
|
+
// non-durable -- so this is not yet an error; but a promise that
|
|
1266
|
+
// never settles would wedge the alarm handler in silence, with the
|
|
1267
|
+
// instance "running" forever. A grace timer splits the two: if the
|
|
1268
|
+
// same nothing-can-progress state still holds when it fires, the
|
|
1269
|
+
// instance fails loudly. The timer is a plain in-isolate timeout,
|
|
1270
|
+
// not a durable alarm, on purpose: an eviction or a restart replays
|
|
1271
|
+
// run() from the top anyway, so losing the timer loses nothing --
|
|
1272
|
+
// the replay re-enters the same stall and re-arms its own grace.
|
|
1273
|
+
if (driver.stallTimer !== undefined) return;
|
|
1274
|
+
driver.stallTimer = setTimeout(() => {
|
|
1275
|
+
driver.stallTimer = undefined;
|
|
1276
|
+
if (driver.finished || driver.running !== 0 || driver.blocked.length > 0) {
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
driver.finish({
|
|
1280
|
+
kind: "error",
|
|
1281
|
+
error: __wfError(
|
|
1282
|
+
"run() has been awaiting something that is not a step for " +
|
|
1283
|
+
(__WF_STALL_GRACE_MS / 1000) + " seconds while no step is " +
|
|
1284
|
+
"running or blocked; a replay cannot resume this wait, so " +
|
|
1285
|
+
"wrap long async work in step.do",
|
|
1286
|
+
),
|
|
1287
|
+
});
|
|
1288
|
+
}, __WF_STALL_GRACE_MS);
|
|
1289
|
+
}, 0);
|
|
1290
|
+
};
|
|
1291
|
+
if (
|
|
1292
|
+
meta.status !== "running" &&
|
|
1293
|
+
!(await settle((current) => {
|
|
1294
|
+
current.status = "running";
|
|
1295
|
+
}))
|
|
1296
|
+
) {
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const step = __wfMakeStep(driver);
|
|
1300
|
+
const event = {
|
|
1301
|
+
payload: meta.params,
|
|
1302
|
+
timestamp: new Date(meta.createdMs),
|
|
1303
|
+
instanceId: meta.instanceId,
|
|
1304
|
+
workflowName: meta.workflowName,
|
|
1305
|
+
};
|
|
1306
|
+
// ExecutionContext-shaped, as the WorkflowEntrypoint constructor
|
|
1307
|
+
// documents upstream.
|
|
1308
|
+
const ctx = {
|
|
1309
|
+
waitUntil: __registerWaitUntil,
|
|
1310
|
+
passThroughOnException() {},
|
|
1311
|
+
};
|
|
1312
|
+
(async () => new cls(ctx, __cell.env).run(event, step))().then(
|
|
1313
|
+
(value) => driver.finish({ kind: "complete", value }),
|
|
1314
|
+
(error) => driver.finish({ kind: "error", error }),
|
|
1315
|
+
);
|
|
1316
|
+
/*__CELLD_TEST_WORKFLOW_AFTER_RUN_STARTED__*/
|
|
1317
|
+
let result = await outcome;
|
|
1318
|
+
// The workflow can catch a rejected step promise, but it cannot turn an
|
|
1319
|
+
// engine storage failure into a successful run. Preserve the alarm so
|
|
1320
|
+
// the runtime retries over the last committed ledger prefix.
|
|
1321
|
+
if (driver.storageFailure !== undefined) {
|
|
1322
|
+
throw driver.storageFailure;
|
|
1323
|
+
}
|
|
1324
|
+
const currentMeta = await storage.get("__wf.meta");
|
|
1325
|
+
if (
|
|
1326
|
+
result.kind === "pause" ||
|
|
1327
|
+
(currentMeta !== undefined &&
|
|
1328
|
+
currentMeta.generation === meta.generation &&
|
|
1329
|
+
currentMeta.status === "waitingForPause")
|
|
1330
|
+
) {
|
|
1331
|
+
/*__CELLD_TEST_WORKFLOW_BEFORE_PAUSE_SETTLE__*/
|
|
1332
|
+
settle((current, transaction) => {
|
|
1333
|
+
// A resume can cancel waitingForPause after this drive selected its
|
|
1334
|
+
// pause result. Preserve that newer running state and its alarm, or
|
|
1335
|
+
// this stale settlement strands an acknowledged resume as paused.
|
|
1336
|
+
if (current.status !== "waitingForPause") return;
|
|
1337
|
+
current.status = "paused";
|
|
1338
|
+
current.pausedMs = Date.now();
|
|
1339
|
+
transaction.deleteAlarm();
|
|
1340
|
+
});
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
if (result.kind === "suspend") {
|
|
1344
|
+
let at = Math.min(...driver.blocked.map((entry) => entry.at));
|
|
1345
|
+
// A nudge that landed mid-drive must not wait out the pending
|
|
1346
|
+
// deadline. It is visible in one of two places: getAlarm(), when the
|
|
1347
|
+
// nudge re-armed and no second alarm dispatched yet, or `_wfNudge`,
|
|
1348
|
+
// when a second alarm() already entered and parked on the drive
|
|
1349
|
+
// guard -- its dispatch consumed the armed alarm, so getAlarm()
|
|
1350
|
+
// alone would miss it.
|
|
1351
|
+
const nudge = await storage.getAlarm();
|
|
1352
|
+
if (nudge !== null) at = Math.min(at, nudge);
|
|
1353
|
+
if (this._wfNudge !== undefined) {
|
|
1354
|
+
at = Math.min(at, this._wfNudge);
|
|
1355
|
+
this._wfNudge = undefined;
|
|
1356
|
+
}
|
|
1357
|
+
settle((current, transaction) => {
|
|
1358
|
+
current.status = "waiting";
|
|
1359
|
+
transaction.setAlarm(at);
|
|
1360
|
+
});
|
|
1361
|
+
// The explicit re-arm replaces the alarm that fired this drive.
|
|
1362
|
+
// Durability is per SQLite commit, not per turn: a crash can persist
|
|
1363
|
+
// any prefix of this invocation's ledger writes, and the unconsumed
|
|
1364
|
+
// fired alarm is what re-drives the replay over whatever prefix
|
|
1365
|
+
// survived. A terminated instance re-arms nothing; its leftover
|
|
1366
|
+
// alarm fires once, sees the terminal status, and retires itself.
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
if (result.kind === "complete") {
|
|
1370
|
+
try {
|
|
1371
|
+
if (result.value !== undefined) {
|
|
1372
|
+
__wfCheckValue(result.value, "the run() return value");
|
|
1373
|
+
}
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
result = { kind: "error", error };
|
|
1376
|
+
}
|
|
1377
|
+
if (result.kind === "complete") {
|
|
1378
|
+
// Keep settlement outside the value-validation catch. A storage
|
|
1379
|
+
// failure must escape the alarm handler so the engine retries the
|
|
1380
|
+
// still-armed drive; converting it to a user error would consume the
|
|
1381
|
+
// alarm and make a transient commit failure terminal.
|
|
1382
|
+
settle((current, transaction) => {
|
|
1383
|
+
current.status = "complete";
|
|
1384
|
+
if (result.value !== undefined) current.output = result.value;
|
|
1385
|
+
// The terminal commit installs the expiry wake with the retained
|
|
1386
|
+
// state, so a crash can expose neither without the other.
|
|
1387
|
+
__wfFinishRetention(current, transaction);
|
|
1388
|
+
/*__CELLD_TEST_WORKFLOW_TERMINAL_ALARM_SET__*/
|
|
1389
|
+
});
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
settle((current, transaction) => {
|
|
1394
|
+
current.status = "errored";
|
|
1395
|
+
current.error = __wfErrorRecord(result.error);
|
|
1396
|
+
__wfFinishRetention(current, transaction);
|
|
1397
|
+
/*__CELLD_TEST_WORKFLOW_TERMINAL_ALARM_SET__*/
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
})();
|
|
1402
|
+
__runtimeClass("__Workflow", __WorkflowCell);
|
|
1403
|
+
|
|
1404
|
+
class NonRetryableError extends Error {
|
|
1405
|
+
constructor(message, name = "NonRetryableError") {
|
|
1406
|
+
super(message);
|
|
1407
|
+
this.name = name;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
// Backing object for the `cloudflare:workflows` builtin module.
|
|
1411
|
+
__celld.__cfWorkflows = { NonRetryableError };
|
|
1412
|
+
// `celld:python`, imported only by the Python Worker bundle.
|
|
1413
|
+
__celld.__pythonHost = {
|
|
1414
|
+
condemnIsolate: (reason) => __isolate_condemn(String(reason)),
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1417
|
+
const __wfValidInstanceId = (id) =>
|
|
1418
|
+
typeof id === "string" && id.length >= 1 && id.length <= 100 &&
|
|
1419
|
+
__WF_NAME_RE.test(id);
|
|
1420
|
+
const __wfCreateOptions = (options, what, validateParams = true) => {
|
|
1421
|
+
if (!__wfOwnObject(options)) {
|
|
1422
|
+
throw __wfError(`${what} needs an object`);
|
|
1423
|
+
}
|
|
1424
|
+
const retention = __wfRetentionOptions(options.retention, what);
|
|
1425
|
+
const locationHint = __wfLocationHint(options.locationHint, what);
|
|
1426
|
+
const id = options.id === undefined ? crypto.randomUUID() : options.id;
|
|
1427
|
+
if (!__wfValidInstanceId(id)) {
|
|
1428
|
+
throw __wfError(
|
|
1429
|
+
`invalid instance id ${JSON.stringify(id)}: use letters, digits, ` +
|
|
1430
|
+
"\"-\" and \"_\" (not starting with \"-\"), at most 100 characters",
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
if (validateParams && options.params !== undefined) {
|
|
1434
|
+
// The cell repeats this serialization check because it owns the durable
|
|
1435
|
+
// create invariant. A single create rejects before it addresses the cell.
|
|
1436
|
+
__wfCheckValue(options.params, "the workflow params");
|
|
1437
|
+
}
|
|
1438
|
+
return { id, params: options.params, retention, locationHint };
|
|
1439
|
+
};
|
|
1440
|
+
|
|
1441
|
+
// Named so SDKs that sniff a binding by `constructor.name` recognise it.
|
|
1442
|
+
class WorkflowInstance {
|
|
1443
|
+
constructor(workflowName, id) {
|
|
1444
|
+
Object.defineProperty(this, "_workflowName", { value: workflowName });
|
|
1445
|
+
this.id = id;
|
|
1446
|
+
}
|
|
1447
|
+
// Resolved per call rather than cached: a cell can move between calls.
|
|
1448
|
+
get _stub() {
|
|
1449
|
+
// The reserved class is script-scoped (`deploy::workflow_class`), because
|
|
1450
|
+
// a workflow instance is: two co-hosted scripts must not share one entry
|
|
1451
|
+
// in the deployment's flat class registry. `__cell.script` is injected
|
|
1452
|
+
// before any binding runs, so this resolves at call time.
|
|
1453
|
+
return __cell.makeNamespace("__Workflow." + __cell.script)
|
|
1454
|
+
.getByName(this._workflowName + "/" + this.id);
|
|
1455
|
+
}
|
|
1456
|
+
async status() {
|
|
1457
|
+
return await this._stub.__wfStatus();
|
|
1458
|
+
}
|
|
1459
|
+
async terminate(options) {
|
|
1460
|
+
if (options && options.rollback) {
|
|
1461
|
+
throw __wfError(
|
|
1462
|
+
"terminate({rollback: true}) is not implemented in celld; step " +
|
|
1463
|
+
"rollbackOptions are not implemented, so no handler could run",
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
await this._stub.__wfTerminate();
|
|
1467
|
+
}
|
|
1468
|
+
async sendEvent(options) {
|
|
1469
|
+
await this._stub.__wfSendEvent(__wfEventOptions(options));
|
|
1470
|
+
}
|
|
1471
|
+
async pause() {
|
|
1472
|
+
await this._stub.__wfPause();
|
|
1473
|
+
}
|
|
1474
|
+
async resume() {
|
|
1475
|
+
await this._stub.__wfResume();
|
|
1476
|
+
}
|
|
1477
|
+
async restart(options) {
|
|
1478
|
+
await this._stub.__wfRestart(__wfRestartOptions(options));
|
|
1479
|
+
}
|
|
1480
|
+
async delete() {
|
|
1481
|
+
try {
|
|
1482
|
+
await this._stub.__wfDelete();
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
// The deletion commit resets an active Workflow cell so an in-flight
|
|
1485
|
+
// callback cannot put old ledger state back. That reset rejects this
|
|
1486
|
+
// RPC with its private completion marker; it is the successful path.
|
|
1487
|
+
if (!String(error && error.message || error).includes("WORKFLOW_INSTANCE_DELETED")) {
|
|
1488
|
+
throw error;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
class Workflow {
|
|
1495
|
+
constructor(workflowName) {
|
|
1496
|
+
Object.defineProperty(this, "_workflowName", { value: workflowName });
|
|
1497
|
+
}
|
|
1498
|
+
async _create(options, skipExisting) {
|
|
1499
|
+
const { id, params, retention, locationHint } = options;
|
|
1500
|
+
const instance = new WorkflowInstance(this._workflowName, id);
|
|
1501
|
+
const result = await instance._stub.__wfCreate({
|
|
1502
|
+
workflowName: this._workflowName,
|
|
1503
|
+
instanceId: id,
|
|
1504
|
+
params,
|
|
1505
|
+
retention,
|
|
1506
|
+
locationHint,
|
|
1507
|
+
skipExisting,
|
|
1508
|
+
});
|
|
1509
|
+
return result.created ? instance : undefined;
|
|
1510
|
+
}
|
|
1511
|
+
async create(options = {}) {
|
|
1512
|
+
return await this._create(__wfCreateOptions(options, "create()"), false);
|
|
1513
|
+
}
|
|
1514
|
+
async createBatch(batch) {
|
|
1515
|
+
if (!Array.isArray(batch)) {
|
|
1516
|
+
throw __wfError("createBatch() needs an array of create options");
|
|
1517
|
+
}
|
|
1518
|
+
if (batch.length > 100) {
|
|
1519
|
+
throw __wfError(
|
|
1520
|
+
`createBatch() accepts at most 100 instances, got ${batch.length}`,
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
if (batch.length === 0) {
|
|
1524
|
+
throw __wfError("createBatch() needs at least one instance");
|
|
1525
|
+
}
|
|
1526
|
+
// Cloudflare rejects a structural error in any item before it creates the
|
|
1527
|
+
// first instance, but it filters a params clone failure per item. Keep
|
|
1528
|
+
// these two gates separate so a bad ID stays atomic while a clone failure
|
|
1529
|
+
// does not suppress the valid items around it.
|
|
1530
|
+
const optionsList = batch.map((options, index) =>
|
|
1531
|
+
__wfCreateOptions(options, `createBatch() item ${index}`, false));
|
|
1532
|
+
const creatable = [];
|
|
1533
|
+
for (const options of optionsList) {
|
|
1534
|
+
let encoded;
|
|
1535
|
+
try {
|
|
1536
|
+
if (options.params !== undefined) {
|
|
1537
|
+
encoded = __wfEncodeValue(options.params, "the workflow params");
|
|
1538
|
+
}
|
|
1539
|
+
} catch {
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
if (encoded !== undefined) {
|
|
1543
|
+
__wfCheckEncodedValue(encoded, "the workflow params");
|
|
1544
|
+
}
|
|
1545
|
+
creatable.push(options);
|
|
1546
|
+
}
|
|
1547
|
+
const created = [];
|
|
1548
|
+
for (const options of creatable) {
|
|
1549
|
+
const instance = await this._create(options, true);
|
|
1550
|
+
if (instance !== undefined) created.push(instance);
|
|
1551
|
+
}
|
|
1552
|
+
return created;
|
|
1553
|
+
}
|
|
1554
|
+
async get(id) {
|
|
1555
|
+
if (!__wfValidInstanceId(id)) {
|
|
1556
|
+
throw __wfError(`invalid instance id ${JSON.stringify(id)}`);
|
|
1557
|
+
}
|
|
1558
|
+
const instance = new WorkflowInstance(this._workflowName, id);
|
|
1559
|
+
// The cell exists on first address like any cell, but get() must not
|
|
1560
|
+
// mint an instance for a typo: an id with no ledger rejects here.
|
|
1561
|
+
await instance._stub.__wfStatus();
|
|
1562
|
+
return instance;
|
|
1563
|
+
}
|
|
1564
|
+
async deleteBatch(instanceIds) {
|
|
1565
|
+
if (!Array.isArray(instanceIds)) {
|
|
1566
|
+
throw __wfError("deleteBatch() needs an array of instance ids");
|
|
1567
|
+
}
|
|
1568
|
+
if (instanceIds.length < 1 || instanceIds.length > 100) {
|
|
1569
|
+
throw __wfError(
|
|
1570
|
+
`deleteBatch() accepts 1 to 100 instance ids, got ${instanceIds.length}`,
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
for (const id of instanceIds) {
|
|
1574
|
+
if (!__wfValidInstanceId(id)) {
|
|
1575
|
+
throw __wfError(`invalid instance id ${JSON.stringify(id)}`);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
const uniqueIds = [...new Set(instanceIds)];
|
|
1579
|
+
const settled = await Promise.allSettled(uniqueIds.map(async (id) => {
|
|
1580
|
+
const instance = new WorkflowInstance(this._workflowName, id);
|
|
1581
|
+
await instance.delete();
|
|
1582
|
+
}));
|
|
1583
|
+
const results = new Map(uniqueIds.map((id, index) => [id, settled[index]]));
|
|
1584
|
+
const output = { deleted: [], errors: [] };
|
|
1585
|
+
for (const id of instanceIds) {
|
|
1586
|
+
const result = results.get(id);
|
|
1587
|
+
if (result.status === "fulfilled") {
|
|
1588
|
+
output.deleted.push({ id });
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
const notFound = String(result.reason && result.reason.message || result.reason)
|
|
1592
|
+
.includes("instance does not exist");
|
|
1593
|
+
output.errors.push({
|
|
1594
|
+
id,
|
|
1595
|
+
code: notFound ? 10400 : 10001,
|
|
1596
|
+
message: notFound
|
|
1597
|
+
? "workflows.api.error.instance.not_found"
|
|
1598
|
+
: "workflows.api.error.internal_server",
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
return output;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
const __makeWorkflow = __celld.__makeWorkflow =
|
|
1606
|
+
(workflowName) => new Workflow(workflowName);
|
|
1607
|
+
|
|
1608
|
+
return { WorkflowCell: classes.__Workflow, makeWorkflow: __celld.__makeWorkflow,
|
|
1609
|
+
NonRetryableError: __celld.__cfWorkflows.NonRetryableError };
|
|
1610
|
+
}
|