@lostgradient/weft 0.24.0 → 0.24.2
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.d.ts +1 -1
- package/dist/core/engine/bulk-operations-purge.js +17 -22
- package/dist/core/engine/bulk-operations-retry.d.ts +24 -0
- package/dist/core/engine/bulk-operations-retry.js +190 -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.d.ts +3 -2
- package/dist/core/engine/bulk-operations.js +10 -169
- package/dist/core/engine/callback-creators-bundles.js +5 -3
- 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/child-workflow.d.ts +13 -1
- package/dist/core/engine/child-workflow.js +22 -4
- 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/generation-codec.d.ts +22 -0
- package/dist/core/engine/generation-codec.js +2 -0
- package/dist/core/engine/index.d.ts +18 -10
- package/dist/core/engine/index.js +22 -11
- 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-isolation.d.ts +20 -0
- package/dist/core/engine/lifecycle/recovery-isolation.js +26 -0
- 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-body.d.ts +44 -0
- package/dist/core/engine/lifecycle/resume-body.js +211 -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.d.ts +46 -3
- package/dist/core/engine/lifecycle/resume.js +21 -201
- package/dist/core/engine/lifecycle/shared.d.ts +43 -0
- package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +80 -2
- package/dist/core/engine/lifecycle/standalone-claim-acquire.js +20 -4
- 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 +24 -7
- package/dist/core/engine/lifecycle/start-commit.js +57 -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-resolution.d.ts +8 -2
- package/dist/core/engine/lifecycle/start-or-signal-resolution.js +11 -3
- package/dist/core/engine/lifecycle/start-or-signal.js +1 -1
- package/dist/core/engine/lifecycle/start-precondition-attribution.d.ts +55 -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-schedule-timing.d.ts +11 -0
- package/dist/core/engine/lifecycle/start-schedule-timing.js +21 -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 +146 -4
- package/dist/core/engine/lifecycle/start-terminal-conflict-purge.js +33 -4
- package/dist/core/engine/lifecycle/start.d.ts +54 -7
- package/dist/core/engine/lifecycle/start.js +53 -90
- package/dist/core/engine/lifecycle/transition.d.ts +12 -4
- package/dist/core/engine/lifecycle/transition.js +40 -95
- package/dist/core/engine/lifecycle.d.ts +5 -2
- package/dist/core/engine/lifecycle.js +7 -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-overlap.js +2 -1
- 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.d.ts +10 -0
- package/dist/core/engine/schedule-run.js +2 -1
- package/dist/core/engine/schedules.d.ts +11 -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/complete.js +1 -1
- 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/termination/suspend.js +3 -0
- 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 +29 -4
- package/dist/core/engine/validation/schedule.js +19 -36
- package/dist/core/engine/validation.js +13 -17
- package/dist/core/engine/workflow-claim-reclaim-target.d.ts +1 -1
- package/dist/core/engine/workflow-claim-reclaim-target.js +22 -8
- package/dist/core/engine/workflow-claim-registry.d.ts +79 -75
- package/dist/core/engine/workflow-claim-registry.js +15 -2
- package/dist/core/engine/workflow-generation-fence.d.ts +74 -0
- package/dist/core/engine/workflow-generation-fence.js +19 -0
- 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/start-workflow-validation.d.ts +15 -0
- package/dist/core/start-workflow-validation.js +11 -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/core/workflow-identifiers.d.ts +37 -0
- package/dist/core/workflow-identifiers.js +19 -1
- 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 +2 -2
- 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/fleet-event-feed.js +6 -6
- 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/rest-binding.d.ts +11 -0
- package/dist/server/runtime/task-dispatch-envelope.d.ts +41 -0
- package/dist/server/runtime/task-dispatch-envelope.js +54 -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.d.ts +3 -1
- package/dist/server/runtime/task-dispatch.js +20 -44
- 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 +28 -0
- package/dist/server/task-ledger-codec.js +7 -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 +3 -1
- package/dist/server/task-queue-types.d.ts +2 -0
- package/dist/storage/bun-sql.js +135 -100
- 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/generation-keys.d.ts +52 -0
- package/dist/storage/generation-keys.js +4 -0
- package/dist/storage/index.d.ts +76 -63
- package/dist/storage/interface.d.ts +24 -230
- package/dist/storage/interface.js +1 -1
- package/dist/storage/key-prefixes.d.ts +1 -1
- package/dist/storage/key-prefixes.js +2 -0
- package/dist/storage/lease-keys.d.ts +48 -0
- package/dist/storage/lease-keys.js +8 -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 +3 -3
- package/dist/storage/node-sqlite.js +135 -100
- package/dist/storage/postgres.js +3 -3
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/signal-keys.d.ts +23 -0
- package/dist/storage/signal-keys.js +9 -0
- package/dist/storage/testing.js +1 -1
- package/dist/storage/turso.js +2 -2
- package/dist/storage/workflow-lifecycle-keys.d.ts +128 -0
- package/dist/storage/workflow-lifecycle-keys.js +46 -0
- package/dist/storage/workflow-record-keys.d.ts +98 -0
- package/dist/storage/workflow-record-keys.js +47 -0
- 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
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { decodeWorkflowState, isTerminalWorkflowStatus } from "./validation.js";
|
|
2
|
+
import { isTopLevelWorkflowStateKey } from "./workflow-state-stream.js";
|
|
3
|
+
export async function countNonTerminalRunsForRevision(storage, type, revision) {
|
|
4
|
+
let count = 0;
|
|
5
|
+
for await (const [key, bytes] of storage.scan("wf:")) {
|
|
6
|
+
if (!isTopLevelWorkflowStateKey(key))
|
|
7
|
+
continue;
|
|
8
|
+
const state = decodeWorkflowState(bytes);
|
|
9
|
+
if (state.type !== type || state.revision !== revision)
|
|
10
|
+
continue;
|
|
11
|
+
if (isTerminalWorkflowStatus(state.status))
|
|
12
|
+
continue;
|
|
13
|
+
count += 1;
|
|
14
|
+
}
|
|
15
|
+
return count;
|
|
16
|
+
}
|
|
@@ -1,31 +1,13 @@
|
|
|
1
1
|
import type { ContextOperationRequest } from '../context.ts';
|
|
2
|
-
import type {
|
|
3
|
-
import type { WorkflowVersionTuple } from '../workflow-version-tuple.ts';
|
|
2
|
+
import type { StartOptions, TimerEntry } from '../types.ts';
|
|
4
3
|
import type { EngineInternals } from './internals.ts';
|
|
5
|
-
type
|
|
4
|
+
import { type TimeOperationCallbacks } from './time-operation-callbacks.ts';
|
|
6
5
|
type SleepOperation = Extract<ContextOperationRequest, {
|
|
7
6
|
type: 'sleep';
|
|
8
7
|
}>;
|
|
9
|
-
export type TimeOperationCallbacks
|
|
10
|
-
completeOperation: (workflowId: string, value: unknown) => void;
|
|
11
|
-
dispatchEvent: (event: Event) => void;
|
|
12
|
-
loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>;
|
|
13
|
-
failWorkflow: (workflowId: string, error: Error) => Promise<void>;
|
|
14
|
-
runSerializedWorkflowStateWrite: <Result>(workflowId: string, writeOperation: () => Promise<Result>) => Promise<Result>;
|
|
15
|
-
beginWorkflowExecution: (workflowId: string, workflowExecutionToken: string | undefined, workflowType: string, input: unknown, checkpoint: Checkpoint, executionDeadline: number | undefined, executionStateOwnerId: string, registration: RegistrationEntry) => void;
|
|
16
|
-
workflowVersionTupleFromState: (state: WorkflowState) => WorkflowVersionTuple;
|
|
17
|
-
setWorkflowStartHeaders: (workflowId: string, headers: Map<string, string> | undefined) => void;
|
|
18
|
-
loadWorkflowStartHeaders: (workflowId: string) => Promise<Map<string, string> | undefined>;
|
|
19
|
-
parseStartOptionDuration: (value: Duration, fieldName: 'options.executionTimeout' | 'options.startAfter') => number;
|
|
20
|
-
runDeferredTerminalCleanup: (workflowId: string, timerId: string) => Promise<void>;
|
|
21
|
-
runWorkflowFinalizer: (workflowId: string, timerId: string) => Promise<void>;
|
|
22
|
-
handleScheduleTimer: (entry: TimerEntry) => Promise<void>;
|
|
23
|
-
timeout: (workflowId: string) => Promise<void>;
|
|
24
|
-
handleCleanupError: (source: string, error: unknown, workflowId: string) => void;
|
|
25
|
-
};
|
|
8
|
+
export type { TimeOperationCallbacks };
|
|
26
9
|
export declare function createDelayedStartTimerEntry(_internals: EngineInternals, workflowId: string, scheduledStartAt: number, options: StartOptions | undefined, callbacks: Pick<TimeOperationCallbacks, 'parseStartOptionDuration'>): TimerEntry;
|
|
27
10
|
export declare function processSleepOperation(internals: EngineInternals, workflowId: string, operation: SleepOperation, callbacks: Pick<TimeOperationCallbacks, 'completeOperation' | 'loadWorkflowState'>): Promise<void>;
|
|
28
11
|
export declare function registerSleepResolver(internals: EngineInternals, workflowId: string, operationId: string, resolve: () => void, scheduledFireAt: number): void;
|
|
29
|
-
export declare function startDelayedWorkflow(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'beginWorkflowExecution' | 'dispatchEvent' | 'failWorkflow' | 'handleCleanupError' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'runSerializedWorkflowStateWrite' | 'setWorkflowStartHeaders' | 'workflowVersionTupleFromState'>): Promise<void>;
|
|
30
|
-
export declare function handleTimerFired(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'failWorkflow' | 'handleCleanupError' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'runDeferredTerminalCleanup' | 'runWorkflowFinalizer' | 'runSerializedWorkflowStateWrite' | 'handleScheduleTimer' | 'setWorkflowStartHeaders' | 'timeout' | 'beginWorkflowExecution' | 'dispatchEvent' | 'workflowVersionTupleFromState'>): Promise<void>;
|
|
31
|
-
export {};
|
|
12
|
+
export declare function startDelayedWorkflow(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'beginWorkflowExecution' | 'dispatchEvent' | 'failWorkflow' | 'handleCleanupError' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'resolveExecutableRegistrationForRevision' | 'runSerializedWorkflowStateWrite' | 'setWorkflowStartHeaders' | 'workflowVersionTupleFromState'>): Promise<void>;
|
|
13
|
+
export declare function handleTimerFired(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'failWorkflow' | 'handleCleanupError' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'runDeferredTerminalCleanup' | 'runWorkflowFinalizer' | 'runSerializedWorkflowStateWrite' | 'handleScheduleTimer' | 'setWorkflowStartHeaders' | 'timeout' | 'beginWorkflowExecution' | 'dispatchEvent' | 'resolveExecutableRegistrationForRevision' | 'workflowVersionTupleFromState'>): Promise<void>;
|
|
@@ -4,7 +4,9 @@ import { encode } from "../codec.js";
|
|
|
4
4
|
import { buildTimerBatchOperations, normalizeStorageTimestamp } from "../scheduler.js";
|
|
5
5
|
import { notifyConditionWaiters, notifyConditionWaitersForTimerFire } from "./condition-waiters.js";
|
|
6
6
|
import { commitFencedEngineWrite } from "./fenced-write.js";
|
|
7
|
+
import { resolveDelayedStartRegistrationOrFail } from "./lifecycle/delayed-start-registration.js";
|
|
7
8
|
import { reprovideRecoveredServices } from "./lifecycle/recovered-services.js";
|
|
9
|
+
import { ensureDelayedStartClaimAndCleanupBeforeFailure } from "./lifecycle/standalone-claim-acquire.js";
|
|
8
10
|
import {
|
|
9
11
|
acknowledgeSupersededSleepTimers,
|
|
10
12
|
handleSleepTimerWithAcknowledgement,
|
|
@@ -88,23 +90,22 @@ export async function startDelayedWorkflow(internals, entry, callbacks) {
|
|
|
88
90
|
const checkpoint = await loadDelayedWorkflowCheckpoint(internals, entry, callbacks);
|
|
89
91
|
if (!checkpoint)
|
|
90
92
|
return;
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
await callbacks.failWorkflow(entry.workflowId, Error(`No workflow registered with name "${state.type}"`));
|
|
93
|
+
const resolvedRegistration = await resolveDelayedStartRegistrationOrFail(internals, entry, state.type, state.revision, callbacks);
|
|
94
|
+
if (resolvedRegistration === null)
|
|
94
95
|
return;
|
|
95
|
-
}
|
|
96
|
-
const now = internals.options.getNow(), executionDeadline = await resolveDelayedExecutionDeadline(entry, now, callbacks);
|
|
96
|
+
const { entry: registration, revision: resolvedRevision } = resolvedRegistration, now = internals.options.getNow(), executionDeadline = await resolveDelayedExecutionDeadline(internals, entry, now, callbacks);
|
|
97
97
|
if (executionDeadline === "invalid")
|
|
98
98
|
return;
|
|
99
99
|
const runningState = await callbacks.runSerializedWorkflowStateWrite(entry.workflowId, async () => {
|
|
100
100
|
const latestState = await callbacks.loadWorkflowState(entry.workflowId);
|
|
101
101
|
if (!latestState || latestState.status !== "pending")
|
|
102
102
|
return null;
|
|
103
|
-
const nextRunningState = {
|
|
103
|
+
const effectiveRevision = latestState.revision ?? resolvedRevision, nextRunningState = {
|
|
104
104
|
...latestState,
|
|
105
105
|
status: "running",
|
|
106
106
|
startedAt: now,
|
|
107
107
|
updatedAt: now,
|
|
108
|
+
...effectiveRevision !== void 0 && { revision: effectiveRevision },
|
|
108
109
|
...executionDeadline !== void 0 && { executionDeadline }
|
|
109
110
|
}, operations = [
|
|
110
111
|
{
|
|
@@ -136,7 +137,7 @@ export async function startDelayedWorkflow(internals, entry, callbacks) {
|
|
|
136
137
|
internals.checkpoints.set(entry.workflowId, checkpoint);
|
|
137
138
|
internals.workflowVersionTuples.set(entry.workflowId, callbacks.workflowVersionTupleFromState(runningState));
|
|
138
139
|
callbacks.setWorkflowStartHeaders(entry.workflowId, await callbacks.loadWorkflowStartHeaders(entry.workflowId));
|
|
139
|
-
callbacks.beginWorkflowExecution(entry.workflowId, runningState.workflowExecutionToken, runningState.type, runningState.input, checkpoint, executionDeadline, runningState.executionStateOwnerId ?? entry.workflowId, registration);
|
|
140
|
+
callbacks.beginWorkflowExecution(entry.workflowId, runningState.workflowExecutionToken, runningState.type, runningState.revision, runningState.input, checkpoint, executionDeadline, runningState.executionStateOwnerId ?? entry.workflowId, registration);
|
|
140
141
|
}
|
|
141
142
|
async function loadDelayedWorkflowCheckpoint(internals, entry, callbacks) {
|
|
142
143
|
const checkpointBytes = await internals.storage.get(KEYS.checkpoint(entry.workflowId));
|
|
@@ -146,21 +147,22 @@ async function loadDelayedWorkflowCheckpoint(internals, entry, callbacks) {
|
|
|
146
147
|
}
|
|
147
148
|
return deserializeCheckpoint(checkpointBytes);
|
|
148
149
|
}
|
|
149
|
-
async function resolveDelayedExecutionDeadline(entry, now, callbacks) {
|
|
150
|
+
async function resolveDelayedExecutionDeadline(internals, entry, now, callbacks) {
|
|
150
151
|
if (entry.executionTimeoutMs === void 0)
|
|
151
152
|
return;
|
|
152
153
|
if (!Number.isFinite(entry.executionTimeoutMs) || entry.executionTimeoutMs < 0) {
|
|
153
|
-
await failInvalidDelayedExecutionTimeout(entry, callbacks);
|
|
154
|
+
await failInvalidDelayedExecutionTimeout(internals, entry, callbacks);
|
|
154
155
|
return "invalid";
|
|
155
156
|
}
|
|
156
157
|
try {
|
|
157
158
|
return normalizeStorageTimestamp(now + entry.executionTimeoutMs, `Delayed execution timeout for workflow "${entry.workflowId}"`);
|
|
158
159
|
} catch {
|
|
159
|
-
await failInvalidDelayedExecutionTimeout(entry, callbacks);
|
|
160
|
+
await failInvalidDelayedExecutionTimeout(internals, entry, callbacks);
|
|
160
161
|
return "invalid";
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
|
-
async function failInvalidDelayedExecutionTimeout(entry, callbacks) {
|
|
164
|
+
async function failInvalidDelayedExecutionTimeout(internals, entry, callbacks) {
|
|
165
|
+
await ensureDelayedStartClaimAndCleanupBeforeFailure(internals, entry.workflowId);
|
|
164
166
|
await callbacks.failWorkflow(entry.workflowId, Error(`Invalid delayed execution timeout for workflow "${entry.workflowId}"`));
|
|
165
167
|
}
|
|
166
168
|
export async function handleTimerFired(internals, entry, callbacks) {
|
|
@@ -85,7 +85,7 @@ export type WorkflowLeaseOwnershipBootstrapOptions = {
|
|
|
85
85
|
* doc for why an engine that cannot execute a candidate's registered type
|
|
86
86
|
* must never win its claim in the first place.
|
|
87
87
|
*/
|
|
88
|
-
isWorkflowTypeRegistered?: (workflowType: string) => boolean;
|
|
88
|
+
isWorkflowTypeRegistered?: (workflowType: string, revision: string | undefined) => boolean;
|
|
89
89
|
};
|
|
90
90
|
/**
|
|
91
91
|
* Everything `Engine` needs to hold on `EngineInternals` once bootstrap
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Count schedules pinned to an exact `(type, revision)` (WFT-20). Wired into
|
|
3
|
+
* `catalog-removal.ts`'s `countWorkflowRevisionReferences()`, replacing the
|
|
4
|
+
* `pinnedSchedules: 0` stub WFT-12 left in place pending this batch (see
|
|
5
|
+
* that field's own doc comment: "always 0 until WFT-20").
|
|
6
|
+
*
|
|
7
|
+
* Mirrors `nonterminal-revision-count.ts`'s bounded-scan shape exactly: only
|
|
8
|
+
* top-level `schedule:{id}` records are schedule states — `schedule:{id}:*`
|
|
9
|
+
* suffix keys (timer index, run metadata) share the `schedule:` prefix but
|
|
10
|
+
* are not, the same skip `listSchedules`/`recoverOrphanedScheduleTimers`
|
|
11
|
+
* already apply.
|
|
12
|
+
*
|
|
13
|
+
* @module core/engine/pinned-schedule-revision-count
|
|
14
|
+
*/
|
|
15
|
+
import type { Storage } from '../../storage/interface.ts';
|
|
16
|
+
/**
|
|
17
|
+
* Scan `storage` for schedules whose `workflowType` and `pinnedRevision`
|
|
18
|
+
* match exactly, returning the count. Only `revisionPolicy: 'pinned'`
|
|
19
|
+
* schedules ever count — an `'active-at-fire'` schedule against the same
|
|
20
|
+
* `(type, revision)` never blocks removal, since it resolves whatever is
|
|
21
|
+
* active at each future fire regardless of what is being removed now. A
|
|
22
|
+
* `'cancelled'` pinned schedule is excluded too: it will never fire again,
|
|
23
|
+
* so it must not block removal of the revision it once pinned. A `'paused'`
|
|
24
|
+
* pinned schedule still counts — it can be resumed and fire again later.
|
|
25
|
+
*/
|
|
26
|
+
export declare function countPinnedSchedulesForRevision(storage: Storage, type: string, revision: string): Promise<number>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { decodeScheduleState } from "./validation/schedule.js";
|
|
2
|
+
export async function countPinnedSchedulesForRevision(storage, type, revision) {
|
|
3
|
+
let count = 0;
|
|
4
|
+
for await (const [key, bytes] of storage.scan("schedule:")) {
|
|
5
|
+
if (key.slice(9).includes(":"))
|
|
6
|
+
continue;
|
|
7
|
+
const state = decodeScheduleState(bytes);
|
|
8
|
+
if (!state)
|
|
9
|
+
continue;
|
|
10
|
+
if (state.workflowType !== type)
|
|
11
|
+
continue;
|
|
12
|
+
if (state.revisionPolicy !== "pinned" || state.pinnedRevision !== revision)
|
|
13
|
+
continue;
|
|
14
|
+
if (state.status === "cancelled")
|
|
15
|
+
continue;
|
|
16
|
+
count += 1;
|
|
17
|
+
}
|
|
18
|
+
return count;
|
|
19
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revision resolution for a `revisionPolicy: 'pinned'` schedule (WFT-20):
|
|
3
|
+
* capture-time resolution (what revision would run right now, recorded on
|
|
4
|
+
* the schedule) and fire-time resolution (resolve and reserve EXACTLY the
|
|
5
|
+
* captured revision, enforcing a real, checkable commitment for an eager
|
|
6
|
+
* workflow type rather than silently degrading to active-at-fire behavior).
|
|
7
|
+
*
|
|
8
|
+
* Fire-time resolution deliberately does NOT reuse
|
|
9
|
+
* `resolveExecutableRegistrationForRevision()` unchanged for an eager type:
|
|
10
|
+
* that resolver's documented "eager ignores the pin" rule is correct for
|
|
11
|
+
* run RECOVERY (a process only ever runs the code it has loaded, so a stale
|
|
12
|
+
* pin on an eager type is harmless) but wrong for a schedule's
|
|
13
|
+
* forward-looking commitment — silently falling back to whatever is active
|
|
14
|
+
* would make a "pinned" schedule lie about what it guarantees, and would
|
|
15
|
+
* make `pinnedSchedules` reference-count accounting
|
|
16
|
+
* ({@link import('./pinned-schedule-revision-count.ts').countPinnedSchedulesForRevision})
|
|
17
|
+
* block removal of a revision nothing will ever actually run again.
|
|
18
|
+
*
|
|
19
|
+
* @module core/engine/pinned-schedule-revision
|
|
20
|
+
*/
|
|
21
|
+
import type { ConditionalBatchCondition } from '../../storage/interface.ts';
|
|
22
|
+
import type { ScheduleRevisionPolicy } from '../types.ts';
|
|
23
|
+
import { type ExecutableRegistration } from './dynamic-source-execution.ts';
|
|
24
|
+
import type { Engine } from './index.ts';
|
|
25
|
+
import type { EngineInternals } from './internals.ts';
|
|
26
|
+
/**
|
|
27
|
+
* Capture-time resolution: the revision that would run RIGHT NOW for `type`,
|
|
28
|
+
* reusing the same resolver (and revision-selection rule) a fresh
|
|
29
|
+
* `engine.start()` call would use. Called by `schedule()`/`updateSchedule()`
|
|
30
|
+
* when a caller requests `revisionPolicy: 'pinned'`, so the captured
|
|
31
|
+
* {@link import('../types/schedules.ts').ScheduleMetadata.pinnedRevision} is
|
|
32
|
+
* exactly what would have run had the schedule fired at this instant. No
|
|
33
|
+
* `inFlightStartsByRevision` reservation here — this is not launching a
|
|
34
|
+
* workflow, only recording a decision; the write that persists the pin is
|
|
35
|
+
* separately fenced against a concurrent `removeWorkflowRevision()` via
|
|
36
|
+
* `buildCatalogEntryRevisionCondition` at the `writeScheduleState` call site.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveScheduleRevisionForPin(engine: Engine, internals: EngineInternals, type: string): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* `schedule()`'s create-time revision resolution — split out to keep that
|
|
41
|
+
* function under the complexity ceiling. For `revisionPolicy: 'pinned'`,
|
|
42
|
+
* captures and returns the current revision via
|
|
43
|
+
* {@link resolveScheduleRevisionForPin} (which already resolves, and for a
|
|
44
|
+
* lazy type loads, `type` internally). For `'active-at-fire'`, returns
|
|
45
|
+
* `undefined` and — for a lazy type not yet eagerly registered — resolves
|
|
46
|
+
* once here purely to trigger the loader at schedule-creation time, exactly
|
|
47
|
+
* as before this revision-policy field existed.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveScheduleCreationRevision(internals: EngineInternals, type: string, revisionPolicy: ScheduleRevisionPolicy): Promise<string | undefined>;
|
|
50
|
+
/**
|
|
51
|
+
* Fire-time resolve-and-reserve for a pinned schedule occurrence — the
|
|
52
|
+
* pinned-revision counterpart of
|
|
53
|
+
* `catalog-removal.ts`'s `resolveAndReserveExecutableRegistration`, reusing
|
|
54
|
+
* the identical early/late `inFlightStartsByRevision` reservation shape so a
|
|
55
|
+
* concurrent `removeWorkflowRevision()` sees an in-flight reference the same
|
|
56
|
+
* way it would for an ordinary start.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveAndReservePinnedExecutableRegistration(engine: Engine, internals: EngineInternals, type: string, revision: string): Promise<{
|
|
59
|
+
registration: ExecutableRegistration['entry'];
|
|
60
|
+
inFlightRevision: string | undefined;
|
|
61
|
+
resolvedRevision: string | undefined;
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* The `writeScheduleState` `extraConditions`/`onExtraConditionsLost` options
|
|
65
|
+
* that fence a pinned schedule's create/update write against a concurrent
|
|
66
|
+
* `removeWorkflowRevision()` landing between {@link resolveScheduleRevisionForPin}'s
|
|
67
|
+
* capture and this commit — see `storage-io.ts`'s `writeScheduleState` doc for
|
|
68
|
+
* why this is necessary (the scan-based `pinnedSchedules` reference count
|
|
69
|
+
* cannot see an uncommitted schedule write, so `removeWorkflowRevision()`'s
|
|
70
|
+
* own reference check cannot block it on its own). `undefined` (no fencing)
|
|
71
|
+
* when `pinnedRevision` is `undefined` — this write is not pinning.
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildPinnedRevisionWriteOptions(internals: EngineInternals, type: string, pinnedRevision: string | undefined): Promise<{
|
|
74
|
+
extraConditions: ConditionalBatchCondition[];
|
|
75
|
+
onExtraConditionsLost: () => Error;
|
|
76
|
+
} | undefined>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { releaseInFlightStart, reserveInFlightStart } from "./catalog-removal.js";
|
|
2
|
+
import {
|
|
3
|
+
resolveExecutableRegistration,
|
|
4
|
+
resolveExecutableRegistrationForRevision
|
|
5
|
+
} from "./dynamic-source-execution.js";
|
|
6
|
+
import { buildCatalogEntryRevisionCondition } from "./lifecycle/start-commit.js";
|
|
7
|
+
import {
|
|
8
|
+
resolveCachedStartRevision,
|
|
9
|
+
resolveStartRevisionUncached
|
|
10
|
+
} from "./lifecycle/start-revision-resolution.js";
|
|
11
|
+
import { WorkflowRevisionUnavailableError } from "./revision-errors.js";
|
|
12
|
+
export async function resolveScheduleRevisionForPin(engine, internals, type) {
|
|
13
|
+
const { revision: resolvedRevision } = await resolveExecutableRegistration(engine, internals, type);
|
|
14
|
+
return resolveCachedStartRevision(internals, type, resolvedRevision) ?? await resolveStartRevisionUncached(internals, type);
|
|
15
|
+
}
|
|
16
|
+
async function resolvePinnedExecutableRegistration(engine, internals, type, revision, onRevisionChosen) {
|
|
17
|
+
const eager = internals.registrations.get(type);
|
|
18
|
+
if (eager !== void 0) {
|
|
19
|
+
if (internals.registeredCatalogRevisions.get(type) !== revision)
|
|
20
|
+
throw new WorkflowRevisionUnavailableError(type, revision, "not-registered");
|
|
21
|
+
onRevisionChosen?.(revision);
|
|
22
|
+
return { entry: eager, revision: void 0 };
|
|
23
|
+
}
|
|
24
|
+
onRevisionChosen?.(revision);
|
|
25
|
+
return resolveExecutableRegistrationForRevision(engine, internals, type, revision);
|
|
26
|
+
}
|
|
27
|
+
export async function resolveScheduleCreationRevision(internals, type, revisionPolicy) {
|
|
28
|
+
if (revisionPolicy === "pinned")
|
|
29
|
+
return resolveScheduleRevisionForPin(internals.engine, internals, type);
|
|
30
|
+
if (!internals.registrations.has(type))
|
|
31
|
+
await resolveExecutableRegistration(internals.engine, internals, type);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
export async function resolveAndReservePinnedExecutableRegistration(engine, internals, type, revision) {
|
|
35
|
+
let earlyReservation;
|
|
36
|
+
try {
|
|
37
|
+
const { entry: registration, revision: resolvedRevision } = await resolvePinnedExecutableRegistration(engine, internals, type, revision, (chosen) => {
|
|
38
|
+
earlyReservation = reserveInFlightStart(internals, type, chosen);
|
|
39
|
+
}), inFlightRevision = earlyReservation !== void 0 ? earlyReservation : reserveInFlightStart(internals, type, resolvedRevision);
|
|
40
|
+
return { registration, inFlightRevision, resolvedRevision };
|
|
41
|
+
} catch (error) {
|
|
42
|
+
releaseInFlightStart(internals, type, earlyReservation);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function buildPinnedRevisionWriteOptions(internals, type, pinnedRevision) {
|
|
47
|
+
if (pinnedRevision === void 0)
|
|
48
|
+
return;
|
|
49
|
+
return {
|
|
50
|
+
extraConditions: [await buildCatalogEntryRevisionCondition(internals, type, pinnedRevision)],
|
|
51
|
+
onExtraConditionsLost: () => new WorkflowRevisionUnavailableError(type, pinnedRevision, "not-installed")
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -6,6 +6,13 @@ export type RegistrationCallbacks = {
|
|
|
6
6
|
ensureRetentionSweepInterval: () => void;
|
|
7
7
|
dispatchEvent: (event: Event) => void;
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* Reject constraints on a workflow definition when the engine has no
|
|
11
|
+
* inline execution strategy. Exported so `dynamic-source-execution.ts`
|
|
12
|
+
* (WFT-15/16) can apply the identical guard to a dynamically-resolved
|
|
13
|
+
* definition — see that module's own call site.
|
|
14
|
+
*/
|
|
15
|
+
export declare function assertConstraintsSupported(internals: EngineInternals, name: string, registration: WorkflowDefinition): void;
|
|
9
16
|
/**
|
|
10
17
|
* Build the `RegistrationEntry` `engine.register()` would store for
|
|
11
18
|
* `registration`, without committing anything — pure normalization
|
|
@@ -28,6 +35,29 @@ export type RegistrationCallbacks = {
|
|
|
28
35
|
* ```
|
|
29
36
|
*/
|
|
30
37
|
export declare function buildRegistrationEntry(name: string, registration: WorkflowDefinition): RegistrationEntry;
|
|
38
|
+
/**
|
|
39
|
+
* Visit every RESOLVED `registerSource()`-registered type's normalized
|
|
40
|
+
* retention policy (via {@link buildRegistrationEntry}) — the dynamic-source
|
|
41
|
+
* counterpart to walking `internals.registrations.values()` directly for an
|
|
42
|
+
* eager type. Used by `bulk-operations-purge.ts` so a dynamic workflow's own
|
|
43
|
+
* (possibly shorter) retention window narrows the purge sweep's scan bound
|
|
44
|
+
* the same way an eager registration's does.
|
|
45
|
+
*/
|
|
46
|
+
export declare function forEachResolvedDynamicRetentionPolicy(internals: EngineInternals, visit: (policy: RegistrationEntry['retention']) => void): void;
|
|
47
|
+
/**
|
|
48
|
+
* True when at least one `registerSource()`-registered type has a
|
|
49
|
+
* registered candidate revision this process has NOT yet resolved — its
|
|
50
|
+
* retention policy (and everything else `buildRegistrationEntry` would
|
|
51
|
+
* derive) is genuinely unknown, so `getMinimumRetentionMs()`'s scan-bound
|
|
52
|
+
* optimization in `bulk-operations-purge.ts` cannot be trusted (WFT-19
|
|
53
|
+
* review round 1): it silently excluded exactly this class of workflow
|
|
54
|
+
* from the periodic retention sweep's scan bound, since an unresolved
|
|
55
|
+
* candidate's possibly-shorter-than-default retention window was invisible
|
|
56
|
+
* to the minimum computation. Used to fall back to an unbounded terminal
|
|
57
|
+
* scan while any such candidate remains unresolved — self-healing once
|
|
58
|
+
* `shouldPurgeWorkflowState`'s own per-run resolve installs it.
|
|
59
|
+
*/
|
|
60
|
+
export declare function hasUnresolvedDynamicSourceCandidate(internals: EngineInternals): boolean;
|
|
31
61
|
/**
|
|
32
62
|
* Structural predicate for `BuiltWorkflowDefinition` — the runtime shape
|
|
33
63
|
* `workflow({ name }).execute(...)` returns. Exported so the dynamic
|
|
@@ -15,7 +15,7 @@ function copiedTags(tags) {
|
|
|
15
15
|
function isWorkflowDefinition(value) {
|
|
16
16
|
return typeof value === "object" && value !== null && "name" in value && "handler" in value && typeof value.handler === "function";
|
|
17
17
|
}
|
|
18
|
-
function assertConstraintsSupported(internals, name, registration) {
|
|
18
|
+
export function assertConstraintsSupported(internals, name, registration) {
|
|
19
19
|
if (!registration.constraints || registration.constraints.length === 0)
|
|
20
20
|
return;
|
|
21
21
|
if (internals.inlineStrategy === null)
|
|
@@ -68,6 +68,20 @@ export function buildRegistrationEntry(name, registration) {
|
|
|
68
68
|
}
|
|
69
69
|
return entry;
|
|
70
70
|
}
|
|
71
|
+
export function forEachResolvedDynamicRetentionPolicy(internals, visit) {
|
|
72
|
+
for (const [type, revisions] of internals.sources.resolved)
|
|
73
|
+
for (const resolved of revisions.values())
|
|
74
|
+
visit(buildRegistrationEntry(type, resolved.definition).retention);
|
|
75
|
+
}
|
|
76
|
+
export function hasUnresolvedDynamicSourceCandidate(internals) {
|
|
77
|
+
for (const [type, byRevision] of internals.sources.byName) {
|
|
78
|
+
const resolvedForType = internals.sources.resolved.get(type);
|
|
79
|
+
for (const revision of byRevision.keys())
|
|
80
|
+
if (resolvedForType?.has(revision) !== !0)
|
|
81
|
+
return !0;
|
|
82
|
+
}
|
|
83
|
+
return !1;
|
|
84
|
+
}
|
|
71
85
|
function normalizeMessageDefinitions(workflowType, messageKind, definitions) {
|
|
72
86
|
const normalized = Object.create(null), sortedDefinitions = Object.values(definitions).toSorted((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
73
87
|
for (const definition of sortedDefinitions) {
|
|
@@ -80,13 +94,14 @@ function normalizeMessageDefinitions(workflowType, messageKind, definitions) {
|
|
|
80
94
|
function commitWorkflowDefinition(internals, definition, callbacks) {
|
|
81
95
|
const name = definition.name;
|
|
82
96
|
validateWorkflowOrActivityName(name, "workflow");
|
|
83
|
-
if (internals.
|
|
97
|
+
if (internals.sources.byName.has(name))
|
|
84
98
|
throw Error(`Cannot register("${name}"): "${name}" is already registered as a dynamic workflow source via engine.registerSource(). A workflow name may not be both eagerly registered and a dynamic source.`);
|
|
85
99
|
assertConstraintsSupported(internals, name, definition);
|
|
86
100
|
const entry = buildRegistrationEntry(name, definition);
|
|
87
101
|
internals.registrations.set(name, entry);
|
|
88
102
|
callbacks.ensureRetentionSweepInterval();
|
|
89
103
|
internals.workflowTypesByHandler.set(definition.handler, name);
|
|
104
|
+
internals.registeredCatalogRevisions.delete(name);
|
|
90
105
|
if (!internals.pendingCatalogInstalls.includes(name))
|
|
91
106
|
internals.pendingCatalogInstalls.push(name);
|
|
92
107
|
callbacks.dispatchEvent(new WorkflowDefinitionRegisteredEvent(name));
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { purgeInternal } from "./bulk-operations.js";
|
|
2
|
+
import { resolveLastKnownDynamicRegistration } from "./dynamic-source-execution.js";
|
|
2
3
|
export function hasConfiguredRetention(internals) {
|
|
3
4
|
if (internals.options.retention !== null)
|
|
4
5
|
return !0;
|
|
5
6
|
for (const registration of internals.registrations.values())
|
|
6
7
|
if (registration.retention !== void 0 && registration.retention !== null)
|
|
7
8
|
return !0;
|
|
9
|
+
for (const revisions of internals.sources.resolved.values())
|
|
10
|
+
for (const resolved of revisions.values())
|
|
11
|
+
if (resolved.definition.retention !== void 0 && resolved.definition.retention !== null)
|
|
12
|
+
return !0;
|
|
8
13
|
return !1;
|
|
9
14
|
}
|
|
10
15
|
export function setNextRetentionSweepAt(internals) {
|
|
@@ -49,7 +54,7 @@ export async function runRetentionSweep(internals, handleCleanupError, cleanupWa
|
|
|
49
54
|
}
|
|
50
55
|
}
|
|
51
56
|
export function resolveWorkflowTypeRetention(internals, type) {
|
|
52
|
-
const registration = internals
|
|
57
|
+
const registration = resolveLastKnownDynamicRegistration(internals, type);
|
|
53
58
|
if (registration?.retention)
|
|
54
59
|
return {
|
|
55
60
|
type,
|
|
@@ -69,7 +74,9 @@ export function resolveWorkflowTypeRetention(internals, type) {
|
|
|
69
74
|
};
|
|
70
75
|
}
|
|
71
76
|
export function getRetentionOverview(internals, resolveRetentionForType = (type) => resolveWorkflowTypeRetention(internals, type)) {
|
|
72
|
-
const workflowTypes = [
|
|
77
|
+
const workflowTypes = [
|
|
78
|
+
...new Set([...internals.registrations.keys(), ...internals.sources.byName.keys()])
|
|
79
|
+
].toSorted().map((type) => resolveRetentionForType(type));
|
|
73
80
|
return {
|
|
74
81
|
defaultRetention: internals.options.retention,
|
|
75
82
|
sweepIntervalMs: internals.options.retentionSweepIntervalMs,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {@link WorkflowRevisionUnavailableError} (WFT-17/WFT-18) — thrown by
|
|
3
|
+
* {@link import('./dynamic-source-execution.ts').resolveExecutableRegistrationForRevision}
|
|
4
|
+
* when a resumed run's persisted, EXACT pinned `WorkflowState.revision` cannot
|
|
5
|
+
* be resolved in this process. Distinct from
|
|
6
|
+
* {@link import('./dynamic-source-errors.ts').DynamicWorkflowSourceUnavailableError},
|
|
7
|
+
* which covers a dynamic source whose ACTIVE revision is ambiguous or fails
|
|
8
|
+
* to load — this error instead covers "the run's own pin cannot be honored",
|
|
9
|
+
* which can also happen for a legacy (pre-revision-pinning) record on a
|
|
10
|
+
* dynamic source with two or more registered candidates, where there is no
|
|
11
|
+
* pin at all and the ambiguity cannot be resolved by falling back to
|
|
12
|
+
* whichever revision happens to be active (that fallback is exactly the bug
|
|
13
|
+
* this batch closes — see `documentation/guides/recovery-and-deploys.md`).
|
|
14
|
+
*
|
|
15
|
+
* @module core/engine/revision-errors
|
|
16
|
+
*/
|
|
17
|
+
import { WeftError } from '../weft-error.ts';
|
|
18
|
+
/**
|
|
19
|
+
* Thrown when a workflow run's pinned {@link import('../types/state.ts').WorkflowState.revision}
|
|
20
|
+
* cannot be resolved to an executable registration in this process:
|
|
21
|
+
*
|
|
22
|
+
* - `reason: 'not-registered'` — the run pins a specific revision (a
|
|
23
|
+
* dynamic-source candidate this process has never `registerSource()`-registered,
|
|
24
|
+
* including the case where the pin no longer matches a dynamic source's sole
|
|
25
|
+
* remaining candidate).
|
|
26
|
+
* - `reason: 'legacy-ambiguous'` — the run predates revision pinning (no
|
|
27
|
+
* `revision` was ever persisted for it) and its type is a dynamic source
|
|
28
|
+
* with two or more registered candidates, so there is no way to tell which
|
|
29
|
+
* one it actually started against.
|
|
30
|
+
* - `reason: 'not-installed'` — thrown at commit-time ADMISSION, not
|
|
31
|
+
* recovery, for either a fresh `start()` or a checkpoint-backed failed-run
|
|
32
|
+
* `retryFailedAll()` reactivation (WFT-17/WFT-18 Codex review on PR #958):
|
|
33
|
+
* the resolved (or pinned) revision's durable catalog entry was
|
|
34
|
+
* concurrently removed — `removeWorkflowRevision()` racing a `start()` on
|
|
35
|
+
* a DIFFERENT process under a lease ownership mode, or racing a
|
|
36
|
+
* `retryFailedAll()` on ANY process/mode, since a failed run contributes no
|
|
37
|
+
* `inFlightStartsByRevision` reservation of its own — between this
|
|
38
|
+
* process resolving the revision and that commit batch actually landing.
|
|
39
|
+
* Fails the one call outright — never retried inside the engine — so the
|
|
40
|
+
* caller re-issues it, which re-resolves against whatever is actually
|
|
41
|
+
* still installed.
|
|
42
|
+
*
|
|
43
|
+
* `engine.recoverAll()` classifies the affected `(type, revision)` group
|
|
44
|
+
* `unavailable`: only the runs in that group fail (with this error as their
|
|
45
|
+
* `system` failure cause, delivered via `failWorkflow()`/`onRecoveredWorkflow`,
|
|
46
|
+
* never thrown to `recoverAll()`'s own caller); sibling groups — including
|
|
47
|
+
* OTHER revisions of the same dynamic-source type — continue recovering
|
|
48
|
+
* normally. A standalone `engine.resume(workflowId)` call has no such
|
|
49
|
+
* isolation to fall back on — there is only the one run — so it throws this
|
|
50
|
+
* error directly to its caller instead.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { Engine, WorkflowRevisionUnavailableError } from '@lostgradient/weft';
|
|
55
|
+
*
|
|
56
|
+
* const engine = new Engine();
|
|
57
|
+
* try {
|
|
58
|
+
* await engine.resume('workflow-id');
|
|
59
|
+
* } catch (err) {
|
|
60
|
+
* if (err instanceof WorkflowRevisionUnavailableError) {
|
|
61
|
+
* console.error(err.workflowType, err.revision, err.reason);
|
|
62
|
+
* }
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare class WorkflowRevisionUnavailableError extends WeftError<'WorkflowRevisionUnavailableError'> {
|
|
67
|
+
readonly workflowType: string;
|
|
68
|
+
readonly revision: string | undefined;
|
|
69
|
+
readonly reason: 'not-registered' | 'legacy-ambiguous' | 'not-installed';
|
|
70
|
+
constructor(workflowType: string, revision: string | undefined, reason: 'not-registered' | 'legacy-ambiguous' | 'not-installed');
|
|
71
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { WeftError } from "../weft-error.js";
|
|
2
|
+
|
|
3
|
+
export class WorkflowRevisionUnavailableError extends WeftError {
|
|
4
|
+
workflowType;
|
|
5
|
+
revision;
|
|
6
|
+
reason;
|
|
7
|
+
constructor(workflowType, revision, reason) {
|
|
8
|
+
super("WorkflowRevisionUnavailableError", reason === "legacy-ambiguous" ? `Cannot recover workflow type "${workflowType}": this run predates revision pinning and the type is a dynamic source with multiple registered revisions, so which one it started against cannot be determined.` : reason === "not-installed" ? `Cannot admit workflow type "${workflowType}": revision${revision === void 0 ? "" : ` "${revision}"`} is no longer installed in the durable catalog (removed concurrently). Retry the operation.` : `Cannot recover workflow type "${workflowType}": its pinned revision${revision === void 0 ? "" : ` "${revision}"`} is not registered in this process. Register the exact revision this run started against before retrying.`);
|
|
9
|
+
this.workflowType = workflowType;
|
|
10
|
+
this.revision = revision;
|
|
11
|
+
this.reason = reason;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -44,7 +44,8 @@ export async function drainQueuedScheduleRun(state, callbacks, completedWorkflow
|
|
|
44
44
|
workflowId: queuedRun.workflowId,
|
|
45
45
|
...queuedRun.occurrence !== void 0 && { occurrence: queuedRun.occurrence },
|
|
46
46
|
scheduleStateAfterStart: stateAfterStart,
|
|
47
|
-
...completedWorkflowId !== void 0 && { completedWorkflowId }
|
|
47
|
+
...completedWorkflowId !== void 0 && { completedWorkflowId },
|
|
48
|
+
skipAdmissionIdCheck: !0
|
|
48
49
|
});
|
|
49
50
|
return stateAfterStart;
|
|
50
51
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pinned-vs-active-at-fire branch point for a scheduled occurrence
|
|
3
|
+
* (WFT-20), split into its own tiny module so `schedule-run.ts` — a
|
|
4
|
+
* deliberately small, purpose-built module — has one obvious call site
|
|
5
|
+
* instead of inlining the branch.
|
|
6
|
+
*
|
|
7
|
+
* @module core/engine/schedule-revision-fire
|
|
8
|
+
*/
|
|
9
|
+
import type { ScheduleState } from '../types.ts';
|
|
10
|
+
/**
|
|
11
|
+
* The revision override `startScheduledRun` should pass to
|
|
12
|
+
* `ScheduleCallbacks.startWorkflow` for this occurrence: `state.pinnedRevision`
|
|
13
|
+
* when `state.revisionPolicy === 'pinned'`, else `undefined` (an
|
|
14
|
+
* `'active-at-fire'` schedule resolves whatever revision is active at fire
|
|
15
|
+
* time, unchanged from pre-WFT-20 behavior).
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildPinnedRevisionOverride(state: ScheduleState): string | undefined;
|
|
@@ -9,6 +9,16 @@ export type ScheduledRunStartOptions = {
|
|
|
9
9
|
scheduleStateAfterStart?: ScheduleState;
|
|
10
10
|
/** Prior terminal run whose transient schedule metadata is now settled. */
|
|
11
11
|
completedWorkflowId?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Internal only (WFT-95). Set by `drainQueuedScheduleRun()` when `workflowId`
|
|
14
|
+
* is a persisted `queuedRuns[].workflowId` being replayed, not a freshly
|
|
15
|
+
* minted one — safe unconditionally, since a post-fix queued run was already
|
|
16
|
+
* validated as non-`.`/`..` at schedule-admission time (relaxing the check
|
|
17
|
+
* here is then a no-op), and it is what lets a legacy pre-WFT-95 queued run
|
|
18
|
+
* whose id is `.`/`..` keep draining instead of pausing the schedule.
|
|
19
|
+
* Threaded straight through to `startWorkflow`'s `skipAdmissionIdCheck`.
|
|
20
|
+
*/
|
|
21
|
+
skipAdmissionIdCheck?: boolean;
|
|
12
22
|
};
|
|
13
23
|
/**
|
|
14
24
|
* Launch one scheduled occurrence's workflow run and emit `schedule:fired`.
|
|
@@ -3,6 +3,7 @@ import { encode } from "../codec.js";
|
|
|
3
3
|
import { ScheduleFiredEvent } from "../events.js";
|
|
4
4
|
import { unavailableServicesError } from "./lifecycle/recovered-services.js";
|
|
5
5
|
import { EMPTY_STORAGE_VALUE } from "./lifecycle/shared.js";
|
|
6
|
+
import { buildPinnedRevisionOverride } from "./schedule-revision-fire.js";
|
|
6
7
|
import { encodeScheduleRunMetadata } from "./schedule-run-metadata.js";
|
|
7
8
|
export async function startScheduledRun(internals, state, callbacks, options = {}) {
|
|
8
9
|
const workflowId = options.workflowId ?? crypto.randomUUID(), occurrence = options.occurrence, metadata = encodeScheduleRunMetadata(state.id, occurrence), scheduleRunOperations = [
|
|
@@ -49,7 +50,7 @@ export async function startScheduledRun(internals, state, callbacks, options = {
|
|
|
49
50
|
internals.workflowServices.set(workflowId, resolution.services);
|
|
50
51
|
}
|
|
51
52
|
internals.workflowsNeedingTerminalCleanup.add(workflowId);
|
|
52
|
-
await callbacks.startWorkflow(state.workflowType, state.input, { id: workflowId }, scheduleRunOperations);
|
|
53
|
+
await callbacks.startWorkflow(state.workflowType, state.input, { id: workflowId }, scheduleRunOperations, buildPinnedRevisionOverride(state), options.skipAdmissionIdCheck);
|
|
53
54
|
internals.engine.dispatchEvent(new ScheduleFiredEvent(state.id, workflowId, internals.options.getNow(), occurrence));
|
|
54
55
|
if (resolution !== null && resolution.status === "unavailable") {
|
|
55
56
|
const unavailableError = unavailableServicesError(workflowId, resolution.reason);
|
|
@@ -10,9 +10,19 @@ export type RefreshedScheduleState = {
|
|
|
10
10
|
currentWorkflowState: WorkflowState | null;
|
|
11
11
|
};
|
|
12
12
|
export type ScheduleCallbacks = {
|
|
13
|
+
/**
|
|
14
|
+
* `revisionOverride` (WFT-20) is the exact revision a `revisionPolicy: 'pinned'`
|
|
15
|
+
* occurrence must resolve against; `undefined` for an `'active-at-fire'` schedule.
|
|
16
|
+
*
|
|
17
|
+
* `skipAdmissionIdCheck` (WFT-95, internal only) is threaded straight through to
|
|
18
|
+
* `startWorkflow`'s parameter of the same name — see its doc comment. Set only when
|
|
19
|
+
* replaying a schedule's already-persisted `queuedRuns[].workflowId` via
|
|
20
|
+
* `drainQueuedScheduleRun()`; every other schedule-run start (a fresh cadence tick,
|
|
21
|
+
* a `cancel-running` replacement) omits it and gets strict id admission.
|
|
22
|
+
*/
|
|
13
23
|
startWorkflow: (type: string, input: unknown, options: {
|
|
14
24
|
id: string;
|
|
15
|
-
}, additionalStartOperations?: BatchOperation[]) => Promise<void>;
|
|
25
|
+
}, additionalStartOperations?: BatchOperation[], revisionOverride?: string, skipAdmissionIdCheck?: boolean) => Promise<void>;
|
|
16
26
|
loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null | undefined>;
|
|
17
27
|
cancelWorkflow: (workflowId: string) => Promise<void>;
|
|
18
28
|
getWorkflowResult: (workflowId: string) => Promise<unknown>;
|