@lostgradient/weft 0.24.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli/conformance.js +5 -2
- package/dist/cli/generated/operation-catalog.snapshot.json +95 -1
- package/dist/cli/generated/operation-client.generated.d.ts +28 -2
- package/dist/cli/generated/operation-client.generated.js +2 -0
- package/dist/cli/parse-schedule-arguments.js +15 -3
- package/dist/cli/schedule.js +2 -1
- package/dist/cli/types.d.ts +2 -1
- package/dist/client/http-client-requests.js +6 -9
- package/dist/client/start-body.js +1 -0
- package/dist/core/catalog/index.d.ts +2 -2
- package/dist/core/catalog/index.js +9 -2
- package/dist/core/catalog/reference-counts.d.ts +29 -14
- package/dist/core/catalog/removal.d.ts +63 -10
- package/dist/core/catalog/removal.js +18 -2
- package/dist/core/catalog/storage-io.d.ts +23 -0
- package/dist/core/catalog/storage-io.js +1 -1
- package/dist/core/engine/activity-resolution.js +17 -17
- package/dist/core/engine/bulk-operations-purge.js +14 -20
- package/dist/core/engine/bulk-operations-retry.d.ts +9 -0
- package/dist/core/engine/bulk-operations-retry.js +177 -0
- package/dist/core/engine/bulk-operations-shared.d.ts +18 -0
- package/dist/core/engine/bulk-operations-shared.js +14 -0
- package/dist/core/engine/bulk-operations.js +8 -167
- package/dist/core/engine/callback-creators-bundles.js +4 -2
- package/dist/core/engine/callback-creators-core.js +9 -1
- package/dist/core/engine/callback-creators-schedule.js +2 -2
- package/dist/core/engine/catalog-readiness.js +2 -0
- package/dist/core/engine/catalog-removal.d.ts +107 -3
- package/dist/core/engine/catalog-removal.js +66 -13
- package/dist/core/engine/catalog-tombstone-recovery.d.ts +56 -0
- package/dist/core/engine/catalog-tombstone-recovery.js +39 -0
- package/dist/core/engine/constraints.js +5 -1
- package/dist/core/engine/construction.d.ts +1 -1
- package/dist/core/engine/decode-revision.d.ts +32 -0
- package/dist/core/engine/decode-revision.js +9 -0
- package/dist/core/engine/disposal.d.ts +6 -2
- package/dist/core/engine/disposal.js +24 -10
- package/dist/core/engine/dynamic-source-errors.d.ts +81 -0
- package/dist/core/engine/dynamic-source-errors.js +25 -0
- package/dist/core/engine/dynamic-source-execution.d.ts +173 -0
- package/dist/core/engine/dynamic-source-execution.js +108 -0
- package/dist/core/engine/engine-internal-types.d.ts +16 -0
- package/dist/core/engine/engine-workflows-namespace.d.ts +10 -0
- package/dist/core/engine/engine-workflows-namespace.js +3 -1
- package/dist/core/engine/index.d.ts +18 -10
- package/dist/core/engine/index.js +21 -10
- package/dist/core/engine/inline-launch-queue.js +1 -1
- package/dist/core/engine/internals.d.ts +26 -30
- package/dist/core/engine/lifecycle/checkpoint-launch.d.ts +29 -0
- package/dist/core/engine/lifecycle/checkpoint-launch.js +75 -0
- package/dist/core/engine/lifecycle/delayed-start-registration.d.ts +30 -0
- package/dist/core/engine/lifecycle/delayed-start-registration.js +11 -0
- package/dist/core/engine/lifecycle/fork-helpers.d.ts +33 -1
- package/dist/core/engine/lifecycle/fork-helpers.js +3 -1
- package/dist/core/engine/lifecycle/recovery-revision-groups.d.ts +75 -0
- package/dist/core/engine/lifecycle/recovery-revision-groups.js +64 -0
- package/dist/core/engine/lifecycle/resume-generation-guard.d.ts +58 -0
- package/dist/core/engine/lifecycle/resume-generation-guard.js +11 -0
- package/dist/core/engine/lifecycle/resume.js +18 -9
- package/dist/core/engine/lifecycle/shared.d.ts +43 -0
- package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +12 -0
- package/dist/core/engine/lifecycle/standalone-claim-acquire.js +6 -0
- package/dist/core/engine/lifecycle/start-commit-errors.d.ts +12 -0
- package/dist/core/engine/lifecycle/start-commit-errors.js +6 -0
- package/dist/core/engine/lifecycle/start-commit.d.ts +22 -7
- package/dist/core/engine/lifecycle/start-commit.js +49 -15
- package/dist/core/engine/lifecycle/start-exec.d.ts +18 -2
- package/dist/core/engine/lifecycle/start-exec.js +7 -5
- package/dist/core/engine/lifecycle/start-or-signal-create.js +1 -3
- package/dist/core/engine/lifecycle/start-or-signal.js +1 -1
- package/dist/core/engine/lifecycle/start-precondition-attribution.d.ts +49 -0
- package/dist/core/engine/lifecycle/start-precondition-attribution.js +13 -0
- package/dist/core/engine/lifecycle/start-revision-resolution.d.ts +34 -0
- package/dist/core/engine/lifecycle/start-revision-resolution.js +11 -0
- package/dist/core/engine/lifecycle/start-state.d.ts +16 -0
- package/dist/core/engine/lifecycle/start-state.js +60 -0
- package/dist/core/engine/lifecycle/start-terminal-conflict-purge.d.ts +66 -3
- package/dist/core/engine/lifecycle/start-terminal-conflict-purge.js +13 -3
- package/dist/core/engine/lifecycle/start.d.ts +16 -6
- package/dist/core/engine/lifecycle/start.js +38 -68
- package/dist/core/engine/lifecycle/transition.d.ts +3 -4
- package/dist/core/engine/lifecycle/transition.js +30 -80
- package/dist/core/engine/lifecycle.d.ts +4 -2
- package/dist/core/engine/lifecycle.js +6 -6
- package/dist/core/engine/listing.js +15 -1
- package/dist/core/engine/memo-durable-activity.js +4 -1
- package/dist/core/engine/nonterminal-revision-count.d.ts +25 -0
- package/dist/core/engine/nonterminal-revision-count.js +16 -0
- package/dist/core/engine/operations-time.d.ts +5 -23
- package/dist/core/engine/operations-time.js +13 -11
- package/dist/core/engine/ownership-bootstrap.d.ts +1 -1
- package/dist/core/engine/pinned-schedule-revision-count.d.ts +26 -0
- package/dist/core/engine/pinned-schedule-revision-count.js +19 -0
- package/dist/core/engine/pinned-schedule-revision.d.ts +76 -0
- package/dist/core/engine/pinned-schedule-revision.js +53 -0
- package/dist/core/engine/registration.d.ts +30 -0
- package/dist/core/engine/registration.js +17 -2
- package/dist/core/engine/retention.js +9 -2
- package/dist/core/engine/revision-errors.d.ts +71 -0
- package/dist/core/engine/revision-errors.js +13 -0
- package/dist/core/engine/schedule-revision-fire.d.ts +17 -0
- package/dist/core/engine/schedule-revision-fire.js +3 -0
- package/dist/core/engine/schedule-run.js +2 -1
- package/dist/core/engine/schedules.d.ts +2 -1
- package/dist/core/engine/schedules.js +28 -9
- package/dist/core/engine/source-diagnostics.d.ts +76 -0
- package/dist/core/engine/source-diagnostics.js +105 -0
- package/dist/core/engine/source-registration.js +4 -2
- package/dist/core/engine/source-resolution.d.ts +25 -9
- package/dist/core/engine/source-resolution.js +49 -14
- package/dist/core/engine/source-runtime-state.d.ts +92 -0
- package/dist/core/engine/source-runtime-state.js +36 -0
- package/dist/core/engine/storage-io.d.ts +19 -2
- package/dist/core/engine/storage-io.js +21 -2
- package/dist/core/engine/termination/finalizer-registration.d.ts +18 -0
- package/dist/core/engine/termination/finalizer-registration.js +21 -0
- package/dist/core/engine/termination/finalizer.d.ts +16 -19
- package/dist/core/engine/termination/finalizer.js +3 -3
- package/dist/core/engine/time-operation-callbacks.d.ts +32 -0
- package/dist/core/engine/time-operation-callbacks.js +0 -0
- package/dist/core/engine/validation/schedule-cadence.d.ts +16 -0
- package/dist/core/engine/validation/schedule-cadence.js +20 -0
- package/dist/core/engine/validation/schedule-options.d.ts +4 -1
- package/dist/core/engine/validation/schedule-options.js +14 -0
- package/dist/core/engine/validation/schedule-revision.d.ts +24 -0
- package/dist/core/engine/validation/schedule-revision.js +21 -0
- package/dist/core/engine/validation/schedule-warnings.d.ts +11 -0
- package/dist/core/engine/validation/schedule-warnings.js +5 -0
- package/dist/core/engine/validation/schedule.d.ts +4 -3
- package/dist/core/engine/validation/schedule.js +15 -26
- package/dist/core/engine/validation.js +3 -0
- package/dist/core/engine/workflow-claim-reclaim-target.d.ts +1 -1
- package/dist/core/engine/workflow-claim-reclaim-target.js +16 -2
- package/dist/core/engine/workflow-retention-deadline.d.ts +16 -0
- package/dist/core/engine/workflow-retention-deadline.js +33 -0
- package/dist/core/events/event-map.d.ts +5 -0
- package/dist/core/events/index.d.ts +1 -0
- package/dist/core/events/index.js +1 -0
- package/dist/core/events/workflow-source-events.d.ts +102 -0
- package/dist/core/events/workflow-source-events.js +57 -0
- package/dist/core/execution-strategy.d.ts +7 -0
- package/dist/core/inline-execution-strategy.context-options.d.ts +1 -1
- package/dist/core/inline-execution-strategy.js +1 -1
- package/dist/core/types/checkpoint.d.ts +14 -0
- package/dist/core/types/schedules.d.ts +52 -1
- package/dist/core/types/state.d.ts +16 -0
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +4 -1
- package/dist/core/worker-execution-strategy.d.ts +1 -0
- package/dist/core/worker-execution-strategy.js +13 -1
- package/dist/core/worker-fault-handling.d.ts +10 -0
- package/dist/core/worker-fault-handling.js +1 -0
- package/dist/core/worker-inbound-message.d.ts +1 -0
- package/dist/core/worker-inbound-message.js +1 -0
- package/dist/core/worker-protocol-guard.js +4 -0
- package/dist/core/worker-protocol.d.ts +7 -0
- package/dist/core/worker-protocol.js +2 -1
- package/dist/core/worker-turn-watchdog.d.ts +8 -1
- package/dist/core/worker-turn-watchdog.js +3 -2
- package/dist/diagnostics/format.js +5 -0
- package/dist/diagnostics/types.d.ts +24 -0
- package/dist/diagnostics/version-check.js +8 -2
- package/dist/http.js +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +7 -0
- package/dist/indexeddb.js +1 -1
- package/dist/json-schema.js +3 -3
- package/dist/server/fault-to-json-rpc.js +2 -1
- package/dist/server/index.d.ts +11 -0
- package/dist/server/operation-fault.d.ts +2 -0
- package/dist/server/operation-fault.js +2 -1
- package/dist/server/operations/create-schedule.d.ts +2 -0
- package/dist/server/operations/create-schedule.js +10 -6
- package/dist/server/operations/get-catalog-diagnostics.d.ts +28 -0
- package/dist/server/operations/get-catalog-diagnostics.js +10 -2
- package/dist/server/operations/get-task-detail-schema.d.ts +8 -0
- package/dist/server/operations/get-task-detail-schema.js +1 -0
- package/dist/server/operations/get-task-detail.d.ts +8 -0
- package/dist/server/operations/get-task-detail.js +1 -0
- package/dist/server/operations/get-task-diagnostics.d.ts +4 -4
- package/dist/server/operations/preload-workflow-revision.d.ts +28 -0
- package/dist/server/operations/preload-workflow-revision.js +60 -0
- package/dist/server/operations/schedule-faults.d.ts +1 -0
- package/dist/server/operations/schedule-faults.js +32 -4
- package/dist/server/operations/schedule-rest-body.d.ts +1 -0
- package/dist/server/operations/schedule-rest-body.js +2 -1
- package/dist/server/operations/static-registrations.js +6 -0
- package/dist/server/operations/update-schedule.d.ts +2 -0
- package/dist/server/operations/update-schedule.js +4 -2
- package/dist/server/operations/workflow-catalog-operation-helpers.js +27 -0
- package/dist/server/runtime/task-dispatch-envelope.d.ts +14 -0
- package/dist/server/runtime/task-dispatch-envelope.js +32 -0
- package/dist/server/runtime/task-dispatch-revision.d.ts +29 -0
- package/dist/server/runtime/task-dispatch-revision.js +13 -0
- package/dist/server/runtime/task-dispatch.js +12 -37
- package/dist/server/runtime/task-ledger-recovery.js +1 -1
- package/dist/server/runtime/task-ledger-runtime.d.ts +14 -2
- package/dist/server/runtime/task-ledger-runtime.js +5 -3
- package/dist/server/runtime/task-polling.js +9 -3
- package/dist/server/runtime/task-reconciliation.js +2 -1
- package/dist/server/runtime/websocket-worker.js +9 -0
- package/dist/server/task-ledger-codec.d.ts +9 -0
- package/dist/server/task-ledger-codec.js +4 -1
- package/dist/server/task-ledger-transition-helpers.js +1 -0
- package/dist/server/task-ledger-types.d.ts +9 -0
- package/dist/server/task-ledger.d.ts +1 -1
- package/dist/server/task-ledger.js +2 -1
- package/dist/server/task-queue-types.d.ts +2 -0
- package/dist/storage/bun-sql.js +3 -1
- package/dist/storage/catalog-keys.d.ts +19 -0
- package/dist/storage/catalog-keys.js +3 -1
- package/dist/storage/compressed-storage.js +1 -1
- package/dist/storage/index.d.ts +3 -1
- package/dist/storage/interface.d.ts +2 -0
- package/dist/storage/interface.js +1 -1
- package/dist/storage/key-prefixes.d.ts +1 -1
- package/dist/storage/key-prefixes.js +1 -0
- package/dist/storage/lmdb.d.ts +30 -2
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon.js +2 -2
- package/dist/storage/node-sqlite.js +3 -1
- 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/testing.js +1 -1
- package/dist/storage/turso.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/web-extension.js +1 -1
- package/dist/worker/protocol-messages.d.ts +17 -0
- package/dist/worker/protocol-schemas.d.ts +32 -0
- package/dist/worker/protocol-schemas.js +7 -3
- package/dist/worker/protocol-task-result.js +35 -3
- package/dist/worker/protocol.js +1 -1
- package/dist/worker/registry/types.d.ts +2 -0
- package/dist/worker/registry.d.ts +1 -1
- package/dist/worker/registry.js +3 -1
- package/dist/workers/workflow-runner.d.ts +9 -0
- package/dist/workers/workflow-runner.js +9 -2
- package/dist/workers/workflow-worker-entry.js +9 -4
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BatchOperation } from '../../storage/interface.ts';
|
|
1
|
+
import { type BatchOperation, type ConditionalBatchCondition } from '../../storage/interface.ts';
|
|
2
2
|
import type { ScheduleState, WorkflowState } from '../types.ts';
|
|
3
3
|
import type { EngineInternals } from './internals.ts';
|
|
4
4
|
import { type WorkflowClaimTransitionFragment } from './workflow-claim-transitions.ts';
|
|
@@ -84,11 +84,28 @@ export declare function commitExternalTerminalWorkflowStateOperations(internals:
|
|
|
84
84
|
export declare function loadScheduleState(internals: EngineInternals, scheduleId: string): Promise<ScheduleState | null>;
|
|
85
85
|
/** Load a schedule state. */
|
|
86
86
|
export declare function requireScheduleState(internals: EngineInternals, scheduleId: string): Promise<ScheduleState>;
|
|
87
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Persist schedule state and optionally write the next schedule timer.
|
|
89
|
+
*
|
|
90
|
+
* `extraConditions` (WFT-20) additionally fences the commit on caller-supplied
|
|
91
|
+
* preconditions — used by a `revisionPolicy: 'pinned'` create/update to fence
|
|
92
|
+
* the write on `buildCatalogEntryRevisionCondition(type, pinnedRevision)` so a
|
|
93
|
+
* concurrent `removeWorkflowRevision()` landing between pin capture and this
|
|
94
|
+
* commit cannot let a pin to an already-removed revision durably land. When
|
|
95
|
+
* the commit's `extraConditions` are the ones that lost the CAS,
|
|
96
|
+
* `onExtraConditionsLost` (required whenever `extraConditions` is non-empty)
|
|
97
|
+
* supplies the thrown error instead of this function's own generic
|
|
98
|
+
* lost-precondition error, so the caller sees an actionable, typed failure
|
|
99
|
+
* (e.g. `WorkflowRevisionUnavailableError`) rather than a bare "lost its
|
|
100
|
+
* precondition" message. Every existing caller passes neither option and gets
|
|
101
|
+
* byte-for-byte the pre-WFT-20 commit shape.
|
|
102
|
+
*/
|
|
88
103
|
export declare function writeScheduleState(internals: EngineInternals, state: ScheduleState, options?: {
|
|
89
104
|
includeTimer?: boolean;
|
|
90
105
|
replaceTimerFrom?: ScheduleState;
|
|
91
106
|
additionalOperations?: BatchOperation[];
|
|
107
|
+
extraConditions?: ConditionalBatchCondition[];
|
|
108
|
+
onExtraConditionsLost?: () => Error;
|
|
92
109
|
}): Promise<void>;
|
|
93
110
|
/** Load persisted workflow start headers by workflow ID. */
|
|
94
111
|
export declare function loadWorkflowStartHeaders(internals: EngineInternals, workflowId: string): Promise<Map<string, string> | undefined>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
KEYS
|
|
2
|
+
KEYS,
|
|
3
|
+
storageValuesEqual
|
|
3
4
|
} from "../../storage/interface.js";
|
|
4
5
|
import { encode } from "../codec.js";
|
|
5
6
|
import { buildTimerBatchOperations } from "../scheduler.js";
|
|
@@ -8,7 +9,10 @@ import {
|
|
|
8
9
|
clearPendingAtomicWorkflowCommitSideEffects,
|
|
9
10
|
takePendingAtomicWorkflowCommitSideEffects
|
|
10
11
|
} from "./checkpoint-side-effects.js";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
commitFencedEngineWrite,
|
|
14
|
+
commitFencedEngineWriteAllowingPreconditionFailure
|
|
15
|
+
} from "./fenced-write.js";
|
|
12
16
|
import { getWorkflowExecutionStartedAt } from "./handles.js";
|
|
13
17
|
import { resolveEffectiveScheduleFireAt } from "./schedule-jitter.js";
|
|
14
18
|
import { createScheduleTimerId, decodeWorkflowStartHeaders } from "./state-utilities.js";
|
|
@@ -150,8 +154,23 @@ export async function writeScheduleState(internals, state, options) {
|
|
|
150
154
|
], includeTimer = options?.includeTimer ?? state.status === "active";
|
|
151
155
|
operations.push(...buildScheduleTimerReplacementOperations(state, includeTimer, options?.replaceTimerFrom));
|
|
152
156
|
operations.push(...options?.additionalOperations ?? []);
|
|
157
|
+
const extraConditions = options?.extraConditions ?? [];
|
|
158
|
+
if (extraConditions.length > 0) {
|
|
159
|
+
await commitScheduleStateWithExtraConditions(internals, state, operations, extraConditions, options?.onExtraConditionsLost);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
153
162
|
await commitFencedEngineWrite(internals, null, operations, [], () => Error(`Schedule state commit for schedule "${state.id}" lost its precondition.`));
|
|
154
163
|
}
|
|
164
|
+
async function commitScheduleStateWithExtraConditions(internals, state, operations, extraConditions, onExtraConditionsLost) {
|
|
165
|
+
if (await commitFencedEngineWriteAllowingPreconditionFailure(internals, null, operations, extraConditions))
|
|
166
|
+
return;
|
|
167
|
+
for (const condition of extraConditions) {
|
|
168
|
+
const currentValue = await internals.storage.get(condition.key);
|
|
169
|
+
if (!storageValuesEqual(currentValue, condition.expectedValue))
|
|
170
|
+
throw onExtraConditionsLost?.() ?? Error(`Schedule state commit for schedule "${state.id}" lost its precondition.`);
|
|
171
|
+
}
|
|
172
|
+
throw Error(`Schedule state commit for schedule "${state.id}" lost its precondition.`);
|
|
173
|
+
}
|
|
155
174
|
export async function loadWorkflowStartHeaders(internals, workflowId) {
|
|
156
175
|
const bytes = await internals.storage.get(KEYS.workflowHeaders(workflowId));
|
|
157
176
|
if (!bytes)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EngineInternals } from '../internals.ts';
|
|
2
|
+
import type { RunnableFinalizer } from './finalizer-activity.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve `type`'s `finalizer` against the workflow instance's own EXACT
|
|
5
|
+
* pinned `revision` (WFT-17/WFT-19), awaiting a full dynamic-source resolve
|
|
6
|
+
* when it is `registerSource()`-registered but unresolved on this process —
|
|
7
|
+
* {@link getResolvedDynamicRegistration}'s sync-only lookup cannot close
|
|
8
|
+
* that gap. Without this, a type never started/resumed/recovered here after
|
|
9
|
+
* a fresh restart (only torn down) rearms its `wf-teardown:` timer forever,
|
|
10
|
+
* since nothing else triggers the resolve `lastResolvedRevisionByName`
|
|
11
|
+
* needs. Returns `undefined` (rearm) when `type` has no registration at
|
|
12
|
+
* all, when the pinned revision cannot be resolved on this process
|
|
13
|
+
* ({@link WorkflowRevisionUnavailableError}), or when the resolve itself
|
|
14
|
+
* fails ({@link DynamicWorkflowSourceUnavailableError}) — a load failure
|
|
15
|
+
* here doesn't fail the (already terminal) workflow, it just defers to the
|
|
16
|
+
* next self-heal attempt.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveFinalizerRegistration(internals: EngineInternals, type: string, revision: string | undefined): Promise<RunnableFinalizer | undefined>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DynamicWorkflowSourceUnavailableError } from "../dynamic-source-errors.js";
|
|
2
|
+
import {
|
|
3
|
+
getResolvedDynamicRegistration,
|
|
4
|
+
resolveExecutableRegistrationForRevision
|
|
5
|
+
} from "../dynamic-source-execution.js";
|
|
6
|
+
import { WorkflowRevisionUnavailableError } from "../revision-errors.js";
|
|
7
|
+
export async function resolveFinalizerRegistration(internals, type, revision) {
|
|
8
|
+
const eagerOrAlreadyResolved = getResolvedDynamicRegistration(internals, type, revision)?.finalizer;
|
|
9
|
+
if (eagerOrAlreadyResolved !== void 0)
|
|
10
|
+
return eagerOrAlreadyResolved;
|
|
11
|
+
if (!internals.sources.byName.has(type))
|
|
12
|
+
return;
|
|
13
|
+
try {
|
|
14
|
+
const { entry } = await resolveExecutableRegistrationForRevision(internals.engine, internals, type, revision);
|
|
15
|
+
return entry.finalizer;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error instanceof DynamicWorkflowSourceUnavailableError || error instanceof WorkflowRevisionUnavailableError)
|
|
18
|
+
return;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -3,29 +3,26 @@
|
|
|
3
3
|
* Dispatched by the scheduler when a `wf-teardown:` timer fires, it drives a
|
|
4
4
|
* workflow's definition-level `finalizer` to durable completion after a
|
|
5
5
|
* `cancelled`/`timed-out` terminal — claiming the durable teardown marker, running
|
|
6
|
-
* the finalizer activity (via {@link runFinalizerActivity}), and then clearing,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* the finalizer activity (via {@link runFinalizerActivity}), and then clearing, backing
|
|
7
|
+
* off, or dead-lettering based on the outcome. The byte-level claim mechanics (CAS,
|
|
8
|
+
* settle, re-arm, dead-letter, stale horizon, backoff) live in `./finalizer-claim.ts`;
|
|
9
|
+
* this module is the orchestration that decides which to call.
|
|
10
10
|
*
|
|
11
|
-
* Concurrency model — a single durable, TIME-based claim:
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* be re-driven concurrently, which is why workflow finalizers must be idempotent (a
|
|
22
|
-
* contract of the #446 design).
|
|
11
|
+
* Concurrency model — a single durable, TIME-based claim: the durable `teardownOwed`
|
|
12
|
+
* marker carries a `{ status, attempts, token, claimedAt }` claim ({@link TeardownClaim}).
|
|
13
|
+
* A holder fenced-CAS's `owed → running` (stamping `claimedAt`) before running, and
|
|
14
|
+
* settle-CAS's the exact `running` bytes it wrote when clearing or rescheduling — so a
|
|
15
|
+
* concurrent reclaimer can never clobber a fresher claim. Liveness is decided purely by
|
|
16
|
+
* the clock: a `running` claim is reclaimable once `claimedAt` is older than
|
|
17
|
+
* {@link teardownStaleThresholdMs}. There is NO in-memory liveness set and NO epoch in
|
|
18
|
+
* the record; crash recovery is an ordinary stale-claim retry driven by the timer that
|
|
19
|
+
* survived the terminal batch — the tradeoff is a finalizer running past the stale
|
|
20
|
+
* threshold may be re-driven concurrently, which is why finalizers must be idempotent.
|
|
23
21
|
*
|
|
24
22
|
* Self-heal invariant: every exit that does NOT settle the claim (a lost claim CAS, a
|
|
25
23
|
* presumed-live `running` claim, a shutdown-aborted attempt, or a missing registration)
|
|
26
|
-
* re-arms a future `wf-teardown:` timer before returning
|
|
27
|
-
*
|
|
28
|
-
* re-arm would strand the marker with no timer to re-drive it.
|
|
24
|
+
* re-arms a future `wf-teardown:` timer before returning — the scheduler deletes the fired
|
|
25
|
+
* timer on return, so a non-settling exit that forgot to re-arm strands the marker.
|
|
29
26
|
*
|
|
30
27
|
* @module core/engine/termination/finalizer
|
|
31
28
|
*/
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
teardownStaleThresholdMs,
|
|
19
19
|
teardownTimerOperations
|
|
20
20
|
} from "./finalizer-claim.js";
|
|
21
|
+
import { resolveFinalizerRegistration } from "./finalizer-registration.js";
|
|
21
22
|
export { teardownStaleThresholdMs } from "./finalizer-claim.js";
|
|
22
23
|
const TERMINAL_STATUSES_OWED_TEARDOWN = new Set([
|
|
23
24
|
"cancelled",
|
|
@@ -53,10 +54,9 @@ async function resolveTeardownDrive(internals, workflowId, token, callbacks) {
|
|
|
53
54
|
const state = await callbacks.loadWorkflowState(workflowId);
|
|
54
55
|
if (state === null || !TERMINAL_STATUSES_OWED_TEARDOWN.has(state.status))
|
|
55
56
|
return clearOrRearm(internals, workflowId, token, markerBytes);
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
57
|
+
const finalizer = await resolveFinalizerRegistration(internals, state.type, state.revision);
|
|
58
|
+
if (finalizer === void 0)
|
|
58
59
|
return { kind: "rearm", token };
|
|
59
|
-
const finalizer = registeredFinalizer;
|
|
60
60
|
if (!runningClaimIsStale(internals, claim, finalizer))
|
|
61
61
|
return { kind: "rearm", token };
|
|
62
62
|
const finalizerStateBytes = await internals.storage.get(KEYS.finalizerState(workflowId));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {@link TimeOperationCallbacks} — the callback bundle `operations-time.ts`'s
|
|
3
|
+
* timer-fire handlers (delayed start, sleep, deadline, schedule) take. Split
|
|
4
|
+
* out of that file, which has no headroom under the repository's 500-line
|
|
5
|
+
* implementation-file ceiling for this type inline.
|
|
6
|
+
*
|
|
7
|
+
* @module core/engine/time-operation-callbacks
|
|
8
|
+
*/
|
|
9
|
+
import type { Checkpoint, Duration, TimerEntry, WorkflowState } from '../types.ts';
|
|
10
|
+
import type { WorkflowVersionTuple } from '../workflow-version-tuple.ts';
|
|
11
|
+
import type { ExecutableRegistration } from './dynamic-source-execution.ts';
|
|
12
|
+
type RegistrationEntry = ExecutableRegistration['entry'];
|
|
13
|
+
export type TimeOperationCallbacks = {
|
|
14
|
+
completeOperation: (workflowId: string, value: unknown) => void;
|
|
15
|
+
dispatchEvent: (event: Event) => void;
|
|
16
|
+
loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>;
|
|
17
|
+
failWorkflow: (workflowId: string, error: Error) => Promise<void>;
|
|
18
|
+
runSerializedWorkflowStateWrite: <Result>(workflowId: string, writeOperation: () => Promise<Result>) => Promise<Result>;
|
|
19
|
+
beginWorkflowExecution: (workflowId: string, workflowExecutionToken: string | undefined, workflowType: string, revision: string | undefined, input: unknown, checkpoint: Checkpoint, executionDeadline: number | undefined, executionStateOwnerId: string, registration: RegistrationEntry) => void;
|
|
20
|
+
workflowVersionTupleFromState: (state: WorkflowState) => WorkflowVersionTuple;
|
|
21
|
+
setWorkflowStartHeaders: (workflowId: string, headers: Map<string, string> | undefined) => void;
|
|
22
|
+
loadWorkflowStartHeaders: (workflowId: string) => Promise<Map<string, string> | undefined>;
|
|
23
|
+
parseStartOptionDuration: (value: Duration, fieldName: 'options.executionTimeout' | 'options.startAfter') => number;
|
|
24
|
+
runDeferredTerminalCleanup: (workflowId: string, timerId: string) => Promise<void>;
|
|
25
|
+
runWorkflowFinalizer: (workflowId: string, timerId: string) => Promise<void>;
|
|
26
|
+
handleScheduleTimer: (entry: TimerEntry) => Promise<void>;
|
|
27
|
+
timeout: (workflowId: string) => Promise<void>;
|
|
28
|
+
handleCleanupError: (source: string, error: unknown, workflowId: string) => void;
|
|
29
|
+
/** Resolve `type` against an EXACT pinned `WorkflowState.revision` (WFT-17, `undefined` for a legacy record) — never the catalog's active pointer. Used by a delayed-start fire, matching `resumeWorkflowFromStorage()`. */
|
|
30
|
+
resolveExecutableRegistrationForRevision: (type: string, revision: string | undefined) => Promise<ExecutableRegistration>;
|
|
31
|
+
};
|
|
32
|
+
export {};
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode a persisted schedule record's cadence fields (`cronExpression` XOR
|
|
3
|
+
* `intervalMs`). Split out of `schedule.ts` purely to stay under this
|
|
4
|
+
* repository's 500-line-per-file ceiling (the same precedent
|
|
5
|
+
* `lifecycle/start.ts`/`decode-revision.ts` set for WFT-17/18) — there is no
|
|
6
|
+
* behavioral reason to import this module directly instead of `schedule.ts`,
|
|
7
|
+
* which re-exports nothing from here because `decodeScheduleCadence` is
|
|
8
|
+
* consumed only by `decodeScheduleIdentityFields` in that file.
|
|
9
|
+
*
|
|
10
|
+
* @module core/engine/validation/schedule-cadence
|
|
11
|
+
*/
|
|
12
|
+
/** Decode the cadence fields (`cronExpression` XOR `intervalMs`) of a persisted schedule record, or `null` (and warn) on a malformed record. */
|
|
13
|
+
export declare function decodeScheduleCadence(decoded: Record<string, unknown>, scheduleId: string): {
|
|
14
|
+
cronExpression?: string;
|
|
15
|
+
intervalMs?: number;
|
|
16
|
+
} | null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { parseCronExpression } from "../../schedule.js";
|
|
2
|
+
import { rejectInvalidScheduleRecord } from "./schedule-warnings.js";
|
|
3
|
+
export function decodeScheduleCadence(decoded, scheduleId) {
|
|
4
|
+
const { cronExpression, intervalMs } = decoded, hasCron = cronExpression !== void 0, hasInterval = intervalMs !== void 0;
|
|
5
|
+
if (hasCron === hasInterval)
|
|
6
|
+
return rejectInvalidScheduleRecord(scheduleId, "with conflicting or missing cadence (expected exactly one of cronExpression or intervalMs)");
|
|
7
|
+
if (hasInterval) {
|
|
8
|
+
if (typeof intervalMs !== "number" || !Number.isSafeInteger(intervalMs) || intervalMs <= 0)
|
|
9
|
+
return rejectInvalidScheduleRecord(scheduleId, "with invalid intervalMs");
|
|
10
|
+
return { intervalMs };
|
|
11
|
+
}
|
|
12
|
+
if (typeof cronExpression !== "string")
|
|
13
|
+
return rejectInvalidScheduleRecord(scheduleId, "with invalid cronExpression");
|
|
14
|
+
try {
|
|
15
|
+
parseCronExpression(cronExpression);
|
|
16
|
+
} catch {
|
|
17
|
+
return rejectInvalidScheduleRecord(scheduleId, "with invalid cronExpression");
|
|
18
|
+
}
|
|
19
|
+
return { cronExpression };
|
|
20
|
+
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import type { ScheduleOptions, ScheduleOverlapPolicy, ScheduleUpdateOptions } from '../../types.ts';
|
|
1
|
+
import type { ScheduleOptions, ScheduleOverlapPolicy, ScheduleRevisionPolicy, ScheduleUpdateOptions } from '../../types.ts';
|
|
2
2
|
export declare const SCHEDULE_OVERLAP_POLICIES: Set<ScheduleOverlapPolicy>;
|
|
3
|
+
export declare const SCHEDULE_REVISION_POLICIES: Set<ScheduleRevisionPolicy>;
|
|
3
4
|
type NormalizedScheduleOptions = Required<Pick<ScheduleOptions, 'overlap' | 'backfill'>> & {
|
|
4
5
|
id?: string;
|
|
5
6
|
description?: string;
|
|
6
7
|
jitterMs?: number;
|
|
8
|
+
revisionPolicy?: ScheduleRevisionPolicy;
|
|
7
9
|
};
|
|
8
10
|
type NormalizedScheduleUpdateOptions = {
|
|
9
11
|
description?: string;
|
|
10
12
|
overlap?: ScheduleOverlapPolicy;
|
|
11
13
|
backfill?: boolean;
|
|
12
14
|
jitterMs?: number;
|
|
15
|
+
revisionPolicy?: ScheduleRevisionPolicy;
|
|
13
16
|
};
|
|
14
17
|
export declare function normalizeScheduleOptions(options: ScheduleOptions | undefined): NormalizedScheduleOptions;
|
|
15
18
|
export declare function normalizeScheduleUpdateOptions(options: ScheduleUpdateOptions | undefined): NormalizedScheduleUpdateOptions;
|
|
@@ -5,6 +5,9 @@ export const SCHEDULE_OVERLAP_POLICIES = new Set([
|
|
|
5
5
|
"queue",
|
|
6
6
|
"cancel-running",
|
|
7
7
|
"allow"
|
|
8
|
+
]), SCHEDULE_REVISION_POLICIES = new Set([
|
|
9
|
+
"active-at-fire",
|
|
10
|
+
"pinned"
|
|
8
11
|
]);
|
|
9
12
|
export function normalizeScheduleOptions(options) {
|
|
10
13
|
if (options === void 0)
|
|
@@ -20,6 +23,9 @@ export function normalizeScheduleOptions(options) {
|
|
|
20
23
|
},
|
|
21
24
|
...options.jitter !== void 0 && {
|
|
22
25
|
jitterMs: normalizeScheduleJitter(options.jitter, "options.jitter")
|
|
26
|
+
},
|
|
27
|
+
...options.revisionPolicy !== void 0 && {
|
|
28
|
+
revisionPolicy: normalizeScheduleRevisionPolicy(options.revisionPolicy, "options.revisionPolicy")
|
|
23
29
|
}
|
|
24
30
|
};
|
|
25
31
|
}
|
|
@@ -38,9 +44,17 @@ export function normalizeScheduleUpdateOptions(options) {
|
|
|
38
44
|
},
|
|
39
45
|
...options.jitter !== void 0 && {
|
|
40
46
|
jitterMs: normalizeScheduleJitter(options.jitter, "options.jitter")
|
|
47
|
+
},
|
|
48
|
+
...options.revisionPolicy !== void 0 && {
|
|
49
|
+
revisionPolicy: normalizeScheduleRevisionPolicy(options.revisionPolicy, "options.revisionPolicy")
|
|
41
50
|
}
|
|
42
51
|
};
|
|
43
52
|
}
|
|
53
|
+
function normalizeScheduleRevisionPolicy(value, fieldName) {
|
|
54
|
+
if (!SCHEDULE_REVISION_POLICIES.has(value))
|
|
55
|
+
throw Error(`${fieldName} must be one of ${[...SCHEDULE_REVISION_POLICIES].join(", ")}`);
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
44
58
|
function normalizeScheduleDescription(description) {
|
|
45
59
|
if (typeof description !== "string")
|
|
46
60
|
throw Error("options.description must be a string when provided");
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode a persisted schedule record's revision-policy fields
|
|
3
|
+
* (`revisionPolicy` / `pinnedRevision`, WFT-20). Split out of `schedule.ts`
|
|
4
|
+
* purely to stay under this repository's 500-line-per-file ceiling — see
|
|
5
|
+
* that file's own precedent (`lifecycle/start.ts`/`decode-revision.ts` for
|
|
6
|
+
* WFT-17/18) — there is no behavioral reason to import this module directly
|
|
7
|
+
* instead of `schedule.ts`, which composes it into `decodeScheduleState`.
|
|
8
|
+
*
|
|
9
|
+
* @module core/engine/validation/schedule-revision
|
|
10
|
+
*/
|
|
11
|
+
import type { ScheduleState } from '../../types.ts';
|
|
12
|
+
type ScheduleRevisionFields = Pick<ScheduleState, 'revisionPolicy' | 'pinnedRevision'>;
|
|
13
|
+
/**
|
|
14
|
+
* Decode `revisionPolicy`/`pinnedRevision` from a persisted schedule record.
|
|
15
|
+
* A record with NO `revisionPolicy` field predates WFT-20 and decodes as
|
|
16
|
+
* `'active-at-fire'` — the same "absent means legacy, not corrupt" treatment
|
|
17
|
+
* `WorkflowState.revision` got (WFT-17/18) — never rejected as malformed. An
|
|
18
|
+
* explicit but unrecognized `revisionPolicy` string, or a `pinnedRevision`
|
|
19
|
+
* present while `revisionPolicy !== 'pinned'` (or absent while it IS
|
|
20
|
+
* `'pinned'`), both reject the whole record via
|
|
21
|
+
* {@link rejectInvalidScheduleRecord} rather than silently coercing.
|
|
22
|
+
*/
|
|
23
|
+
export declare function decodeScheduleRevisionPolicyFields(decoded: Record<string, unknown>, scheduleId: string): ScheduleRevisionFields | null;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SCHEDULE_REVISION_POLICIES } from "./schedule-options.js";
|
|
2
|
+
import { rejectInvalidScheduleRecord } from "./schedule-warnings.js";
|
|
3
|
+
export function decodeScheduleRevisionPolicyFields(decoded, scheduleId) {
|
|
4
|
+
const rawRevisionPolicy = decoded.revisionPolicy;
|
|
5
|
+
if (rawRevisionPolicy === void 0) {
|
|
6
|
+
if (decoded.pinnedRevision !== void 0)
|
|
7
|
+
return rejectInvalidScheduleRecord(scheduleId, "with a pinnedRevision but no revisionPolicy");
|
|
8
|
+
return { revisionPolicy: "active-at-fire" };
|
|
9
|
+
}
|
|
10
|
+
if (!SCHEDULE_REVISION_POLICIES.has(rawRevisionPolicy))
|
|
11
|
+
return rejectInvalidScheduleRecord(scheduleId, "with invalid revisionPolicy");
|
|
12
|
+
const revisionPolicy = rawRevisionPolicy, pinnedRevision = decoded.pinnedRevision, hasPinnedRevision = pinnedRevision !== void 0;
|
|
13
|
+
if (revisionPolicy === "pinned") {
|
|
14
|
+
if (!hasPinnedRevision || typeof pinnedRevision !== "string" || pinnedRevision.length === 0)
|
|
15
|
+
return rejectInvalidScheduleRecord(scheduleId, 'with revisionPolicy "pinned" but a missing or invalid pinnedRevision');
|
|
16
|
+
return { revisionPolicy, pinnedRevision };
|
|
17
|
+
}
|
|
18
|
+
if (hasPinnedRevision)
|
|
19
|
+
return rejectInvalidScheduleRecord(scheduleId, 'with a pinnedRevision but revisionPolicy is not "pinned"');
|
|
20
|
+
return { revisionPolicy };
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared "ignore this malformed persisted schedule record" warning
|
|
3
|
+
* helper, split into its own leaf module so both `schedule.ts` and its
|
|
4
|
+
* decode helpers (`schedule-cadence.ts`, `schedule-revision.ts`) can import
|
|
5
|
+
* it without an import cycle — `schedule.ts` re-exports it so every existing
|
|
6
|
+
* caller keeps importing it from there.
|
|
7
|
+
*
|
|
8
|
+
* @module core/engine/validation/schedule-warnings
|
|
9
|
+
*/
|
|
10
|
+
/** Warn and return `null` for a persisted schedule record that failed decode validation. */
|
|
11
|
+
export declare function rejectInvalidScheduleRecord(scheduleId: string | undefined, message: string): null;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { ScheduleFilter, ScheduleOverlapPolicy, ScheduleSpec, ScheduleState, ScheduleStatus } from '../../types.ts';
|
|
2
|
-
export { normalizeScheduleOptions, normalizeScheduleUpdateOptions, SCHEDULE_OVERLAP_POLICIES, } from './schedule-options.ts';
|
|
1
|
+
import type { ScheduleFilter, ScheduleOverlapPolicy, ScheduleRevisionPolicy, ScheduleSpec, ScheduleState, ScheduleStatus } from '../../types.ts';
|
|
2
|
+
export { normalizeScheduleOptions, normalizeScheduleUpdateOptions, SCHEDULE_OVERLAP_POLICIES, SCHEDULE_REVISION_POLICIES, } from './schedule-options.ts';
|
|
3
|
+
export { rejectInvalidScheduleRecord } from './schedule-warnings.ts';
|
|
3
4
|
export declare const SCHEDULE_STATUSES: Set<ScheduleStatus>;
|
|
4
5
|
export declare function isValidScheduleTimestamp(value: unknown): value is number;
|
|
5
6
|
export declare function isValidScheduleStatus(value: unknown): value is ScheduleStatus;
|
|
6
7
|
export declare function isValidScheduleOverlapPolicy(value: unknown): value is ScheduleOverlapPolicy;
|
|
8
|
+
export declare function isValidScheduleRevisionPolicy(value: unknown): value is ScheduleRevisionPolicy;
|
|
7
9
|
export declare function isValidScheduleIdentifier(value: unknown): value is string;
|
|
8
10
|
export declare function coerceScheduleId(scheduleId: string, fieldName: string): string;
|
|
9
11
|
/**
|
|
@@ -24,7 +26,6 @@ export type NormalizedScheduleSpec = {
|
|
|
24
26
|
*/
|
|
25
27
|
export declare function normalizeScheduleSpec(spec: string | ScheduleSpec): NormalizedScheduleSpec;
|
|
26
28
|
export declare function normalizeScheduleFilter(filter: ScheduleFilter | undefined): ScheduleFilter | undefined;
|
|
27
|
-
export declare function rejectInvalidScheduleRecord(scheduleId: string | undefined, message: string): null;
|
|
28
29
|
export declare function decodeScheduleIdentityFields(decoded: Record<string, unknown>): (Pick<ScheduleState, 'id' | 'workflowType' | 'status' | 'overlap'> & {
|
|
29
30
|
cronExpression?: string;
|
|
30
31
|
intervalMs?: number;
|
|
@@ -3,12 +3,17 @@ import { isRecord } from "../../debug-output.js";
|
|
|
3
3
|
import { parseCronExpression } from "../../schedule.js";
|
|
4
4
|
import { parseDuration } from "../../scheduler.js";
|
|
5
5
|
import { coerceStartWorkflowId } from "../../start-workflow-validation.js";
|
|
6
|
-
import {
|
|
6
|
+
import { decodeScheduleCadence } from "./schedule-cadence.js";
|
|
7
|
+
import { SCHEDULE_OVERLAP_POLICIES, SCHEDULE_REVISION_POLICIES } from "./schedule-options.js";
|
|
8
|
+
import { decodeScheduleRevisionPolicyFields } from "./schedule-revision.js";
|
|
9
|
+
import { rejectInvalidScheduleRecord } from "./schedule-warnings.js";
|
|
7
10
|
export {
|
|
8
11
|
normalizeScheduleOptions,
|
|
9
12
|
normalizeScheduleUpdateOptions,
|
|
10
|
-
SCHEDULE_OVERLAP_POLICIES
|
|
13
|
+
SCHEDULE_OVERLAP_POLICIES,
|
|
14
|
+
SCHEDULE_REVISION_POLICIES
|
|
11
15
|
} from "./schedule-options.js";
|
|
16
|
+
export { rejectInvalidScheduleRecord } from "./schedule-warnings.js";
|
|
12
17
|
export const SCHEDULE_STATUSES = new Set(["active", "paused", "cancelled"]);
|
|
13
18
|
export function isValidScheduleTimestamp(value) {
|
|
14
19
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
@@ -19,6 +24,9 @@ export function isValidScheduleStatus(value) {
|
|
|
19
24
|
export function isValidScheduleOverlapPolicy(value) {
|
|
20
25
|
return typeof value === "string" && SCHEDULE_OVERLAP_POLICIES.has(value);
|
|
21
26
|
}
|
|
27
|
+
export function isValidScheduleRevisionPolicy(value) {
|
|
28
|
+
return typeof value === "string" && SCHEDULE_REVISION_POLICIES.has(value);
|
|
29
|
+
}
|
|
22
30
|
export function isValidScheduleIdentifier(value) {
|
|
23
31
|
if (typeof value !== "string")
|
|
24
32
|
return !1;
|
|
@@ -94,29 +102,6 @@ export function normalizeScheduleFilter(filter) {
|
|
|
94
102
|
validateScheduleFilterBound(filter.offset, "offset");
|
|
95
103
|
return filter;
|
|
96
104
|
}
|
|
97
|
-
export function rejectInvalidScheduleRecord(scheduleId, message) {
|
|
98
|
-
const prefix = scheduleId === void 0 ? "[weft] Ignoring malformed schedule record" : `[weft] Ignoring malformed schedule "${scheduleId}"`;
|
|
99
|
-
console.warn(`${prefix} ${message}.`);
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
function decodeScheduleCadence(decoded, scheduleId) {
|
|
103
|
-
const { cronExpression, intervalMs } = decoded, hasCron = cronExpression !== void 0, hasInterval = intervalMs !== void 0;
|
|
104
|
-
if (hasCron === hasInterval)
|
|
105
|
-
return rejectInvalidScheduleRecord(scheduleId, "with conflicting or missing cadence (expected exactly one of cronExpression or intervalMs)");
|
|
106
|
-
if (hasInterval) {
|
|
107
|
-
if (typeof intervalMs !== "number" || !Number.isSafeInteger(intervalMs) || intervalMs <= 0)
|
|
108
|
-
return rejectInvalidScheduleRecord(scheduleId, "with invalid intervalMs");
|
|
109
|
-
return { intervalMs };
|
|
110
|
-
}
|
|
111
|
-
if (typeof cronExpression !== "string")
|
|
112
|
-
return rejectInvalidScheduleRecord(scheduleId, "with invalid cronExpression");
|
|
113
|
-
try {
|
|
114
|
-
parseCronExpression(cronExpression);
|
|
115
|
-
} catch {
|
|
116
|
-
return rejectInvalidScheduleRecord(scheduleId, "with invalid cronExpression");
|
|
117
|
-
}
|
|
118
|
-
return { cronExpression };
|
|
119
|
-
}
|
|
120
105
|
export function decodeScheduleIdentityFields(decoded) {
|
|
121
106
|
const id = decoded.id;
|
|
122
107
|
if (!isValidScheduleIdentifier(id))
|
|
@@ -307,9 +292,13 @@ export function decodeScheduleState(bytes) {
|
|
|
307
292
|
const runtime = decodeScheduleRuntimeFields(decoded, identity.id);
|
|
308
293
|
if (!runtime)
|
|
309
294
|
return null;
|
|
295
|
+
const revisionFields = decodeScheduleRevisionPolicyFields(decoded, identity.id);
|
|
296
|
+
if (!revisionFields)
|
|
297
|
+
return null;
|
|
310
298
|
return {
|
|
311
299
|
...identity,
|
|
312
300
|
input: decoded.input,
|
|
313
|
-
...runtime
|
|
301
|
+
...runtime,
|
|
302
|
+
...revisionFields
|
|
314
303
|
};
|
|
315
304
|
}
|
|
@@ -4,6 +4,7 @@ import { normalizeFailureCategory } from "../failure-categories.js";
|
|
|
4
4
|
import { coerceStartWorkflowId, parseStartWorkflowDuration } from "../start-workflow-validation.js";
|
|
5
5
|
import { DEFAULT_WORKFLOW_VERSION } from "../versioning.js";
|
|
6
6
|
import { isWorkflowTagArray } from "../workflow-tags.js";
|
|
7
|
+
import { sanitizeDecodedRevision } from "./decode-revision.js";
|
|
7
8
|
import {
|
|
8
9
|
MAX_TIMELINE_COORDINATOR_DETAILS,
|
|
9
10
|
MAX_TIMELINE_DETAIL_STRING_LENGTH
|
|
@@ -30,6 +31,7 @@ const WORKFLOW_STATE_FIELD_NAMES = new Set(defineCompleteWorkflowStateFieldNames
|
|
|
30
31
|
"failureCategory",
|
|
31
32
|
"terminationReason",
|
|
32
33
|
"versionTuple",
|
|
34
|
+
"revision",
|
|
33
35
|
"workflowExecutionToken",
|
|
34
36
|
"executionStateOwnerId",
|
|
35
37
|
"parentWorkflowId",
|
|
@@ -140,6 +142,7 @@ export function decodeWorkflowState(bytes) {
|
|
|
140
142
|
}
|
|
141
143
|
sanitizeDecodedParentLineage(state);
|
|
142
144
|
sanitizeDecodedRestartLineage(state);
|
|
145
|
+
sanitizeDecodedRevision(state);
|
|
143
146
|
return decodedRecord === void 0 ? state : stripUnknownWorkflowStateFields(state, decodedRecord);
|
|
144
147
|
}
|
|
145
148
|
function sanitizeDecodedParentLineage(state) {
|
|
@@ -104,4 +104,4 @@ export declare function createWorkflowClaimReclaimTarget(registry: WorkflowClaim
|
|
|
104
104
|
* every existing caller/test that does not care about mixed workflow-type
|
|
105
105
|
* fleets keeps working unchanged.
|
|
106
106
|
*/
|
|
107
|
-
isTypeRegistered?: (workflowType: string) => boolean): WorkflowClaimReclaimTargetHandle;
|
|
107
|
+
isTypeRegistered?: (workflowType: string, revision: string | undefined) => boolean): WorkflowClaimReclaimTargetHandle;
|
|
@@ -17,7 +17,21 @@ async function isWorkflowTypeRegistered(storage, workflowId, isTypeRegistered) {
|
|
|
17
17
|
if (bytes === null)
|
|
18
18
|
return !1;
|
|
19
19
|
try {
|
|
20
|
-
|
|
20
|
+
const state = decodeWorkflowState(bytes);
|
|
21
|
+
return isTypeRegistered(state.type, state.revision);
|
|
22
|
+
} catch {
|
|
23
|
+
return !1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function isWorkflowStillRunningAndEligible(storage, workflowId, isTypeRegistered) {
|
|
27
|
+
const bytes = await storage.get(KEYS.workflow(workflowId));
|
|
28
|
+
if (bytes === null)
|
|
29
|
+
return !1;
|
|
30
|
+
try {
|
|
31
|
+
const state = decodeWorkflowState(bytes);
|
|
32
|
+
if (state.status !== "running")
|
|
33
|
+
return !1;
|
|
34
|
+
return isTypeRegistered === void 0 || isTypeRegistered(state.type, state.revision);
|
|
21
35
|
} catch {
|
|
22
36
|
return !1;
|
|
23
37
|
}
|
|
@@ -43,7 +57,7 @@ export function createWorkflowClaimReclaimTarget(registry, storage, metrics, onR
|
|
|
43
57
|
await registry.release(workflowId);
|
|
44
58
|
}
|
|
45
59
|
async function confirmStillRunningOrReleaseFreshClaim(workflowId, acquiredEpoch) {
|
|
46
|
-
if (await
|
|
60
|
+
if (await isWorkflowStillRunningAndEligible(storage, workflowId, isTypeRegistered))
|
|
47
61
|
return !0;
|
|
48
62
|
if (registry.currentEpoch(workflowId) === acquiredEpoch)
|
|
49
63
|
await registry.release(workflowId);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WorkflowState } from '../types.ts';
|
|
2
|
+
import type { EngineInternals } from './internals.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The timestamp at which `state` becomes eligible for retention-driven
|
|
5
|
+
* purge, or `null` when it is non-terminal, has no applicable retention
|
|
6
|
+
* window, or (WFT-19 review round 2) its own pinned revision's retention
|
|
7
|
+
* policy cannot be resolved on this process right now — an unresolvable pin
|
|
8
|
+
* is NOT purge-eligible under the engine default; it is re-examined on a
|
|
9
|
+
* later sweep once the pin becomes resolvable, rather than risking an
|
|
10
|
+
* irreversible early purge against a shorter policy than the run's own.
|
|
11
|
+
* Falls back to the resolved dynamic definition's own retention policy for a
|
|
12
|
+
* `registerSource()`-registered type (see `resolveRetentionPolicyForState()`),
|
|
13
|
+
* then the engine-wide default for any other type — mirroring `retention.ts`'s
|
|
14
|
+
* `resolveWorkflowTypeRetention`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getWorkflowRetentionDeadline(internals: EngineInternals, state: WorkflowState): Promise<number | null>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DynamicWorkflowSourceUnavailableError } from "./dynamic-source-errors.js";
|
|
2
|
+
import {
|
|
3
|
+
getResolvedDynamicRegistration,
|
|
4
|
+
resolveExecutableRegistrationForRevision
|
|
5
|
+
} from "./dynamic-source-execution.js";
|
|
6
|
+
import { WorkflowRevisionUnavailableError } from "./revision-errors.js";
|
|
7
|
+
import { isTerminalWorkflowStatus, resolveRetentionForStatus } from "./validation.js";
|
|
8
|
+
async function resolveRetentionPolicyForState(internals, state) {
|
|
9
|
+
const eagerOrAlreadyResolved = getResolvedDynamicRegistration(internals, state.type, state.revision)?.retention;
|
|
10
|
+
if (eagerOrAlreadyResolved !== void 0)
|
|
11
|
+
return { kind: "resolved", policy: eagerOrAlreadyResolved };
|
|
12
|
+
if (!internals.sources.byName.has(state.type))
|
|
13
|
+
return { kind: "resolved", policy: void 0 };
|
|
14
|
+
try {
|
|
15
|
+
const { entry } = await resolveExecutableRegistrationForRevision(internals.engine, internals, state.type, state.revision);
|
|
16
|
+
return { kind: "resolved", policy: entry.retention };
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (error instanceof DynamicWorkflowSourceUnavailableError || error instanceof WorkflowRevisionUnavailableError)
|
|
19
|
+
return { kind: "unresolvable" };
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function getWorkflowRetentionDeadline(internals, state) {
|
|
24
|
+
if (!isTerminalWorkflowStatus(state.status))
|
|
25
|
+
return null;
|
|
26
|
+
const resolution = await resolveRetentionPolicyForState(internals, state);
|
|
27
|
+
if (resolution.kind === "unresolvable")
|
|
28
|
+
return null;
|
|
29
|
+
const policy = resolution.policy ?? internals.options.retention, retentionMs = resolveRetentionForStatus(policy, state.status);
|
|
30
|
+
if (retentionMs === void 0)
|
|
31
|
+
return null;
|
|
32
|
+
return state.updatedAt + retentionMs;
|
|
33
|
+
}
|
|
@@ -7,6 +7,7 @@ import type { SignalDeliveredEvent, SignalReceivedEvent } from './signal-events.
|
|
|
7
7
|
import type { AlertFiredEvent, AlertResolvedEvent, CheckpointSizeWarningEvent, CleanupWarningEvent, ConstraintViolatedEvent, DevelopmentWarningEvent, StorageSizeReportedEvent, WorkerConnectedEvent, WorkerDisconnectedEvent } from './system-events.ts';
|
|
8
8
|
import type { UpdateCompletedEvent, UpdateReceivedEvent } from './update-events.ts';
|
|
9
9
|
import type { WorkflowCancelledEvent, WorkflowCompletedEvent, WorkflowDefinitionRegisteredEvent, WorkflowFailedEvent, WorkflowRecoverySkippedEvent, WorkflowResumedEvent, WorkflowStartedEvent, WorkflowSuspendedEvent, WorkflowTeardownEvent, WorkflowTimedOutEvent } from './workflow-events.ts';
|
|
10
|
+
import type { WorkflowSourceLoadCancelledEvent, WorkflowSourceLoadFailedEvent, WorkflowSourceLoadReadyEvent, WorkflowSourceLoadStartedEvent } from './workflow-source-events.ts';
|
|
10
11
|
/**
|
|
11
12
|
* Record mapping each event-name string the {@link Engine} dispatches to its
|
|
12
13
|
* corresponding typed `Event` subclass.
|
|
@@ -63,6 +64,10 @@ export type WeftEventMap = {
|
|
|
63
64
|
'catalog:activation-rejected': WorkflowRevisionActivationRejectedEvent;
|
|
64
65
|
'catalog:revision-draining': WorkflowRevisionDrainingEvent;
|
|
65
66
|
'catalog:revision-removed': WorkflowRevisionRemovedEvent;
|
|
67
|
+
'workflow-source:load-started': WorkflowSourceLoadStartedEvent;
|
|
68
|
+
'workflow-source:load-ready': WorkflowSourceLoadReadyEvent;
|
|
69
|
+
'workflow-source:load-failed': WorkflowSourceLoadFailedEvent;
|
|
70
|
+
'workflow-source:load-cancelled': WorkflowSourceLoadCancelledEvent;
|
|
66
71
|
};
|
|
67
72
|
/**
|
|
68
73
|
* Typed version of the `EventTarget` interface that constrains
|