@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,65 @@
|
|
|
1
|
+
import { computePayloadDigest, PayloadDigestError } from "./application-payload-digest.js";
|
|
2
|
+
import {
|
|
3
|
+
createApplicationGuards,
|
|
4
|
+
MAX_APPLICATION_IDENTITY_BYTES
|
|
5
|
+
} from "./application-primitive-guards.js";
|
|
6
|
+
import { decode, encode } from "./codec.js";
|
|
7
|
+
const HEX_DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
|
8
|
+
export const MAX_APPLICATION_PAYLOAD_REFERENCE_BYTES = 2048;
|
|
9
|
+
export function createPayloadValidators(ValidationError) {
|
|
10
|
+
const { requireIdentity, optionalIdentityOf, requireNonNegativeInteger } = createApplicationGuards(ValidationError), validateInlinePayload = async (payload, maxInlinePayloadBytes) => {
|
|
11
|
+
const value = Reflect.get(payload, "value");
|
|
12
|
+
let encoded;
|
|
13
|
+
try {
|
|
14
|
+
encoded = encode(value);
|
|
15
|
+
} catch (cause) {
|
|
16
|
+
throw new ValidationError("payload.value is not encodable by the structured-clone codec.", {
|
|
17
|
+
cause
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if (encoded.byteLength > maxInlinePayloadBytes)
|
|
21
|
+
throw new ValidationError(`payload.value encodes to ${encoded.byteLength} bytes, over the ${maxInlinePayloadBytes}-byte inline ceiling. Store it behind a content-addressed reference instead.`);
|
|
22
|
+
const snapshot = decode(encoded);
|
|
23
|
+
try {
|
|
24
|
+
return {
|
|
25
|
+
payload: { form: "inline", value: snapshot },
|
|
26
|
+
digest: await computePayloadDigest(snapshot)
|
|
27
|
+
};
|
|
28
|
+
} catch (cause) {
|
|
29
|
+
if (cause instanceof PayloadDigestError)
|
|
30
|
+
throw new ValidationError(`payload.value cannot be digested: ${cause.message}`, { cause });
|
|
31
|
+
throw cause;
|
|
32
|
+
}
|
|
33
|
+
}, validateReferencePayload = (payload) => {
|
|
34
|
+
const reference = requireIdentity(Reflect.get(payload, "reference"), "payload.reference", MAX_APPLICATION_PAYLOAD_REFERENCE_BYTES), digest = Reflect.get(payload, "digest");
|
|
35
|
+
if (typeof digest !== "string" || !HEX_DIGEST_PATTERN.test(digest))
|
|
36
|
+
throw new ValidationError("payload.digest must be a 64-character lowercase hexadecimal SHA-256 digest. A reference payload has no other way to bind idempotency to payload identity.");
|
|
37
|
+
const rawByteLength = Reflect.get(payload, "byteLength");
|
|
38
|
+
if (rawByteLength === void 0)
|
|
39
|
+
return { payload: { form: "reference", reference, digest }, digest };
|
|
40
|
+
const referencedBytes = requireNonNegativeInteger(rawByteLength, "payload.byteLength", Number.MAX_SAFE_INTEGER);
|
|
41
|
+
return {
|
|
42
|
+
payload: { form: "reference", reference, digest, byteLength: referencedBytes },
|
|
43
|
+
digest
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
return { validatePayload: async (payload, maxInlinePayloadBytes) => {
|
|
47
|
+
if (typeof payload !== "object" || payload === null || !("form" in payload))
|
|
48
|
+
throw new ValidationError("payload must be an inline or reference payload object.");
|
|
49
|
+
const form = Reflect.get(payload, "form");
|
|
50
|
+
if (form === "inline")
|
|
51
|
+
return validateInlinePayload(payload, maxInlinePayloadBytes);
|
|
52
|
+
if (form !== "reference")
|
|
53
|
+
throw new ValidationError("payload.form must be 'inline' or 'reference'.");
|
|
54
|
+
return validateReferencePayload(payload);
|
|
55
|
+
}, validateCausation: (causation) => {
|
|
56
|
+
if (causation === void 0)
|
|
57
|
+
return;
|
|
58
|
+
if (typeof causation !== "object" || causation === null)
|
|
59
|
+
throw new ValidationError("causation must be an object when present.");
|
|
60
|
+
const correlationId = optionalIdentityOf(causation.correlationId, "causation.correlationId", MAX_APPLICATION_IDENTITY_BYTES), causationId = optionalIdentityOf(causation.causationId, "causation.causationId", MAX_APPLICATION_IDENTITY_BYTES), traceparent = optionalIdentityOf(causation.traceparent, "causation.traceparent", MAX_APPLICATION_IDENTITY_BYTES);
|
|
61
|
+
if (correlationId === void 0 && causationId === void 0 && traceparent === void 0)
|
|
62
|
+
return;
|
|
63
|
+
return { correlationId, causationId, traceparent };
|
|
64
|
+
} };
|
|
65
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timing helpers shared by the durable application primitives (the command
|
|
3
|
+
* mailbox, WFT-84, and the delivery outbox, WFT-85): the deterministic retry
|
|
4
|
+
* backoff every reschedule uses, and the abortable sleep every polling wait
|
|
5
|
+
* uses.
|
|
6
|
+
*
|
|
7
|
+
* @module core/application-primitive-timing
|
|
8
|
+
*/
|
|
9
|
+
/** Deterministic exponential backoff. No jitter, so redelivery timing is testable. */
|
|
10
|
+
export declare function computeRetryBackoffMs(attempt: number, baseMs: number, maximumMs: number): number;
|
|
11
|
+
/**
|
|
12
|
+
* Sleep, resolving `false` when the wait was aborted or the primitive disposed
|
|
13
|
+
* and `true` when the interval actually elapsed.
|
|
14
|
+
*
|
|
15
|
+
* The listeners are removed on every path, including the timer path, so a
|
|
16
|
+
* long-lived primitive does not accumulate abort listeners once per poll.
|
|
17
|
+
*/
|
|
18
|
+
export declare function delayUnlessAborted(milliseconds: number, disposal: AbortSignal, signal?: AbortSignal): Promise<boolean>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function computeRetryBackoffMs(attempt, baseMs, maximumMs) {
|
|
2
|
+
const raw = baseMs * 2 ** Math.max(0, attempt - 1);
|
|
3
|
+
return Math.min(Number.isFinite(raw) ? raw : maximumMs, maximumMs);
|
|
4
|
+
}
|
|
5
|
+
export function delayUnlessAborted(milliseconds, disposal, signal) {
|
|
6
|
+
if (signal?.aborted === !0 || disposal.aborted)
|
|
7
|
+
return Promise.resolve(!1);
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
const settle = (elapsed) => {
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
signal?.removeEventListener("abort", onAbort);
|
|
12
|
+
disposal.removeEventListener("abort", onAbort);
|
|
13
|
+
resolve(elapsed);
|
|
14
|
+
}, onAbort = () => {
|
|
15
|
+
settle(!1);
|
|
16
|
+
}, timer = setTimeout(() => {
|
|
17
|
+
settle(!0);
|
|
18
|
+
}, milliseconds);
|
|
19
|
+
signal?.addEventListener("abort", onAbort, { once: !0 });
|
|
20
|
+
disposal.addEventListener("abort", onAbort, { once: !0 });
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -32,6 +32,8 @@ export function atomicStateDataKey(scope, key) {
|
|
|
32
32
|
return KEYS.stateExecution(scope.ownerWorkflowId, key);
|
|
33
33
|
case "workflow":
|
|
34
34
|
return KEYS.stateWorkflow(scope.workflowType, key);
|
|
35
|
+
default:
|
|
36
|
+
throw Error(`Unknown atomic state scope type: ${JSON.stringify(scope)}`);
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
function assertValidAtomicStateKey(key) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encoding, decoding, and structural validation for durable catalog records.
|
|
3
|
+
*
|
|
4
|
+
* The active-pointer record (`catalog-active:<name>`) is the only catalog
|
|
5
|
+
* value whose bytes are read back and trusted for control-flow decisions
|
|
6
|
+
* (the CAS retry loop, generation comparisons) — so unlike the manifest
|
|
7
|
+
* entry (validated end-to-end by {@link parseWorkflowRevisionManifest}),
|
|
8
|
+
* it gets its own small structural validator here rather than round-tripping
|
|
9
|
+
* through a JSON-Schema-shaped parser.
|
|
10
|
+
*
|
|
11
|
+
* @module core/catalog/codec
|
|
12
|
+
*/
|
|
13
|
+
import type { WorkflowRevisionManifest } from '../contract/types.ts';
|
|
14
|
+
import type { WorkflowCatalogActivePointer } from './types.ts';
|
|
15
|
+
/** Encode an active-pointer record for durable storage. */
|
|
16
|
+
export declare function encodeActivePointer(pointer: WorkflowCatalogActivePointer): Uint8Array;
|
|
17
|
+
/**
|
|
18
|
+
* Decode and structurally validate an active-pointer record read from
|
|
19
|
+
* durable storage. Returns `null` when the bytes do not decode as a valid
|
|
20
|
+
* `{ revision, generation, activatedAt }` record — the caller
|
|
21
|
+
* (`restoreWorkflowCatalog`) fails closed on `null` rather than treating it
|
|
22
|
+
* as absent, matching the codebase's fail-closed convention for corrupted
|
|
23
|
+
* durable singleton records (`ownership-mode-marker.ts`,
|
|
24
|
+
* `EngineLeaseCorruptedError`).
|
|
25
|
+
*/
|
|
26
|
+
export declare function decodeActivePointer(bytes: Uint8Array): WorkflowCatalogActivePointer | null;
|
|
27
|
+
/**
|
|
28
|
+
* Deep-equality comparison between two workflow revision manifests, used by
|
|
29
|
+
* `WorkflowCatalog.install()` to decide whether a re-install for an existing
|
|
30
|
+
* `(name, revision)` key is a byte-identical no-op or a genuine metadata
|
|
31
|
+
* conflict. Compares each manifest's top-level identity scalars alongside
|
|
32
|
+
* its `contract` field's existing canonical serialization
|
|
33
|
+
* ({@link canonicalWorkflowContractJson}, `core/contract/normalize.ts`) —
|
|
34
|
+
* reusing that canonicalizer rather than a fresh `JSON.stringify` keeps this
|
|
35
|
+
* comparison immune to incidental object-key insertion-order differences
|
|
36
|
+
* inside `contract`, the same guarantee `contractHash()`/`revision` already
|
|
37
|
+
* rely on. A caller-supplied `revision` string that changed independently of
|
|
38
|
+
* contract content is also treated as a conflict, not a silent no-op.
|
|
39
|
+
*/
|
|
40
|
+
export declare function manifestsAreByteIdentical(a: WorkflowRevisionManifest, b: WorkflowRevisionManifest): boolean;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { decode, encode } from "../codec.js";
|
|
2
|
+
import { canonicalWorkflowContractJson } from "../contract/normalize.js";
|
|
3
|
+
export function encodeActivePointer(pointer) {
|
|
4
|
+
return encode(pointer);
|
|
5
|
+
}
|
|
6
|
+
export function decodeActivePointer(bytes) {
|
|
7
|
+
let decoded;
|
|
8
|
+
try {
|
|
9
|
+
decoded = decode(bytes);
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (typeof decoded !== "object" || decoded === null)
|
|
14
|
+
return null;
|
|
15
|
+
return validateActivePointerRecord(decoded);
|
|
16
|
+
}
|
|
17
|
+
function validateActivePointerRecord(record) {
|
|
18
|
+
const { revision, generation, activatedAt } = record;
|
|
19
|
+
if (!isNonEmptyString(revision))
|
|
20
|
+
return null;
|
|
21
|
+
if (!isPositiveSafeInteger(generation))
|
|
22
|
+
return null;
|
|
23
|
+
if (typeof activatedAt !== "number" || !Number.isFinite(activatedAt))
|
|
24
|
+
return null;
|
|
25
|
+
return { revision, generation, activatedAt };
|
|
26
|
+
}
|
|
27
|
+
function isNonEmptyString(value) {
|
|
28
|
+
return typeof value === "string" && value.length > 0;
|
|
29
|
+
}
|
|
30
|
+
function isPositiveSafeInteger(value) {
|
|
31
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
|
|
32
|
+
}
|
|
33
|
+
export function manifestsAreByteIdentical(a, b) {
|
|
34
|
+
return canonicalManifestJson(a) === canonicalManifestJson(b);
|
|
35
|
+
}
|
|
36
|
+
function canonicalManifestJson(manifest) {
|
|
37
|
+
return JSON.stringify({
|
|
38
|
+
manifestVersion: manifest.manifestVersion,
|
|
39
|
+
name: manifest.name,
|
|
40
|
+
workflowVersion: manifest.workflowVersion,
|
|
41
|
+
revision: manifest.revision,
|
|
42
|
+
contractHash: manifest.contractHash,
|
|
43
|
+
contract: canonicalWorkflowContractJson(manifest.contract)
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error classes thrown by the durable workflow catalog (WFT-9/WFT-10) and
|
|
3
|
+
* its public promotion, `engine.workflows` (WFT-11).
|
|
4
|
+
*
|
|
5
|
+
* `WorkflowCatalogConflictError` and `WorkflowRevisionNotInstalledError` are
|
|
6
|
+
* public — part of `WeftErrorCode` and re-exported from `src/index.ts` —
|
|
7
|
+
* because `engine.workflows.install()`/`activate()` can throw either of them
|
|
8
|
+
* directly at a caller who holds no other way to reach the catalog.
|
|
9
|
+
* `WorkflowCatalogActivationConflictError` stays internal: it is reachable
|
|
10
|
+
* only through `engine.register()`'s unconditional `activateRegistered`
|
|
11
|
+
* path, which every public entry point already awaits via
|
|
12
|
+
* `ensureWorkflowCatalogReady` rather than calling directly, so no public
|
|
13
|
+
* caller ever observes it as a typed error to catch.
|
|
14
|
+
*
|
|
15
|
+
* @module core/catalog/errors
|
|
16
|
+
*/
|
|
17
|
+
import { WeftError } from '../weft-error.ts';
|
|
18
|
+
/**
|
|
19
|
+
* Thrown by {@link import('./workflow-catalog.ts').WorkflowCatalog.install}
|
|
20
|
+
* when a caller attempts to install a `(name, revision)` pair that already
|
|
21
|
+
* has a durably- or in-memory-installed entry with different manifest bytes.
|
|
22
|
+
* A byte-identical reinstall for the same key is a no-op, not an error — see
|
|
23
|
+
* `install()`'s JSDoc.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { Engine, WorkflowCatalogConflictError } from '@lostgradient/weft';
|
|
28
|
+
*
|
|
29
|
+
* const engine = new Engine();
|
|
30
|
+
* try {
|
|
31
|
+
* throw new WorkflowCatalogConflictError('checkout', 'r1');
|
|
32
|
+
* } catch (error) {
|
|
33
|
+
* if (error instanceof WorkflowCatalogConflictError) {
|
|
34
|
+
* console.error(error.workflowName, error.revision);
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* void engine;
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare class WorkflowCatalogConflictError extends WeftError<'WorkflowCatalogConflictError'> {
|
|
41
|
+
readonly workflowName: string;
|
|
42
|
+
readonly revision: string;
|
|
43
|
+
constructor(workflowName: string, revision: string);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Thrown by
|
|
47
|
+
* {@link import('./workflow-catalog.ts').WorkflowCatalog.activateRegistered}
|
|
48
|
+
* after its bounded 5-attempt CAS retry loop exhausts without successfully
|
|
49
|
+
* committing the active pointer — mirrors `AtomicStateConflictError`'s
|
|
50
|
+
* exhaustion contract.
|
|
51
|
+
*/
|
|
52
|
+
export declare class WorkflowCatalogActivationConflictError extends WeftError<'WorkflowCatalogActivationConflictError'> {
|
|
53
|
+
readonly workflowName: string;
|
|
54
|
+
readonly attempts: number;
|
|
55
|
+
constructor(workflowName: string, attempts: number);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Thrown by `engine.workflows.activate()` when `(workflowName, revision)`
|
|
59
|
+
* has never been installed — distinct from
|
|
60
|
+
* `WorkflowCatalog.activateCandidate`'s own `applied: false` refusal
|
|
61
|
+
* variants, which all assume the candidate IS installed and instead
|
|
62
|
+
* describe why activating it was refused.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* import { Engine, WorkflowRevisionNotInstalledError } from '@lostgradient/weft';
|
|
67
|
+
*
|
|
68
|
+
* const engine = new Engine();
|
|
69
|
+
* try {
|
|
70
|
+
* await engine.workflows.activate('checkout', 'unknown-revision');
|
|
71
|
+
* } catch (error) {
|
|
72
|
+
* if (error instanceof WorkflowRevisionNotInstalledError) {
|
|
73
|
+
* console.error(error.workflowName, error.revision);
|
|
74
|
+
* }
|
|
75
|
+
* }
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare class WorkflowRevisionNotInstalledError extends WeftError<'WorkflowRevisionNotInstalledError'> {
|
|
79
|
+
readonly workflowName: string;
|
|
80
|
+
readonly revision: string;
|
|
81
|
+
constructor(workflowName: string, revision: string);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Thrown by
|
|
85
|
+
* {@link import('./workflow-catalog.ts').WorkflowCatalog.activateCandidate}
|
|
86
|
+
* when the durable active pointer for `name` names a revision whose catalog
|
|
87
|
+
* entry cannot be resolved — neither this process's in-memory cache nor a
|
|
88
|
+
* durable read-through finds it. This should be unreachable in practice:
|
|
89
|
+
* both activation entry points (`install()`-then-activate) always durably
|
|
90
|
+
* install a revision before ever pointing the active pointer at it, so a
|
|
91
|
+
* durably-active revision missing its own entry is a genuine storage
|
|
92
|
+
* inconsistency, not a race any caller can retry past. Fail-closed rather
|
|
93
|
+
* than silently skipping the compatibility check `activateCandidate`
|
|
94
|
+
* exists to enforce — see `#refuseIncompatibleCandidate`'s JSDoc. Stays
|
|
95
|
+
* internal (not re-exported from `src/index.ts`) like
|
|
96
|
+
* {@link WorkflowCatalogActivationConflictError}: no code path lets a public
|
|
97
|
+
* caller observe it as a typed error to catch, only as a generic
|
|
98
|
+
* `EngineFailure`-wrapped operation fault.
|
|
99
|
+
*/
|
|
100
|
+
export declare class WorkflowCatalogActiveEntryMissingError extends WeftError<'WorkflowCatalogActiveEntryMissingError'> {
|
|
101
|
+
readonly workflowName: string;
|
|
102
|
+
readonly revision: string;
|
|
103
|
+
constructor(workflowName: string, revision: string);
|
|
104
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { WeftError } from "../weft-error.js";
|
|
2
|
+
|
|
3
|
+
export class WorkflowCatalogConflictError extends WeftError {
|
|
4
|
+
workflowName;
|
|
5
|
+
revision;
|
|
6
|
+
constructor(workflowName, revision) {
|
|
7
|
+
super("WorkflowCatalogConflictError", `Workflow catalog entry "${workflowName}" revision "${revision}" is already installed with ` + "different contract metadata. A revision identity must be immutable once installed \u2014 " + "register a workflow whose contract content differs under a new revision instead of reusing this one.");
|
|
8
|
+
this.workflowName = workflowName;
|
|
9
|
+
this.revision = revision;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class WorkflowCatalogActivationConflictError extends WeftError {
|
|
14
|
+
workflowName;
|
|
15
|
+
attempts;
|
|
16
|
+
constructor(workflowName, attempts) {
|
|
17
|
+
super("WorkflowCatalogActivationConflictError", `Workflow catalog activation conflict: failed to activate "${workflowName}" after ${String(attempts)} attempts due to sustained concurrent writers.`);
|
|
18
|
+
this.workflowName = workflowName;
|
|
19
|
+
this.attempts = attempts;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class WorkflowRevisionNotInstalledError extends WeftError {
|
|
24
|
+
workflowName;
|
|
25
|
+
revision;
|
|
26
|
+
constructor(workflowName, revision) {
|
|
27
|
+
super("WorkflowRevisionNotInstalledError", `Workflow "${workflowName}" has no installed revision "${revision}" \u2014 install it via ` + "engine.workflows.install() before activating it.");
|
|
28
|
+
this.workflowName = workflowName;
|
|
29
|
+
this.revision = revision;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class WorkflowCatalogActiveEntryMissingError extends WeftError {
|
|
34
|
+
workflowName;
|
|
35
|
+
revision;
|
|
36
|
+
constructor(workflowName, revision) {
|
|
37
|
+
super("WorkflowCatalogActiveEntryMissingError", `Workflow catalog inconsistency: "${workflowName}" is durably active at revision "${revision}", but that revision's catalog entry cannot be resolved (neither in-memory nor durably). Refusing to activate a candidate without being able to check compatibility against the currently active revision.`);
|
|
38
|
+
this.workflowName = workflowName;
|
|
39
|
+
this.revision = revision;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barrel for the durable workflow catalog (WFT-9/WFT-10, extended with
|
|
3
|
+
* reference accounting and removal in WFT-12).
|
|
4
|
+
*
|
|
5
|
+
* `WorkflowCatalog` itself and `WorkflowCatalogEntry` (which can carry a
|
|
6
|
+
* live `RegisteredWorkflowDefinition` function reference) stay
|
|
7
|
+
* package-internal — `core/engine/catalog-readiness.ts`,
|
|
8
|
+
* `core/engine/catalog-activation.ts`, `core/engine/catalog-removal.ts`, and
|
|
9
|
+
* `core/registry-snapshot.ts` are their consumers. Two different public
|
|
10
|
+
* surfaces are built on top: WFT-11's `engine.workflows`
|
|
11
|
+
* (`core/engine/engine-workflows-namespace.ts`), which is why
|
|
12
|
+
* `WorkflowCatalogConflictError`, `WorkflowRevisionNotInstalledError`,
|
|
13
|
+
* `WorkflowCatalogActivationResult`, `WorkflowCatalogActivePointer`, and
|
|
14
|
+
* `WorkflowRevisionRecord` ARE re-exported from `src/index.ts`; and WFT-12's
|
|
15
|
+
* `removeWorkflowRevision`/`getWorkflowRevisionDiagnostics`
|
|
16
|
+
* (`core/engine/catalog-removal.ts`).
|
|
17
|
+
*
|
|
18
|
+
* @module core/catalog
|
|
19
|
+
*/
|
|
20
|
+
export { WorkflowCatalogActivationConflictError, WorkflowCatalogActiveEntryMissingError, WorkflowCatalogConflictError, WorkflowRevisionNotInstalledError, } from './errors.ts';
|
|
21
|
+
export { decrementNestedRevisionCount, incrementNestedRevisionCount, readNestedRevisionCount, totalWorkflowRevisionReferences, type WorkflowRevisionReferenceCounts, } from './reference-counts.ts';
|
|
22
|
+
export { removeCatalogEntry, type WorkflowCatalogRemovalOutcome } from './removal.ts';
|
|
23
|
+
export { restoreWorkflowCatalog, type RestoredWorkflowCatalogState } from './storage-io.ts';
|
|
24
|
+
export type { WorkflowCatalogActivationResult, WorkflowCatalogActivePointer, WorkflowCatalogEntry, WorkflowRevisionRecord, } from './types.ts';
|
|
25
|
+
export { WorkflowCatalog, type ActivateCandidateOptions } from './workflow-catalog.ts';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
WorkflowCatalogActivationConflictError,
|
|
3
|
+
WorkflowCatalogActiveEntryMissingError,
|
|
4
|
+
WorkflowCatalogConflictError,
|
|
5
|
+
WorkflowRevisionNotInstalledError
|
|
6
|
+
} from "./errors.js";
|
|
7
|
+
export {
|
|
8
|
+
decrementNestedRevisionCount,
|
|
9
|
+
incrementNestedRevisionCount,
|
|
10
|
+
readNestedRevisionCount,
|
|
11
|
+
totalWorkflowRevisionReferences
|
|
12
|
+
} from "./reference-counts.js";
|
|
13
|
+
export { removeCatalogEntry } from "./removal.js";
|
|
14
|
+
export { restoreWorkflowCatalog } from "./storage-io.js";
|
|
15
|
+
export { WorkflowCatalog } from "./workflow-catalog.js";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `WorkflowRevisionReferenceCounts` — the bounded accounting interface a
|
|
3
|
+
* `(name, revision)` removal decision is gated on (WFT-12).
|
|
4
|
+
*
|
|
5
|
+
* Seven fields, all always present so a consumer never has to special-case
|
|
6
|
+
* an "unknown" reference kind. Two are wired to real in-process signals this
|
|
7
|
+
* batch; the remaining five stay structurally present but always `0` until
|
|
8
|
+
* run-level revision pinning lands (WFT-17) gives them something real to
|
|
9
|
+
* count — see each field's own doc for its specific WFT-17 dependency. This
|
|
10
|
+
* mirrors `workflow-catalog.ts`'s own precedent of describing a forward
|
|
11
|
+
* dependency in prose rather than leaving a `TODO`/`FIXME` marker.
|
|
12
|
+
*
|
|
13
|
+
* Keyed by structured `(name, revision)` throughout — nested
|
|
14
|
+
* `Map<string, Map<string, number>>`, never a delimiter-joined string — so a
|
|
15
|
+
* workflow `name` or `revision` equal to `'__proto__'`/`'toString'` or
|
|
16
|
+
* containing a colon is always handled correctly, matching
|
|
17
|
+
* `WorkflowCatalog`'s own `#entries` convention.
|
|
18
|
+
*
|
|
19
|
+
* @module core/catalog/reference-counts
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Every reference kind a `(name, revision)` removal decision considers.
|
|
23
|
+
* Removal is refused whenever {@link totalWorkflowRevisionReferences} is
|
|
24
|
+
* nonzero for the target revision.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import type { WorkflowRevisionReferenceCounts } from '@lostgradient/weft';
|
|
29
|
+
*
|
|
30
|
+
* function summarize(counts: WorkflowRevisionReferenceCounts): string {
|
|
31
|
+
* return `${counts.registeredDefinitions} registered, ${counts.inFlightStarts} in flight`;
|
|
32
|
+
* }
|
|
33
|
+
* void summarize;
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export type WorkflowRevisionReferenceCounts = Readonly<{
|
|
37
|
+
/**
|
|
38
|
+
* `1` when this process's own `engine.register()`-drain path most
|
|
39
|
+
* recently activated exactly this revision for this name, `0` otherwise.
|
|
40
|
+
* Wired now, from `EngineInternals.registeredCatalogRevisions`.
|
|
41
|
+
*/
|
|
42
|
+
registeredDefinitions: number;
|
|
43
|
+
/**
|
|
44
|
+
* Count of this process's own in-flight `startWorkflow` calls reserved
|
|
45
|
+
* against this revision (from `EngineInternals.inFlightStartsByRevision`).
|
|
46
|
+
* Wired now; the reservation lives inside `lifecycle/start.ts`'s single
|
|
47
|
+
* `startWorkflow` choke point itself, so every caller that funnels through
|
|
48
|
+
* it is already counted — `engine.start()`/`engine.startOrSignal()`'s
|
|
49
|
+
* create path, and `ctx.startChild()` too, since
|
|
50
|
+
* `createChildWorkflowOperationCallbacks` (`callback-creators-bundles.ts`)
|
|
51
|
+
* calls the very same `startWorkflow`. There is no separate bulk
|
|
52
|
+
* `startBatch()` entry point to funnel — `buildStartBatchOperations` is
|
|
53
|
+
* internal plumbing already inside this same `startWorkflow` call,
|
|
54
|
+
* building one start's own storage-write batch, not a distinct
|
|
55
|
+
* multi-start API.
|
|
56
|
+
*/
|
|
57
|
+
inFlightStarts: number;
|
|
58
|
+
/**
|
|
59
|
+
* Non-terminal runs whose `WorkflowState` pins exactly this revision.
|
|
60
|
+
* Always `0` until run-level revision pinning (WFT-17) gives a run's
|
|
61
|
+
* persisted state a `revision` field to count against.
|
|
62
|
+
*/
|
|
63
|
+
nonTerminalRuns: number;
|
|
64
|
+
/**
|
|
65
|
+
* Schedules pinned to exactly this revision. Always `0` until WFT-17
|
|
66
|
+
* introduces schedule-level revision pinning.
|
|
67
|
+
*/
|
|
68
|
+
pinnedSchedules: number;
|
|
69
|
+
/**
|
|
70
|
+
* Queued dispatches (delayed starts, retries) targeting exactly this
|
|
71
|
+
* revision. Always `0` until WFT-17 threads revision identity through the
|
|
72
|
+
* dispatch ledger.
|
|
73
|
+
*/
|
|
74
|
+
pendingDispatches: number;
|
|
75
|
+
/**
|
|
76
|
+
* Active execution realms (remote worker sessions) currently running
|
|
77
|
+
* exactly this revision. Always `0` until WFT-17 gives a realm's
|
|
78
|
+
* advertised contract a revision this accounting can compare against.
|
|
79
|
+
*/
|
|
80
|
+
activeExecutionRealms: number;
|
|
81
|
+
/**
|
|
82
|
+
* Retained recovery records (crash-recovery checkpoints, dead letters)
|
|
83
|
+
* referencing exactly this revision. Always `0` until WFT-17 threads
|
|
84
|
+
* revision identity through those retained records.
|
|
85
|
+
*/
|
|
86
|
+
retainedRecoveryRecords: number;
|
|
87
|
+
}>;
|
|
88
|
+
/** Sum every field of `counts` — nonzero means removal must be refused. */
|
|
89
|
+
export declare function totalWorkflowRevisionReferences(counts: WorkflowRevisionReferenceCounts): number;
|
|
90
|
+
/**
|
|
91
|
+
* Increment the `(name, revision)` count in a nested
|
|
92
|
+
* `Map<string, Map<string, number>>`, creating the inner map and the entry
|
|
93
|
+
* as needed.
|
|
94
|
+
*/
|
|
95
|
+
export declare function incrementNestedRevisionCount(counts: Map<string, Map<string, number>>, name: string, revision: string): void;
|
|
96
|
+
/**
|
|
97
|
+
* Decrement the `(name, revision)` count in a nested
|
|
98
|
+
* `Map<string, Map<string, number>>`. Removes the revision's own entry once
|
|
99
|
+
* it reaches `0` (rather than leaving a stale `0` behind) and removes the
|
|
100
|
+
* name's inner map once it is empty. A decrement against an absent
|
|
101
|
+
* `(name, revision)` is a no-op — never goes negative.
|
|
102
|
+
*/
|
|
103
|
+
export declare function decrementNestedRevisionCount(counts: Map<string, Map<string, number>>, name: string, revision: string): void;
|
|
104
|
+
/** Read the `(name, revision)` count from a nested map, `0` when absent. */
|
|
105
|
+
export declare function readNestedRevisionCount(counts: Map<string, Map<string, number>>, name: string, revision: string): number;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function totalWorkflowRevisionReferences(counts) {
|
|
2
|
+
return counts.registeredDefinitions + counts.inFlightStarts + counts.nonTerminalRuns + counts.pinnedSchedules + counts.pendingDispatches + counts.activeExecutionRealms + counts.retainedRecoveryRecords;
|
|
3
|
+
}
|
|
4
|
+
export function incrementNestedRevisionCount(counts, name, revision) {
|
|
5
|
+
let byRevision = counts.get(name);
|
|
6
|
+
if (byRevision === void 0) {
|
|
7
|
+
byRevision = new Map;
|
|
8
|
+
counts.set(name, byRevision);
|
|
9
|
+
}
|
|
10
|
+
byRevision.set(revision, (byRevision.get(revision) ?? 0) + 1);
|
|
11
|
+
}
|
|
12
|
+
export function decrementNestedRevisionCount(counts, name, revision) {
|
|
13
|
+
const byRevision = counts.get(name);
|
|
14
|
+
if (byRevision === void 0)
|
|
15
|
+
return;
|
|
16
|
+
const current = byRevision.get(revision);
|
|
17
|
+
if (current === void 0)
|
|
18
|
+
return;
|
|
19
|
+
if (current <= 1) {
|
|
20
|
+
byRevision.delete(revision);
|
|
21
|
+
if (byRevision.size === 0)
|
|
22
|
+
counts.delete(name);
|
|
23
|
+
} else
|
|
24
|
+
byRevision.set(revision, current - 1);
|
|
25
|
+
}
|
|
26
|
+
export function readNestedRevisionCount(counts, name, revision) {
|
|
27
|
+
return counts.get(name)?.get(revision) ?? 0;
|
|
28
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mechanical CAS-delete primitive `WorkflowCatalog.remove()` delegates
|
|
3
|
+
* to. Split into its own module to protect `workflow-catalog.ts`'s size
|
|
4
|
+
* against the repository's 500-line implementation-file ceiling — mirrors
|
|
5
|
+
* how `storage-io.ts`/`codec.ts` already sit alongside `workflow-catalog.ts`
|
|
6
|
+
* rather than inline in it.
|
|
7
|
+
*
|
|
8
|
+
* Fences the delete on BOTH the exact entry bytes read AND the exact active
|
|
9
|
+
* pointer bytes read, in one `conditionalBatch` — not entry bytes alone.
|
|
10
|
+
* Between an in-memory "is this the active revision" check and the delete,
|
|
11
|
+
* a concurrent `activateCandidate`/`activateRegistered` call (this process
|
|
12
|
+
* or another sharing the same durable store) could make the target revision
|
|
13
|
+
* active; fencing the active-pointer key too means that race loses the CAS
|
|
14
|
+
* and surfaces as `'conflict'` (the caller re-decides) instead of silently
|
|
15
|
+
* deleting a revision that became active a moment before the delete landed.
|
|
16
|
+
*
|
|
17
|
+
* Single-shot — no retry loop, matching `activateCandidate`'s own
|
|
18
|
+
* no-retry-caller-decides precedent, since a removal decision (unlike
|
|
19
|
+
* `activateRegistered`'s unconditional retry) already depends on reference
|
|
20
|
+
* counts computed by the caller and should not blindly re-attempt against
|
|
21
|
+
* possibly-stale counts.
|
|
22
|
+
*
|
|
23
|
+
* @module core/catalog/removal
|
|
24
|
+
*/
|
|
25
|
+
import { type Storage } from '../../storage/interface.ts';
|
|
26
|
+
/** Outcome of {@link removeCatalogEntry}. */
|
|
27
|
+
export type WorkflowCatalogRemovalOutcome = Readonly<{
|
|
28
|
+
outcome: 'removed';
|
|
29
|
+
}> | Readonly<{
|
|
30
|
+
outcome: 'not-found';
|
|
31
|
+
}> | Readonly<{
|
|
32
|
+
outcome: 'active';
|
|
33
|
+
activeRevision: string;
|
|
34
|
+
}> | Readonly<{
|
|
35
|
+
outcome: 'conflict';
|
|
36
|
+
}>;
|
|
37
|
+
/**
|
|
38
|
+
* Durably delete the installed-revision record for `(name, revision)`.
|
|
39
|
+
*
|
|
40
|
+
* Reads the current `catalog-entry:<name>:<revision>` bytes; `null` is
|
|
41
|
+
* `'not-found'` (a no-op — the caller may already believe the revision is
|
|
42
|
+
* gone). Reads the current `catalog-active:<name>` bytes and refuses with
|
|
43
|
+
* `'active'` when they decode to exactly this revision — a structural
|
|
44
|
+
* invariant independent of reference counts, since every future or
|
|
45
|
+
* resuming run resolves the active pointer, not a specific installed
|
|
46
|
+
* entry. Otherwise deletes the entry key via a `conditionalBatch` CAS'd on
|
|
47
|
+
* BOTH the exact entry bytes AND the exact active-pointer bytes read above
|
|
48
|
+
* (present as a no-op `put`-free precondition when active-pointer bytes
|
|
49
|
+
* are `null` — never activated). A CAS loss (either key changed
|
|
50
|
+
* concurrently) surfaces as `'conflict'`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function removeCatalogEntry(storage: Storage, name: string, revision: string): Promise<WorkflowCatalogRemovalOutcome>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { KEYS, storageConditionalBatch } from "../../storage/interface.js";
|
|
2
|
+
import { decodeActivePointer } from "./codec.js";
|
|
3
|
+
export async function removeCatalogEntry(storage, name, revision) {
|
|
4
|
+
const entryKey = KEYS.catalogEntry(name, revision), entryBytes = await storage.get(entryKey);
|
|
5
|
+
if (entryBytes === null)
|
|
6
|
+
return { outcome: "not-found" };
|
|
7
|
+
const activeKey = KEYS.catalogActive(name), activeBytes = await storage.get(activeKey), activePointer = activeBytes === null ? null : decodeActivePointer(activeBytes);
|
|
8
|
+
if (activePointer !== null && activePointer.revision === revision)
|
|
9
|
+
return { outcome: "active", activeRevision: activePointer.revision };
|
|
10
|
+
return await storageConditionalBatch(storage, [
|
|
11
|
+
{ key: entryKey, expectedValue: entryBytes },
|
|
12
|
+
{ key: activeKey, expectedValue: activeBytes }
|
|
13
|
+
], [{ type: "delete", key: entryKey }]) ? { outcome: "removed" } : { outcome: "conflict" };
|
|
14
|
+
}
|