@lostgradient/weft 0.23.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -4
- package/dist/cli/codegen-emit-dedup.d.ts +81 -0
- package/dist/cli/codegen-emit-dedup.js +45 -0
- package/dist/cli/codegen-emit-registry.d.ts +90 -0
- package/dist/cli/codegen-emit-registry.js +50 -0
- package/dist/cli/codegen-emit.d.ts +26 -39
- package/dist/cli/codegen-emit.js +4 -27
- package/dist/cli/codegen-validate.d.ts +49 -0
- package/dist/cli/codegen-validate.js +186 -0
- package/dist/cli/codegen.js +6 -87
- package/dist/cli/generated/operation-catalog.snapshot.json +287 -3
- package/dist/cli/generated/operation-client.generated.d.ts +324 -10
- package/dist/cli/generated/operation-client.generated.js +12 -0
- package/dist/cli/output.js +1 -1
- package/dist/cli/storage-factory.js +2 -0
- package/dist/cli/workflow-commands.js +2 -0
- package/dist/cli-main.js +1 -1
- package/dist/core/application-payload-digest.d.ts +42 -0
- package/dist/core/application-payload-digest.js +76 -0
- package/dist/core/application-primitive-abort.d.ts +50 -0
- package/dist/core/application-primitive-abort.js +40 -0
- package/dist/core/application-primitive-attempt-registry.d.ts +74 -0
- package/dist/core/application-primitive-attempt-registry.js +73 -0
- package/dist/core/application-primitive-codec.d.ts +36 -0
- package/dist/core/application-primitive-codec.js +56 -0
- package/dist/core/application-primitive-commit.d.ts +67 -0
- package/dist/core/application-primitive-commit.js +66 -0
- package/dist/core/application-primitive-guards.d.ts +62 -0
- package/dist/core/application-primitive-guards.js +89 -0
- package/dist/core/application-primitive-payload.d.ts +29 -0
- package/dist/core/application-primitive-payload.js +65 -0
- package/dist/core/application-primitive-timing.d.ts +18 -0
- package/dist/core/application-primitive-timing.js +22 -0
- package/dist/core/atomic-state.js +2 -0
- package/dist/core/catalog/codec.d.ts +40 -0
- package/dist/core/catalog/codec.js +45 -0
- package/dist/core/catalog/errors.d.ts +104 -0
- package/dist/core/catalog/errors.js +41 -0
- package/dist/core/catalog/index.d.ts +25 -0
- package/dist/core/catalog/index.js +15 -0
- package/dist/core/catalog/reference-counts.d.ts +105 -0
- package/dist/core/catalog/reference-counts.js +28 -0
- package/dist/core/catalog/removal.d.ts +52 -0
- package/dist/core/catalog/removal.js +14 -0
- package/dist/core/catalog/storage-io.d.ts +78 -0
- package/dist/core/catalog/storage-io.js +106 -0
- package/dist/core/catalog/types.d.ts +108 -0
- package/dist/core/catalog/types.js +0 -0
- package/dist/core/catalog/workflow-catalog.d.ts +165 -0
- package/dist/core/catalog/workflow-catalog.js +198 -0
- package/dist/core/compare-codepoint.d.ts +13 -0
- package/dist/core/compare-codepoint.js +7 -0
- package/dist/core/compression.js +2 -0
- package/dist/core/context/attributes.d.ts +1 -1
- package/dist/core/context/durable-operations.d.ts +2 -2
- package/dist/core/context/index.js +5 -3
- package/dist/core/context/state-namespace.d.ts +1 -1
- package/dist/core/contract/build.d.ts +64 -0
- package/dist/core/contract/build.js +99 -0
- package/dist/core/contract/compatibility.d.ts +187 -0
- package/dist/core/contract/compatibility.js +37 -0
- package/dist/core/contract/failure.d.ts +64 -0
- package/dist/core/contract/failure.js +4 -0
- package/dist/core/contract/hash.d.ts +92 -0
- package/dist/core/contract/hash.js +34 -0
- package/dist/core/contract/index.d.ts +21 -0
- package/dist/core/contract/index.js +22 -0
- package/dist/core/contract/limits.d.ts +74 -0
- package/dist/core/contract/limits.js +1 -0
- package/dist/core/contract/manifest-parse-schema.d.ts +37 -0
- package/dist/core/contract/manifest-parse-schema.js +106 -0
- package/dist/core/contract/manifest-parse.d.ts +76 -0
- package/dist/core/contract/manifest-parse.js +189 -0
- package/dist/core/contract/manifest.d.ts +56 -0
- package/dist/core/contract/manifest.js +31 -0
- package/dist/core/contract/normalize.d.ts +97 -0
- package/dist/core/contract/normalize.js +127 -0
- package/dist/core/contract/revision.d.ts +29 -0
- package/dist/core/contract/revision.js +5 -0
- package/dist/core/contract/types.d.ts +274 -0
- package/dist/core/contract/types.js +1 -0
- package/dist/core/engine/catalog-activation.d.ts +40 -0
- package/dist/core/engine/catalog-activation.js +17 -0
- package/dist/core/engine/catalog-events.d.ts +36 -0
- package/dist/core/engine/catalog-events.js +15 -0
- package/dist/core/engine/catalog-readiness.d.ts +49 -0
- package/dist/core/engine/catalog-readiness.js +59 -0
- package/dist/core/engine/catalog-removal.d.ts +132 -0
- package/dist/core/engine/catalog-removal.js +70 -0
- package/dist/core/engine/construction.d.ts +1 -1
- package/dist/core/engine/construction.js +4 -2
- package/dist/core/engine/disposal.js +9 -0
- package/dist/core/engine/engine-workflows-namespace.d.ts +130 -0
- package/dist/core/engine/engine-workflows-namespace.js +49 -0
- package/dist/core/engine/finalizer-metadata.d.ts +22 -0
- package/dist/core/engine/finalizer-metadata.js +12 -0
- package/dist/core/engine/handle-result.js +2 -0
- package/dist/core/engine/index.d.ts +83 -0
- package/dist/core/engine/index.js +61 -0
- package/dist/core/engine/internals.d.ts +44 -1
- package/dist/core/engine/internals.js +6 -0
- package/dist/core/engine/lifecycle/start.js +3 -0
- package/dist/core/engine/registration.d.ts +76 -0
- package/dist/core/engine/registration.js +12 -6
- package/dist/core/engine/source-registration.d.ts +42 -0
- package/dist/core/engine/source-registration.js +36 -0
- package/dist/core/engine/source-resolution.d.ts +85 -0
- package/dist/core/engine/source-resolution.js +119 -0
- package/dist/core/engine/validation.js +2 -2
- package/dist/core/events/catalog-events.d.ts +117 -0
- package/dist/core/events/catalog-events.js +62 -0
- package/dist/core/events/event-map.d.ts +6 -0
- package/dist/core/events/index.d.ts +1 -0
- package/dist/core/events/index.js +1 -0
- package/dist/core/mailbox-admission.d.ts +26 -0
- package/dist/core/mailbox-admission.js +130 -0
- package/dist/core/mailbox-claims.d.ts +251 -0
- package/dist/core/mailbox-claims.js +0 -0
- package/dist/core/mailbox-codec.d.ts +32 -0
- package/dist/core/mailbox-codec.js +221 -0
- package/dist/core/mailbox-contract.d.ts +290 -0
- package/dist/core/mailbox-contract.js +0 -0
- package/dist/core/mailbox-delivery.d.ts +37 -0
- package/dist/core/mailbox-delivery.js +193 -0
- package/dist/core/mailbox-guards.d.ts +78 -0
- package/dist/core/mailbox-guards.js +58 -0
- package/dist/core/mailbox-index-codec.d.ts +41 -0
- package/dist/core/mailbox-index-codec.js +71 -0
- package/dist/core/mailbox-internals.d.ts +156 -0
- package/dist/core/mailbox-internals.js +154 -0
- package/dist/core/mailbox-maintenance.d.ts +29 -0
- package/dist/core/mailbox-maintenance.js +196 -0
- package/dist/core/mailbox-settlement.d.ts +69 -0
- package/dist/core/mailbox-settlement.js +206 -0
- package/dist/core/mailbox-storage.d.ts +117 -0
- package/dist/core/mailbox-storage.js +115 -0
- package/dist/core/mailbox-transition-helpers.d.ts +70 -0
- package/dist/core/mailbox-transition-helpers.js +43 -0
- package/dist/core/mailbox-transitions-recovery.d.ts +28 -0
- package/dist/core/mailbox-transitions-recovery.js +64 -0
- package/dist/core/mailbox-transitions.d.ts +138 -0
- package/dist/core/mailbox-transitions.js +180 -0
- package/dist/core/mailbox-types.d.ts +406 -0
- package/dist/core/mailbox-types.js +16 -0
- package/dist/core/mailbox-validation.d.ts +65 -0
- package/dist/core/mailbox-validation.js +73 -0
- package/dist/core/mailbox-waits.d.ts +50 -0
- package/dist/core/mailbox-waits.js +97 -0
- package/dist/core/mailbox.d.ts +163 -0
- package/dist/core/mailbox.js +211 -0
- package/dist/core/outbox-claims.d.ts +307 -0
- package/dist/core/outbox-claims.js +0 -0
- package/dist/core/outbox-codec.d.ts +24 -0
- package/dist/core/outbox-codec.js +257 -0
- package/dist/core/outbox-contract.d.ts +346 -0
- package/dist/core/outbox-contract.js +0 -0
- package/dist/core/outbox-delivery.d.ts +34 -0
- package/dist/core/outbox-delivery.js +149 -0
- package/dist/core/outbox-drain.d.ts +29 -0
- package/dist/core/outbox-drain.js +158 -0
- package/dist/core/outbox-enqueue.d.ts +24 -0
- package/dist/core/outbox-enqueue.js +115 -0
- package/dist/core/outbox-guards.d.ts +76 -0
- package/dist/core/outbox-guards.js +60 -0
- package/dist/core/outbox-index-codec.d.ts +33 -0
- package/dist/core/outbox-index-codec.js +71 -0
- package/dist/core/outbox-internals.d.ts +99 -0
- package/dist/core/outbox-internals.js +159 -0
- package/dist/core/outbox-listing.d.ts +12 -0
- package/dist/core/outbox-listing.js +38 -0
- package/dist/core/outbox-maintenance.d.ts +28 -0
- package/dist/core/outbox-maintenance.js +191 -0
- package/dist/core/outbox-operations.d.ts +18 -0
- package/dist/core/outbox-operations.js +46 -0
- package/dist/core/outbox-runner.d.ts +30 -0
- package/dist/core/outbox-runner.js +217 -0
- package/dist/core/outbox-settlement.d.ts +57 -0
- package/dist/core/outbox-settlement.js +183 -0
- package/dist/core/outbox-storage.d.ts +107 -0
- package/dist/core/outbox-storage.js +127 -0
- package/dist/core/outbox-transition-helpers.d.ts +83 -0
- package/dist/core/outbox-transition-helpers.js +63 -0
- package/dist/core/outbox-transitions-recovery.d.ts +30 -0
- package/dist/core/outbox-transitions-recovery.js +42 -0
- package/dist/core/outbox-transitions.d.ts +145 -0
- package/dist/core/outbox-transitions.js +236 -0
- package/dist/core/outbox-types.d.ts +400 -0
- package/dist/core/outbox-types.js +20 -0
- package/dist/core/outbox-validation.d.ts +101 -0
- package/dist/core/outbox-validation.js +127 -0
- package/dist/core/outbox-waits.d.ts +39 -0
- package/dist/core/outbox-waits.js +88 -0
- package/dist/core/outbox.d.ts +156 -0
- package/dist/core/outbox.js +234 -0
- package/dist/core/registry-limits.d.ts +49 -0
- package/dist/core/registry-limits.js +10 -0
- package/dist/core/registry-schema-conversion.d.ts +20 -0
- package/dist/core/registry-schema-conversion.js +22 -0
- package/dist/core/registry-snapshot.d.ts +91 -42
- package/dist/core/registry-snapshot.js +39 -77
- package/dist/core/registry-workflow-contract-draft.d.ts +32 -0
- package/dist/core/registry-workflow-contract-draft.js +28 -0
- package/dist/core/registry-workflow-manifest.d.ts +122 -0
- package/dist/core/registry-workflow-manifest.js +92 -0
- package/dist/core/source/errors.d.ts +55 -0
- package/dist/core/source/errors.js +13 -0
- package/dist/core/source/index.d.ts +19 -0
- package/dist/core/source/index.js +4 -0
- package/dist/core/source/resolvers.d.ts +45 -0
- package/dist/core/source/resolvers.js +9 -0
- package/dist/core/source/types.d.ts +211 -0
- package/dist/core/source/types.js +0 -0
- package/dist/core/source/validate.d.ts +91 -0
- package/dist/core/source/validate.js +62 -0
- package/dist/core/source/workflow-source.d.ts +43 -0
- package/dist/core/source/workflow-source.js +16 -0
- package/dist/core/types/workflow-registries.d.ts +7 -0
- package/dist/core/types/workflow-registry.d.ts +12 -0
- package/dist/core/versioning.js +2 -0
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +9 -1
- package/dist/http.js +2 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +50 -0
- package/dist/indexeddb.js +1 -1
- package/dist/json-schema.js +3 -3
- package/dist/mcp/resources.js +2 -0
- package/dist/runtime/portable.d.ts +9 -1
- package/dist/server/fault-to-json-rpc.js +3 -1
- package/dist/server/handler/auth-context-principal.js +4 -0
- package/dist/server/json-rpc-http.js +2 -0
- package/dist/server/json-rpc-websocket.js +6 -4
- package/dist/server/operation-catalog/index.d.ts +1 -1
- package/dist/server/operation-catalog/pipeline-helpers.js +2 -0
- package/dist/server/operation-catalog/types.d.ts +17 -0
- package/dist/server/operation-fault.d.ts +3 -0
- package/dist/server/operation-fault.js +3 -1
- package/dist/server/operations/activate-workflow-revision.d.ts +30 -0
- package/dist/server/operations/activate-workflow-revision.js +69 -0
- package/dist/server/operations/fleet-events-subscription.js +1 -1
- package/dist/server/operations/fork-workflow.js +1 -1
- package/dist/server/operations/get-active-workflow-revision.d.ts +32 -0
- package/dist/server/operations/get-active-workflow-revision.js +46 -0
- package/dist/server/operations/get-catalog-diagnostics.d.ts +85 -0
- package/dist/server/operations/get-catalog-diagnostics.js +57 -0
- package/dist/server/operations/get-registry.js +17 -4
- package/dist/server/operations/get-workflow-revision.d.ts +25 -0
- package/dist/server/operations/get-workflow-revision.js +50 -0
- package/dist/server/operations/install-workflow-revision.d.ts +27 -0
- package/dist/server/operations/install-workflow-revision.js +46 -0
- package/dist/server/operations/list-workflow-revisions.d.ts +20 -0
- package/dist/server/operations/list-workflow-revisions.js +40 -0
- package/dist/server/operations/start-or-signal-workflow.js +1 -1
- package/dist/server/operations/start-workflow.js +1 -1
- package/dist/server/operations/static-registrations.js +36 -0
- package/dist/server/operations/storage.js +2 -2
- package/dist/server/operations/submit-review-decision.js +1 -1
- package/dist/server/operations/workflow-catalog-operation-helpers.d.ts +59 -0
- package/dist/server/operations/workflow-catalog-operation-helpers.js +135 -0
- package/dist/server/runtime/websocket-upgrade.js +2 -0
- package/dist/storage/bun-sql.js +49 -6
- package/dist/storage/catalog-keys.d.ts +45 -0
- package/dist/storage/catalog-keys.js +6 -0
- package/dist/storage/compressed-storage.js +1 -1
- package/dist/storage/derived-operations.js +1 -1
- package/dist/storage/index.d.ts +26 -1
- package/dist/storage/interface.d.ts +26 -1
- package/dist/storage/interface.js +1 -1
- package/dist/storage/key-encoding.d.ts +15 -0
- package/dist/storage/key-encoding.js +3 -0
- package/dist/storage/key-prefixes.d.ts +1 -1
- package/dist/storage/key-prefixes.js +15 -0
- package/dist/storage/lmdb.d.ts +26 -1
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/mailbox-keys.d.ts +80 -0
- package/dist/storage/mailbox-keys.js +14 -0
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon.js +2 -2
- package/dist/storage/node-sqlite-loader.d.ts +1 -1
- package/dist/storage/node-sqlite.js +49 -6
- package/dist/storage/outbox-keys.d.ts +77 -0
- package/dist/storage/outbox-keys.js +14 -0
- package/dist/storage/postgres.js +2 -2
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/storage-configuration.d.ts +6 -0
- package/dist/storage/testing.js +1 -1
- package/dist/storage/turso.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/web-extension.js +1 -1
- package/dist/worker/manifest/registry-contract-builder.d.ts +21 -3
- package/dist/worker/manifest/registry-contract-builder.js +40 -31
- package/package.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { utf8ByteLength } from "../../worker/manifest/utf8.js";
|
|
2
|
+
import { MAX_CONTRACT_IDENTIFIER_BYTES } from "../contract/limits.js";
|
|
3
|
+
import { validateWorkflowOrActivityName } from "../types/name-grammar.js";
|
|
4
|
+
function assertWellFormedSourceDescriptor(source) {
|
|
5
|
+
const { kind, name, revision } = source.descriptor;
|
|
6
|
+
if (typeof kind !== "string" || kind.length === 0)
|
|
7
|
+
throw TypeError("registerSource(): source.descriptor.kind must be a non-empty string");
|
|
8
|
+
if (typeof name !== "string" || name.length === 0)
|
|
9
|
+
throw TypeError("registerSource(): source.descriptor.name must be a non-empty string");
|
|
10
|
+
if (typeof revision !== "string" || revision.length === 0)
|
|
11
|
+
throw TypeError("registerSource(): source.descriptor.revision must be a non-empty string");
|
|
12
|
+
const revisionBytes = utf8ByteLength(revision);
|
|
13
|
+
if (revisionBytes > MAX_CONTRACT_IDENTIFIER_BYTES)
|
|
14
|
+
throw TypeError(`registerSource(): source.descriptor.revision is ${revisionBytes} bytes, exceeding the maximum identifier size of ${MAX_CONTRACT_IDENTIFIER_BYTES}`);
|
|
15
|
+
}
|
|
16
|
+
export function registerSource(internals, source) {
|
|
17
|
+
assertWellFormedSourceDescriptor(source);
|
|
18
|
+
const { name, revision } = source.descriptor;
|
|
19
|
+
validateWorkflowOrActivityName(name, "workflow");
|
|
20
|
+
Object.freeze(source.descriptor);
|
|
21
|
+
if (internals.registrations.has(name))
|
|
22
|
+
throw Error(`Cannot registerSource("${name}"): "${name}" is already registered as an eager workflow via engine.register(). A workflow name may not be both eagerly registered and a dynamic source.`);
|
|
23
|
+
let byRevision = internals.workflowSourcesByName.get(name);
|
|
24
|
+
if (byRevision === void 0) {
|
|
25
|
+
byRevision = new Map;
|
|
26
|
+
internals.workflowSourcesByName.set(name, byRevision);
|
|
27
|
+
}
|
|
28
|
+
const existing = byRevision.get(revision);
|
|
29
|
+
if (existing === void 0) {
|
|
30
|
+
byRevision.set(revision, source);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (existing === source)
|
|
34
|
+
return;
|
|
35
|
+
throw Error(`Cannot registerSource("${name}", "${revision}"): a different source handle is already registered for this exact (name, revision) key.`);
|
|
36
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `resolveWorkflowSource()` — the async, single-flight entry point that
|
|
3
|
+
* loads, validates, and installs one dynamic workflow source revision
|
|
4
|
+
* (WFT-13/14).
|
|
5
|
+
*
|
|
6
|
+
* Single-flight per `(name, revision)`: the underlying load+validate+install
|
|
7
|
+
* work is shared by every concurrent caller for the same key via
|
|
8
|
+
* `internals.sourceResolutionsInFlight`, but each caller races that shared
|
|
9
|
+
* work against its OWN per-call cancellation interest — a cancelled waiter
|
|
10
|
+
* never aborts a load another waiter still needs, and the shared load
|
|
11
|
+
* itself is never tied to any individual caller's lifetime. Disposal aborts
|
|
12
|
+
* every outstanding waiter (rejecting each pending `resolveWorkflowSource()`
|
|
13
|
+
* call) without touching the shared load, which keeps running to whatever
|
|
14
|
+
* point it naturally settles.
|
|
15
|
+
*
|
|
16
|
+
* @module core/engine/source-resolution
|
|
17
|
+
*/
|
|
18
|
+
import type { WorkflowRevisionRecord } from '../catalog/index.ts';
|
|
19
|
+
import type { Engine } from './index.ts';
|
|
20
|
+
/**
|
|
21
|
+
* Options accepted by {@link resolveWorkflowSource}.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { Engine } from '@lostgradient/weft';
|
|
26
|
+
* import type { ResolveWorkflowSourceOptions } from '@lostgradient/weft';
|
|
27
|
+
*
|
|
28
|
+
* declare const engine: Engine;
|
|
29
|
+
* const options: ResolveWorkflowSourceOptions = { signal: AbortSignal.timeout(5_000) };
|
|
30
|
+
* const record = await engine.resolveWorkflowSource('checkout', 'r1', options);
|
|
31
|
+
* console.log(record.manifest.revision);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export type ResolveWorkflowSourceOptions = {
|
|
35
|
+
/** When aborted, this caller's own `resolveWorkflowSource()` call rejects; a load already in flight for other callers is unaffected. */
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Load, validate, and install one dynamic workflow source revision
|
|
40
|
+
* previously recorded via `engine.registerSource()`. Returns the installed
|
|
41
|
+
* {@link WorkflowRevisionRecord} immediately, without invoking the loader
|
|
42
|
+
* at all, when `(name, revision)` is already durably installed — including
|
|
43
|
+
* a revision installed by a different process. When that is the case,
|
|
44
|
+
* `internals.resolvedWorkflowSources` is NOT populated for this key (there
|
|
45
|
+
* is no locally-loaded `WorkflowDefinition` to stash — only the durable
|
|
46
|
+
* manifest was ever read) even though the catalog itself considers the
|
|
47
|
+
* revision installed; a later batch reading `resolvedWorkflowSources` must
|
|
48
|
+
* account for that gap rather than assuming every installed revision has a
|
|
49
|
+
* live definition available in this process. This fast path still requires
|
|
50
|
+
* `registerSource()` to have been called for this exact key first (see
|
|
51
|
+
* {@link resolveCachedOrHandle}), and still validates a pinned
|
|
52
|
+
* `workflowVersion`/`contractHash` against the cached manifest — the ONLY
|
|
53
|
+
* thing it skips is re-invoking the loader and re-running
|
|
54
|
+
* {@link import('../source/index.ts').validateResolvedWorkflowSource}.
|
|
55
|
+
*
|
|
56
|
+
* Single-flight per `(name, revision)` — see the module doc for the full
|
|
57
|
+
* cancellation contract. Throws a plain `Error` when `registerSource()` was
|
|
58
|
+
* never called for this exact key (a programmer error, not untrusted-input
|
|
59
|
+
* rejection); throws {@link WorkflowSourceValidationError} when the loaded
|
|
60
|
+
* module fails validation, OR when a cached manifest contradicts a pinned
|
|
61
|
+
* `workflowVersion`/`contractHash`; throws {@link EngineDisposedError} when
|
|
62
|
+
* the engine is disposed, either already or during the call; propagates
|
|
63
|
+
* {@link import('../catalog/index.ts').WorkflowCatalogConflictError}
|
|
64
|
+
* unwrapped on a genuine content conflict with an already-installed
|
|
65
|
+
* revision.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* import { Engine, workflowSource } from '@lostgradient/weft';
|
|
70
|
+
*
|
|
71
|
+
* declare const engine: Engine;
|
|
72
|
+
* declare const loadCheckout: () => Promise<{
|
|
73
|
+
* checkout: import('@lostgradient/weft').WorkflowDefinition<unknown, unknown, 'checkout'>;
|
|
74
|
+
* }>;
|
|
75
|
+
* engine.registerSource(
|
|
76
|
+
* workflowSource(
|
|
77
|
+
* { name: 'checkout', location: './checkout.ts', exportName: 'checkout', revision: 'r1' },
|
|
78
|
+
* loadCheckout,
|
|
79
|
+
* ),
|
|
80
|
+
* );
|
|
81
|
+
* const record = await engine.resolveWorkflowSource('checkout', 'r1');
|
|
82
|
+
* console.log(record.manifest.revision);
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export declare function resolveWorkflowSource(engine: Engine, name: string, revision: string, options?: ResolveWorkflowSourceOptions): Promise<WorkflowRevisionRecord>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkWorkflowCompatibility,
|
|
3
|
+
DEFAULT_WORKFLOW_COMPATIBILITY_POLICY
|
|
4
|
+
} from "../contract/compatibility.js";
|
|
5
|
+
import {
|
|
6
|
+
buildExpectedManifest,
|
|
7
|
+
resolveSourceModule,
|
|
8
|
+
validateResolvedWorkflowSource,
|
|
9
|
+
WorkflowSourceValidationError
|
|
10
|
+
} from "../source/index.js";
|
|
11
|
+
import { ensureWorkflowCatalogReady, isWorkflowCatalogReady } from "./catalog-readiness.js";
|
|
12
|
+
import { EngineDisposedError } from "./errors.js";
|
|
13
|
+
import { getInternals, getWorkflowCatalog } from "./internals.js";
|
|
14
|
+
function toAbortError(signal) {
|
|
15
|
+
const reason = signal.reason;
|
|
16
|
+
return reason instanceof Error ? reason : Error("resolveWorkflowSource() aborted");
|
|
17
|
+
}
|
|
18
|
+
function abortRejection(signal) {
|
|
19
|
+
return new Promise((_resolve, reject) => {
|
|
20
|
+
signal.addEventListener("abort", () => reject(toAbortError(signal)), { once: !0 });
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async function runSharedSourceLoad(engine, internals, name, revision, handle) {
|
|
24
|
+
const resolved = await resolveSourceModule(handle.descriptor, handle);
|
|
25
|
+
if (!resolved.ok)
|
|
26
|
+
throw new WorkflowSourceValidationError(name, revision, [resolved.reason]);
|
|
27
|
+
const outcome = await validateResolvedWorkflowSource(handle.descriptor, resolved.moduleValue);
|
|
28
|
+
if (!outcome.ok)
|
|
29
|
+
throw new WorkflowSourceValidationError(name, revision, outcome.reasons);
|
|
30
|
+
if (internals.disposed)
|
|
31
|
+
throw new EngineDisposedError;
|
|
32
|
+
const installed = await getWorkflowCatalog(engine).install(outcome.manifest, outcome.definition);
|
|
33
|
+
if (!internals.disposed) {
|
|
34
|
+
let resolvedByRevision = internals.resolvedWorkflowSources.get(name);
|
|
35
|
+
if (resolvedByRevision === void 0) {
|
|
36
|
+
resolvedByRevision = new Map;
|
|
37
|
+
internals.resolvedWorkflowSources.set(name, resolvedByRevision);
|
|
38
|
+
}
|
|
39
|
+
resolvedByRevision.set(revision, {
|
|
40
|
+
definition: outcome.loadedDefinition,
|
|
41
|
+
activityRegistry: outcome.activityRegistry
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return { manifest: installed.manifest, installedAt: installed.installedAt };
|
|
45
|
+
}
|
|
46
|
+
function getOrCreateSharedSourceLoad(engine, internals, name, revision, handle) {
|
|
47
|
+
let byRevision = internals.sourceResolutionsInFlight.get(name);
|
|
48
|
+
if (byRevision === void 0) {
|
|
49
|
+
byRevision = new Map;
|
|
50
|
+
internals.sourceResolutionsInFlight.set(name, byRevision);
|
|
51
|
+
}
|
|
52
|
+
const existing = byRevision.get(revision);
|
|
53
|
+
if (existing !== void 0)
|
|
54
|
+
return existing;
|
|
55
|
+
const shared = runSharedSourceLoad(engine, internals, name, revision, handle).finally(() => {
|
|
56
|
+
const currentByRevision = internals.sourceResolutionsInFlight.get(name);
|
|
57
|
+
if (currentByRevision?.get(revision) !== shared)
|
|
58
|
+
return;
|
|
59
|
+
currentByRevision.delete(revision);
|
|
60
|
+
if (currentByRevision.size === 0)
|
|
61
|
+
internals.sourceResolutionsInFlight.delete(name);
|
|
62
|
+
});
|
|
63
|
+
shared.catch(() => {});
|
|
64
|
+
byRevision.set(revision, shared);
|
|
65
|
+
return shared;
|
|
66
|
+
}
|
|
67
|
+
function createWaiterAbort(options) {
|
|
68
|
+
const controller = new AbortController, forwardAbort = () => controller.abort(options?.signal?.reason);
|
|
69
|
+
if (options?.signal?.aborted)
|
|
70
|
+
forwardAbort();
|
|
71
|
+
else
|
|
72
|
+
options?.signal?.addEventListener("abort", forwardAbort, { once: !0 });
|
|
73
|
+
return { controller, detach: () => options?.signal?.removeEventListener("abort", forwardAbort) };
|
|
74
|
+
}
|
|
75
|
+
async function resolveCachedOrHandle(engine, internals, name, revision) {
|
|
76
|
+
const handle = internals.workflowSourcesByName.get(name)?.get(revision);
|
|
77
|
+
if (handle === void 0)
|
|
78
|
+
throw Error(`resolveWorkflowSource("${name}", "${revision}") was called before registerSource() ` + "registered this exact (name, revision) \u2014 call engine.registerSource() first.");
|
|
79
|
+
if (!isWorkflowCatalogReady(engine))
|
|
80
|
+
await ensureWorkflowCatalogReady(engine);
|
|
81
|
+
const existing = await getWorkflowCatalog(engine).resolveEntry(name, revision);
|
|
82
|
+
if (existing !== void 0) {
|
|
83
|
+
const expectedManifest = buildExpectedManifest(handle.descriptor, existing.manifest), verdict = checkWorkflowCompatibility(expectedManifest, existing.manifest, DEFAULT_WORKFLOW_COMPATIBILITY_POLICY);
|
|
84
|
+
if (!verdict.compatible)
|
|
85
|
+
throw new WorkflowSourceValidationError(name, revision, verdict.reasons);
|
|
86
|
+
return { cached: existing };
|
|
87
|
+
}
|
|
88
|
+
return { handle };
|
|
89
|
+
}
|
|
90
|
+
export async function resolveWorkflowSource(engine, name, revision, options) {
|
|
91
|
+
const internals = getInternals(engine), waiter = createWaiterAbort(options);
|
|
92
|
+
if (waiter.controller.signal.aborted) {
|
|
93
|
+
waiter.detach();
|
|
94
|
+
throw toAbortError(waiter.controller.signal);
|
|
95
|
+
}
|
|
96
|
+
if (internals.disposed) {
|
|
97
|
+
waiter.detach();
|
|
98
|
+
throw new EngineDisposedError;
|
|
99
|
+
}
|
|
100
|
+
internals.sourceResolutionWaiterControllers.add(waiter.controller);
|
|
101
|
+
try {
|
|
102
|
+
const cachedOrHandle = resolveCachedOrHandle(engine, internals, name, revision), cachedOrHandleAbort = abortRejection(waiter.controller.signal);
|
|
103
|
+
cachedOrHandleAbort.catch(() => {});
|
|
104
|
+
let outcome;
|
|
105
|
+
try {
|
|
106
|
+
outcome = await Promise.race([cachedOrHandle, cachedOrHandleAbort]);
|
|
107
|
+
} finally {
|
|
108
|
+
cachedOrHandle.catch(() => {});
|
|
109
|
+
}
|
|
110
|
+
if (outcome.cached !== void 0)
|
|
111
|
+
return outcome.cached;
|
|
112
|
+
const shared = getOrCreateSharedSourceLoad(engine, internals, name, revision, outcome.handle), waiterAbort = abortRejection(waiter.controller.signal);
|
|
113
|
+
waiterAbort.catch(() => {});
|
|
114
|
+
return await Promise.race([shared, waiterAbort]);
|
|
115
|
+
} finally {
|
|
116
|
+
waiter.detach();
|
|
117
|
+
internals.sourceResolutionWaiterControllers.delete(waiter.controller);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -121,7 +121,7 @@ export function decodeWorkflowState(bytes) {
|
|
|
121
121
|
liftFlatVersionTuple(decodedRecord);
|
|
122
122
|
const state = decoded;
|
|
123
123
|
if (!isValidDecodedTags(state.tags)) {
|
|
124
|
-
console.warn(`[weft] Decoded workflow state for "${
|
|
124
|
+
console.warn(`[weft] Decoded workflow state for "${state.id}" has invalid tags; dropping the malformed tag list from the decoded state.`);
|
|
125
125
|
delete state.tags;
|
|
126
126
|
}
|
|
127
127
|
if (state.failureCategory !== void 0 && state.failureCategory !== null) {
|
|
@@ -135,7 +135,7 @@ export function decodeWorkflowState(bytes) {
|
|
|
135
135
|
try {
|
|
136
136
|
coerceStartWorkflowId(state.executionStateOwnerId, "executionStateOwnerId");
|
|
137
137
|
} catch {
|
|
138
|
-
console.warn(`[weft] Decoded workflow state for "${
|
|
138
|
+
console.warn(`[weft] Decoded workflow state for "${state.id}" has an invalid executionStateOwnerId field; falling back to the workflow id as the execution owner. This usually indicates corruption or tampering of the storage record.`);
|
|
139
139
|
delete state.executionStateOwnerId;
|
|
140
140
|
}
|
|
141
141
|
sanitizeDecodedParentLineage(state);
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { WorkflowCompatibilityReason } from '../contract/compatibility.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Fired on the {@link Engine} when a `(name, revision)` is durably installed
|
|
4
|
+
* into the workflow catalog for the first time. Never fired for a
|
|
5
|
+
* byte-identical reinstall of an already-installed revision. Read
|
|
6
|
+
* `e.workflowType` and `e.revision` directly off the event.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { Engine, WorkflowRevisionInstalledEvent, workflow } from '@lostgradient/weft';
|
|
11
|
+
*
|
|
12
|
+
* const engine = new Engine();
|
|
13
|
+
* engine.addEventListener(WorkflowRevisionInstalledEvent.type, (event) => {
|
|
14
|
+
* console.log('installed:', event.workflowType, event.revision);
|
|
15
|
+
* });
|
|
16
|
+
* engine.register(workflow({ name: 'ping' }).execute(async function* () { return 'pong'; }));
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare class WorkflowRevisionInstalledEvent extends Event {
|
|
20
|
+
static readonly type: "catalog:revision-installed";
|
|
21
|
+
readonly workflowType: string;
|
|
22
|
+
readonly revision: string;
|
|
23
|
+
constructor(workflowType: string, revision: string);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Fired on the {@link Engine} when a `(name, revision)` becomes the active
|
|
27
|
+
* revision for its name — bumping the catalog's per-name `generation`
|
|
28
|
+
* counter. `previousRevision` is `undefined` on a name's first-ever
|
|
29
|
+
* activation, and set to the revision this activation replaced otherwise.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { Engine, WorkflowRevisionActivatedEvent, workflow } from '@lostgradient/weft';
|
|
34
|
+
*
|
|
35
|
+
* const engine = new Engine();
|
|
36
|
+
* engine.addEventListener(WorkflowRevisionActivatedEvent.type, (event) => {
|
|
37
|
+
* console.log('activated:', event.workflowType, event.revision, 'gen', event.generation);
|
|
38
|
+
* });
|
|
39
|
+
* engine.register(workflow({ name: 'ping' }).execute(async function* () { return 'pong'; }));
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare class WorkflowRevisionActivatedEvent extends Event {
|
|
43
|
+
static readonly type: "catalog:revision-activated";
|
|
44
|
+
readonly workflowType: string;
|
|
45
|
+
readonly revision: string;
|
|
46
|
+
readonly generation: number;
|
|
47
|
+
readonly previousRevision: string | undefined;
|
|
48
|
+
constructor(workflowType: string, revision: string, generation: number, previousRevision: string | undefined);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Fired on the {@link Engine} when a candidate activation
|
|
52
|
+
* (`activateCatalogRevisionCandidate`, the guarded primitive) is refused.
|
|
53
|
+
* `reason` is a closed, low-cardinality union safe to use as a metric label;
|
|
54
|
+
* `incompatibilityReasons` is populated only when `reason === 'incompatible'`,
|
|
55
|
+
* carrying every applicable {@link WorkflowCompatibilityReason} — never the
|
|
56
|
+
* full compatibility verdict object.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { Engine, WorkflowRevisionActivationRejectedEvent } from '@lostgradient/weft';
|
|
61
|
+
*
|
|
62
|
+
* const engine = new Engine();
|
|
63
|
+
* engine.addEventListener(WorkflowRevisionActivationRejectedEvent.type, (event) => {
|
|
64
|
+
* console.log('rejected:', event.workflowType, event.candidateRevision, event.reason);
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare class WorkflowRevisionActivationRejectedEvent extends Event {
|
|
69
|
+
static readonly type: "catalog:activation-rejected";
|
|
70
|
+
readonly workflowType: string;
|
|
71
|
+
readonly candidateRevision: string;
|
|
72
|
+
readonly reason: 'incompatible' | 'stale-generation' | 'conflict' | 'expected-generation-required';
|
|
73
|
+
readonly incompatibilityReasons: readonly WorkflowCompatibilityReason[] | undefined;
|
|
74
|
+
constructor(workflowType: string, candidateRevision: string, reason: 'incompatible' | 'stale-generation' | 'conflict' | 'expected-generation-required', incompatibilityReasons?: readonly WorkflowCompatibilityReason[]);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Fired on the {@link Engine} alongside {@link WorkflowRevisionActivatedEvent}
|
|
78
|
+
* when activating a new revision displaces a previously active one for the
|
|
79
|
+
* same name. Never fired on a name's first-ever activation (there is
|
|
80
|
+
* nothing to drain).
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* import { Engine, WorkflowRevisionDrainingEvent } from '@lostgradient/weft';
|
|
85
|
+
*
|
|
86
|
+
* const engine = new Engine();
|
|
87
|
+
* engine.addEventListener(WorkflowRevisionDrainingEvent.type, (event) => {
|
|
88
|
+
* console.log('draining:', event.workflowType, event.revision);
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare class WorkflowRevisionDrainingEvent extends Event {
|
|
93
|
+
static readonly type: "catalog:revision-draining";
|
|
94
|
+
readonly workflowType: string;
|
|
95
|
+
readonly revision: string;
|
|
96
|
+
constructor(workflowType: string, revision: string);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Fired on the {@link Engine} when a `(name, revision)` is durably removed
|
|
100
|
+
* from the workflow catalog via `removeWorkflowRevision`.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* import { Engine, WorkflowRevisionRemovedEvent } from '@lostgradient/weft';
|
|
105
|
+
*
|
|
106
|
+
* const engine = new Engine();
|
|
107
|
+
* engine.addEventListener(WorkflowRevisionRemovedEvent.type, (event) => {
|
|
108
|
+
* console.log('removed:', event.workflowType, event.revision);
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export declare class WorkflowRevisionRemovedEvent extends Event {
|
|
113
|
+
static readonly type: "catalog:revision-removed";
|
|
114
|
+
readonly workflowType: string;
|
|
115
|
+
readonly revision: string;
|
|
116
|
+
constructor(workflowType: string, revision: string);
|
|
117
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export class WorkflowRevisionInstalledEvent extends Event {
|
|
2
|
+
static type = "catalog:revision-installed";
|
|
3
|
+
workflowType;
|
|
4
|
+
revision;
|
|
5
|
+
constructor(workflowType, revision) {
|
|
6
|
+
super(WorkflowRevisionInstalledEvent.type);
|
|
7
|
+
this.workflowType = workflowType;
|
|
8
|
+
this.revision = revision;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class WorkflowRevisionActivatedEvent extends Event {
|
|
13
|
+
static type = "catalog:revision-activated";
|
|
14
|
+
workflowType;
|
|
15
|
+
revision;
|
|
16
|
+
generation;
|
|
17
|
+
previousRevision;
|
|
18
|
+
constructor(workflowType, revision, generation, previousRevision) {
|
|
19
|
+
super(WorkflowRevisionActivatedEvent.type);
|
|
20
|
+
this.workflowType = workflowType;
|
|
21
|
+
this.revision = revision;
|
|
22
|
+
this.generation = generation;
|
|
23
|
+
this.previousRevision = previousRevision;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class WorkflowRevisionActivationRejectedEvent extends Event {
|
|
28
|
+
static type = "catalog:activation-rejected";
|
|
29
|
+
workflowType;
|
|
30
|
+
candidateRevision;
|
|
31
|
+
reason;
|
|
32
|
+
incompatibilityReasons;
|
|
33
|
+
constructor(workflowType, candidateRevision, reason, incompatibilityReasons) {
|
|
34
|
+
super(WorkflowRevisionActivationRejectedEvent.type);
|
|
35
|
+
this.workflowType = workflowType;
|
|
36
|
+
this.candidateRevision = candidateRevision;
|
|
37
|
+
this.reason = reason;
|
|
38
|
+
this.incompatibilityReasons = incompatibilityReasons;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class WorkflowRevisionDrainingEvent extends Event {
|
|
43
|
+
static type = "catalog:revision-draining";
|
|
44
|
+
workflowType;
|
|
45
|
+
revision;
|
|
46
|
+
constructor(workflowType, revision) {
|
|
47
|
+
super(WorkflowRevisionDrainingEvent.type);
|
|
48
|
+
this.workflowType = workflowType;
|
|
49
|
+
this.revision = revision;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class WorkflowRevisionRemovedEvent extends Event {
|
|
54
|
+
static type = "catalog:revision-removed";
|
|
55
|
+
workflowType;
|
|
56
|
+
revision;
|
|
57
|
+
constructor(workflowType, revision) {
|
|
58
|
+
super(WorkflowRevisionRemovedEvent.type);
|
|
59
|
+
this.workflowType = workflowType;
|
|
60
|
+
this.revision = revision;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReviewCompletedEvent, ReviewRequestedEvent } from '../review/events.ts';
|
|
2
2
|
import type { ActivityAsyncPendingEvent, ActivityCompletedEvent, ActivityFailedEvent, ActivityStartedEvent, TaskResultDeadLetteredEvent } from './activity-events.ts';
|
|
3
3
|
import type { AttributesChangedEvent } from './attribute-events.ts';
|
|
4
|
+
import type { WorkflowRevisionActivatedEvent, WorkflowRevisionActivationRejectedEvent, WorkflowRevisionDrainingEvent, WorkflowRevisionInstalledEvent, WorkflowRevisionRemovedEvent } from './catalog-events.ts';
|
|
4
5
|
import type { ScheduleFiredEvent, ScheduleMissedFireEvent } from './schedule-events.ts';
|
|
5
6
|
import type { SignalDeliveredEvent, SignalReceivedEvent } from './signal-events.ts';
|
|
6
7
|
import type { AlertFiredEvent, AlertResolvedEvent, CheckpointSizeWarningEvent, CleanupWarningEvent, ConstraintViolatedEvent, DevelopmentWarningEvent, StorageSizeReportedEvent, WorkerConnectedEvent, WorkerDisconnectedEvent } from './system-events.ts';
|
|
@@ -57,6 +58,11 @@ export type WeftEventMap = {
|
|
|
57
58
|
'alert:fired': AlertFiredEvent;
|
|
58
59
|
'alert:resolved': AlertResolvedEvent;
|
|
59
60
|
'constraint:violated': ConstraintViolatedEvent;
|
|
61
|
+
'catalog:revision-installed': WorkflowRevisionInstalledEvent;
|
|
62
|
+
'catalog:revision-activated': WorkflowRevisionActivatedEvent;
|
|
63
|
+
'catalog:activation-rejected': WorkflowRevisionActivationRejectedEvent;
|
|
64
|
+
'catalog:revision-draining': WorkflowRevisionDrainingEvent;
|
|
65
|
+
'catalog:revision-removed': WorkflowRevisionRemovedEvent;
|
|
60
66
|
};
|
|
61
67
|
/**
|
|
62
68
|
* Typed version of the `EventTarget` interface that constrains
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admission for the application command mailbox (WFT-84): validating an offered
|
|
3
|
+
* command, resolving its idempotency identity, enforcing backlog capacity, and
|
|
4
|
+
* committing the record with its indexes and fleet event.
|
|
5
|
+
*
|
|
6
|
+
* Admission is the one transition that also allocates: it advances the mailbox
|
|
7
|
+
* header's FIFO sequence and open count in the same conditional batch as the
|
|
8
|
+
* record itself, so backlog accounting can never drift from the records it
|
|
9
|
+
* describes.
|
|
10
|
+
*
|
|
11
|
+
* @module core/mailbox-admission
|
|
12
|
+
*/
|
|
13
|
+
import type { ApplicationCommandAdmission, ApplicationCommandInput, MailboxCapacity } from './mailbox-contract.ts';
|
|
14
|
+
import { type MailboxRuntime } from './mailbox-internals.ts';
|
|
15
|
+
/** Backlog accounting, shared by admission rejection and the public `capacity()`. */
|
|
16
|
+
export declare function capacityOf(runtime: MailboxRuntime, open: number, admitted: number): MailboxCapacity;
|
|
17
|
+
/**
|
|
18
|
+
* Offer a command to the mailbox.
|
|
19
|
+
*
|
|
20
|
+
* An exact retry of the same idempotency identity returns the original
|
|
21
|
+
* receipt without creating a second command. Reusing the key with a different
|
|
22
|
+
* caller, target, kind, or payload digest returns a conflict and leaves the
|
|
23
|
+
* original command untouched. A full backlog is rejected before anything is
|
|
24
|
+
* persisted.
|
|
25
|
+
*/
|
|
26
|
+
export declare function admitCommand(runtime: MailboxRuntime, command: ApplicationCommandInput): Promise<ApplicationCommandAdmission>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { computeIdentityDigest } from "./application-payload-digest.js";
|
|
2
|
+
import {
|
|
3
|
+
encodeApplicationCommandIdempotencyRecord,
|
|
4
|
+
encodeApplicationReadyEntry
|
|
5
|
+
} from "./mailbox-index-codec.js";
|
|
6
|
+
import {
|
|
7
|
+
describeCommandTransition,
|
|
8
|
+
MailboxContentionError,
|
|
9
|
+
MAX_MAILBOX_TRANSITION_ATTEMPTS,
|
|
10
|
+
toApplicationCommandReceipt
|
|
11
|
+
} from "./mailbox-internals.js";
|
|
12
|
+
import {
|
|
13
|
+
commitMailboxTransition,
|
|
14
|
+
headerOperation,
|
|
15
|
+
loadCommand,
|
|
16
|
+
loadIdempotencyBinding,
|
|
17
|
+
loadMailboxHeader,
|
|
18
|
+
planCommandTransition
|
|
19
|
+
} from "./mailbox-storage.js";
|
|
20
|
+
import { createAdmittedCommandRecord } from "./mailbox-transitions.js";
|
|
21
|
+
import { MAILBOX_RECORD_VERSION } from "./mailbox-types.js";
|
|
22
|
+
import {
|
|
23
|
+
ApplicationCommandValidationError,
|
|
24
|
+
requireGeneratedIdentifier,
|
|
25
|
+
validateCommandInput
|
|
26
|
+
} from "./mailbox-validation.js";
|
|
27
|
+
import { PersistedDataCorruptError } from "./persisted-data-incompatible-error.js";
|
|
28
|
+
export function capacityOf(runtime, open, admitted) {
|
|
29
|
+
const limit = runtime.policy.maxBacklog;
|
|
30
|
+
return Object.freeze({ open, limit, remaining: Math.max(0, limit - open), admitted });
|
|
31
|
+
}
|
|
32
|
+
export async function admitCommand(runtime, command) {
|
|
33
|
+
const input = await validateCommandInput(command, runtime.policy), identityDigest = await computeIdentityDigest([
|
|
34
|
+
input.caller,
|
|
35
|
+
input.target,
|
|
36
|
+
input.kind,
|
|
37
|
+
input.payloadDigest
|
|
38
|
+
]);
|
|
39
|
+
for (let attempt = 1;attempt <= MAX_MAILBOX_TRANSITION_ATTEMPTS; attempt += 1) {
|
|
40
|
+
let expectedBinding = null;
|
|
41
|
+
if (input.idempotencyKey !== void 0) {
|
|
42
|
+
const resolved = await resolveIdempotency(runtime, input.idempotencyKey, identityDigest);
|
|
43
|
+
if (resolved.admission !== null)
|
|
44
|
+
return resolved.admission;
|
|
45
|
+
expectedBinding = resolved.staleBindingBytes;
|
|
46
|
+
}
|
|
47
|
+
const header = await loadMailboxHeader(runtime.storage, runtime.keys, runtime.policy.namespace, runtime.policy.resourceId);
|
|
48
|
+
if (header.record.openCount >= runtime.policy.maxBacklog)
|
|
49
|
+
return {
|
|
50
|
+
status: "rejected",
|
|
51
|
+
reason: "backlog-full",
|
|
52
|
+
capacity: capacityOf(runtime, header.record.openCount, header.record.admittedCount)
|
|
53
|
+
};
|
|
54
|
+
if (header.record.nextSequence >= Number.MAX_SAFE_INTEGER)
|
|
55
|
+
throw new ApplicationCommandValidationError("This mailbox has exhausted its FIFO sequence allocator; no further commands can be admitted under this namespace and resource id.");
|
|
56
|
+
const now = runtime.now(), record = createAdmittedCommandRecord(input, {
|
|
57
|
+
namespace: runtime.policy.namespace,
|
|
58
|
+
resourceId: runtime.policy.resourceId,
|
|
59
|
+
commandId: requireGeneratedIdentifier(runtime.generateId(), "commandId"),
|
|
60
|
+
sequence: header.record.nextSequence,
|
|
61
|
+
now
|
|
62
|
+
});
|
|
63
|
+
if (!await commitMailboxTransition(runtime.storage, runtime.events, planCommandTransition(runtime.keys, {
|
|
64
|
+
previous: null,
|
|
65
|
+
expectedBytes: null,
|
|
66
|
+
next: record,
|
|
67
|
+
event: describeCommandTransition(null, record),
|
|
68
|
+
now,
|
|
69
|
+
extraConditions: [
|
|
70
|
+
{ key: runtime.keys.header, expectedValue: header.bytes },
|
|
71
|
+
...input.idempotencyKey === void 0 ? [] : [
|
|
72
|
+
{
|
|
73
|
+
key: runtime.keys.idempotency(input.idempotencyKey),
|
|
74
|
+
expectedValue: expectedBinding
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
],
|
|
78
|
+
extraOperations: [
|
|
79
|
+
{
|
|
80
|
+
type: "put",
|
|
81
|
+
key: runtime.keys.bySequence(record.sequence),
|
|
82
|
+
value: encodeApplicationReadyEntry(record.commandId)
|
|
83
|
+
},
|
|
84
|
+
headerOperation(runtime.keys, {
|
|
85
|
+
...header.record,
|
|
86
|
+
nextSequence: header.record.nextSequence + 1,
|
|
87
|
+
openCount: header.record.openCount + 1,
|
|
88
|
+
admittedCount: header.record.admittedCount + 1
|
|
89
|
+
}),
|
|
90
|
+
...input.idempotencyKey === void 0 ? [] : [
|
|
91
|
+
{
|
|
92
|
+
type: "put",
|
|
93
|
+
key: runtime.keys.idempotency(input.idempotencyKey),
|
|
94
|
+
value: encodeApplicationCommandIdempotencyRecord({
|
|
95
|
+
recordVersion: MAILBOX_RECORD_VERSION,
|
|
96
|
+
commandId: record.commandId,
|
|
97
|
+
identityDigest
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
]
|
|
102
|
+
})))
|
|
103
|
+
continue;
|
|
104
|
+
return { status: "admitted", receipt: toApplicationCommandReceipt(record) };
|
|
105
|
+
}
|
|
106
|
+
throw new MailboxContentionError("admit", null);
|
|
107
|
+
}
|
|
108
|
+
async function resolveIdempotency(runtime, idempotencyKey, identityDigest) {
|
|
109
|
+
const binding = await loadIdempotencyBinding(runtime.storage, runtime.keys, idempotencyKey);
|
|
110
|
+
if (binding === null)
|
|
111
|
+
return { admission: null, staleBindingBytes: null };
|
|
112
|
+
const loaded = await loadCommand(runtime.storage, runtime.keys, binding.record.commandId);
|
|
113
|
+
if (loaded === null)
|
|
114
|
+
return { admission: null, staleBindingBytes: binding.bytes };
|
|
115
|
+
const ownDigest = await computeIdentityDigest([
|
|
116
|
+
loaded.record.caller,
|
|
117
|
+
loaded.record.target,
|
|
118
|
+
loaded.record.kind,
|
|
119
|
+
loaded.record.payloadDigest
|
|
120
|
+
]);
|
|
121
|
+
if (loaded.record.idempotencyKey !== idempotencyKey || ownDigest !== binding.record.identityDigest)
|
|
122
|
+
throw new PersistedDataCorruptError(runtime.keys.idempotency(idempotencyKey));
|
|
123
|
+
const receipt = toApplicationCommandReceipt(loaded.record);
|
|
124
|
+
if (binding.record.identityDigest !== identityDigest)
|
|
125
|
+
return {
|
|
126
|
+
admission: { status: "conflict", receipt, reason: "idempotency-identity-mismatch" },
|
|
127
|
+
staleBindingBytes: null
|
|
128
|
+
};
|
|
129
|
+
return { admission: { status: "duplicate", receipt }, staleBindingBytes: null };
|
|
130
|
+
}
|