@lostgradient/weft 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/cli/generated/operation-client.generated.d.ts +1 -0
- package/dist/cli-main.js +63 -63
- package/dist/client/event-stream-options.d.ts +58 -0
- package/dist/client/event-stream-options.js +0 -0
- package/dist/client/event-stream.d.ts +27 -44
- package/dist/client/event-stream.js +21 -56
- package/dist/client/event-tail-lifecycle.d.ts +21 -0
- package/dist/client/event-tail-lifecycle.js +71 -0
- package/dist/client/event-tail.d.ts +3 -1
- package/dist/client/http-client.d.ts +4 -5
- package/dist/client/http-client.js +4 -1
- package/dist/client/http-handle.d.ts +4 -5
- package/dist/client/http-request.d.ts +7 -0
- package/dist/client/index.d.ts +2 -1
- package/dist/client/interface.d.ts +23 -7
- package/dist/client/local-handles.d.ts +2 -0
- package/dist/client/local-handles.js +10 -1
- package/dist/client/local.d.ts +3 -3
- package/dist/client/open-event-subscription.d.ts +3 -2
- package/dist/client/open-event-subscription.js +28 -1
- package/dist/client/sse-event-stream.d.ts +27 -0
- package/dist/client/sse-event-stream.js +276 -0
- package/dist/client/start-body.d.ts +2 -2
- package/dist/client/start-body.js +1 -0
- package/dist/core/context/activity-retry-state.d.ts +6 -5
- package/dist/core/context/activity-retry-state.js +31 -21
- package/dist/core/context/durable-activity.d.ts +117 -0
- package/dist/core/context/durable-activity.js +79 -0
- package/dist/core/context/operation-request.d.ts +15 -0
- package/dist/core/context/parallel-operations.js +1 -0
- package/dist/core/context/run-operation-cached-request.d.ts +8 -0
- package/dist/core/context/run-operation-cached-request.js +59 -0
- package/dist/core/context/run-operation.d.ts +23 -5
- package/dist/core/context/run-operation.js +62 -47
- package/dist/core/engine/activity-heartbeat-tracking.d.ts +8 -17
- package/dist/core/engine/activity-heartbeat-tracking.js +7 -1
- package/dist/core/engine/activity-reconciliation.d.ts +1 -0
- package/dist/core/engine/activity-reconciliation.js +7 -2
- package/dist/core/engine/anonymous-signal-sequence.js +6 -4
- package/dist/core/engine/async-activity-completion.d.ts +5 -7
- package/dist/core/engine/async-activity-completion.js +13 -9
- package/dist/core/engine/bulk-operations-purge.js +2 -1
- package/dist/core/engine/bulk-operations.js +8 -7
- package/dist/core/engine/callback-checkpoint-persistence.d.ts +3 -0
- package/dist/core/engine/callback-checkpoint-persistence.js +25 -0
- package/dist/core/engine/callback-creators-bundles.js +4 -1
- package/dist/core/engine/checkpoint-io.d.ts +4 -1
- package/dist/core/engine/checkpoint-io.js +19 -10
- package/dist/core/engine/completed-review-storage.d.ts +2 -1
- package/dist/core/engine/completed-review-storage.js +4 -3
- package/dist/core/engine/construction.js +3 -1
- package/dist/core/engine/engine-internal-types.d.ts +2 -0
- package/dist/core/engine/errors.d.ts +6 -6
- package/dist/core/engine/errors.js +1 -1
- package/dist/core/engine/handles.js +1 -1
- package/dist/core/engine/index.d.ts +37 -5
- package/dist/core/engine/index.js +7 -1
- package/dist/core/engine/internals.d.ts +2 -1
- package/dist/core/engine/lease-manager.js +2 -2
- package/dist/core/engine/lifecycle/recovered-services.js +8 -1
- package/dist/core/engine/lifecycle/start-or-signal-create.d.ts +2 -2
- package/dist/core/engine/lifecycle/start-or-signal-create.js +17 -15
- package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +1 -1
- package/dist/core/engine/lifecycle/start-or-signal-resolution.js +23 -5
- package/dist/core/engine/lifecycle/start-or-signal.d.ts +6 -4
- package/dist/core/engine/lifecycle/start-or-signal.js +21 -4
- package/dist/core/engine/memo-durable-activity.d.ts +11 -0
- package/dist/core/engine/memo-durable-activity.js +282 -0
- package/dist/core/engine/operations-activity.d.ts +5 -1
- package/dist/core/engine/operations-activity.js +19 -7
- package/dist/core/engine/operations-data.d.ts +4 -1
- package/dist/core/engine/operations-data.js +3 -3
- package/dist/core/engine/reviews.js +7 -4
- package/dist/core/engine/schedule-run.js +12 -3
- package/dist/core/engine/schedule-timer.js +2 -0
- package/dist/core/engine/storage-io.js +2 -2
- package/dist/core/engine/termination/complete.js +3 -3
- package/dist/core/json.js +1 -1
- package/dist/core/scheduler/scheduler-class.js +2 -1
- package/dist/core/start-workflow-validation.d.ts +3 -4
- package/dist/core/start-workflow-validation.js +1 -1
- package/dist/core/timeouts.d.ts +13 -1
- package/dist/core/timeouts.js +4 -1
- package/dist/core/types/constants.d.ts +16 -0
- package/dist/core/types/options.d.ts +60 -12
- package/dist/core/types/services-resolution.d.ts +49 -3
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +3 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +7 -0
- package/dist/json-schema.js +1 -1
- package/dist/mcp/cli.js +26 -26
- package/dist/server/asyncapi-channels.js +82 -16
- package/dist/server/handler/auth-context-principal.d.ts +19 -0
- package/dist/server/handler/auth-context-principal.js +25 -0
- package/dist/server/handler/index.d.ts +2 -1
- package/dist/server/handler/index.js +35 -18
- package/dist/server/handler/route-dispatch.d.ts +12 -22
- package/dist/server/handler/route-dispatch.js +30 -36
- package/dist/server/handler/sse-route-dispatch.d.ts +25 -0
- package/dist/server/handler/sse-route-dispatch.js +45 -0
- package/dist/server/handler.js +28 -28
- package/dist/server/index.d.ts +8 -7
- package/dist/server/index.js +17 -17
- package/dist/server/operations/fleet-events-sse.d.ts +11 -0
- package/dist/server/operations/fleet-events-sse.js +164 -0
- package/dist/server/operations/sse-stream.d.ts +43 -0
- package/dist/server/operations/sse-stream.js +152 -7
- package/dist/server/operations/start-or-signal-workflow.d.ts +2 -0
- package/dist/server/operations/start-or-signal-workflow.js +25 -14
- package/dist/server/operations/start-workflow-options.d.ts +9 -6
- package/dist/server/operations/start-workflow-options.js +13 -0
- package/dist/server/operations/workflow-events-sse.d.ts +17 -0
- package/dist/server/operations/workflow-events-sse.js +200 -0
- package/dist/server/rest-bindings.js +12 -0
- package/dist/server/runtime/authentication-bridge.js +4 -0
- package/dist/server/runtime/cors.d.ts +1 -1
- package/dist/server/runtime/cors.js +6 -1
- package/dist/server/runtime/websocket-stream.d.ts +4 -0
- package/dist/server/runtime/websocket-stream.js +20 -3
- package/dist/server/runtime/websocket-worker.js +7 -2
- package/dist/server/serve-internals.d.ts +28 -0
- package/dist/server/serve-internals.js +4 -2
- package/dist/server/workflow-event-feed.d.ts +1 -0
- package/dist/server/workflow-event-feed.js +17 -6
- package/dist/service-worker/index.js +28 -28
- package/dist/service-worker/setup.d.ts +18 -1
- package/dist/service-worker/setup.js +7 -4
- package/dist/storage/typed-storage.d.ts +1 -1
- package/dist/storage/typed-storage.js +1 -1
- package/dist/testing/index.js +27 -27
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -3,6 +3,8 @@ import { decode, encode } from "../codec.js";
|
|
|
3
3
|
import { ActivityAsyncPendingEvent } from "../events.js";
|
|
4
4
|
import { assertPayloadWithinLimit } from "../payload-size.js";
|
|
5
5
|
import { WeftError } from "../weft-error.js";
|
|
6
|
+
import { stageAtomicWorkflowCommitSideEffects } from "./checkpoint-side-effects.js";
|
|
7
|
+
import { commitFencedEngineWrite } from "./fenced-write.js";
|
|
6
8
|
const ASYNC_ACTIVITY_TOKEN_PREFIX = "async-act:v1", ASYNC_ACTIVITY_KEY_PREFIX = "async-act:v1:";
|
|
7
9
|
export function asyncActivityWorkflowPrefix(workflowId) {
|
|
8
10
|
return `${ASYNC_ACTIVITY_KEY_PREFIX}${encodeStorageKeyComponent(workflowId)}:`;
|
|
@@ -33,7 +35,7 @@ function isPersistedAsyncActivity(value) {
|
|
|
33
35
|
export function deriveAsyncActivityToken(workflowId, step, attempt) {
|
|
34
36
|
return `${ASYNC_ACTIVITY_TOKEN_PREFIX}:${workflowId}:${step}:${attempt}`;
|
|
35
37
|
}
|
|
36
|
-
function
|
|
38
|
+
function buildPersistPendingAsyncActivityOperation(pending) {
|
|
37
39
|
const record = {
|
|
38
40
|
version: 1,
|
|
39
41
|
token: pending.token,
|
|
@@ -44,12 +46,16 @@ function persistPendingAsyncActivity(storage, pending) {
|
|
|
44
46
|
attempt: pending.attempt,
|
|
45
47
|
createdAt: pending.createdAt
|
|
46
48
|
};
|
|
47
|
-
return
|
|
49
|
+
return {
|
|
50
|
+
type: "put",
|
|
51
|
+
key: KEYS.asyncActivity(pending.workflowId, pending.token),
|
|
52
|
+
value: encode(record)
|
|
53
|
+
};
|
|
48
54
|
}
|
|
49
55
|
export async function registerPendingAsyncActivity(internals, pending) {
|
|
50
56
|
const alreadyRegistered = internals.pendingAsyncActivities.has(pending.token);
|
|
51
57
|
internals.pendingAsyncActivities.set(pending.token, pending);
|
|
52
|
-
await
|
|
58
|
+
await commitFencedEngineWrite(internals, [buildPersistPendingAsyncActivityOperation(pending)], [], () => Error(`Async activity registration for token "${pending.token}" lost its precondition.`));
|
|
53
59
|
if (!alreadyRegistered)
|
|
54
60
|
internals.engine.dispatchEvent(new ActivityAsyncPendingEvent(pending.token, pending.operationId, pending.workflowId, pending.activityName, pending.attempt));
|
|
55
61
|
}
|
|
@@ -87,12 +93,10 @@ async function consumePendingAsyncActivity(internals, token) {
|
|
|
87
93
|
if (!pending)
|
|
88
94
|
throw new AsyncActivityTokenNotFoundError(token);
|
|
89
95
|
internals.pendingAsyncActivities.delete(token);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
throw error;
|
|
95
|
-
}
|
|
96
|
+
stageAtomicWorkflowCommitSideEffects(internals, pending.workflowId, {
|
|
97
|
+
conditions: [],
|
|
98
|
+
operations: [{ type: "delete", key: KEYS.asyncActivity(pending.workflowId, token) }]
|
|
99
|
+
});
|
|
96
100
|
return pending;
|
|
97
101
|
}
|
|
98
102
|
function shouldBufferPendingAsyncActivityResolution(internals, workflowId) {
|
|
@@ -10,6 +10,7 @@ import { buildIndexOperations } from "../search-attributes.js";
|
|
|
10
10
|
import { buildWorkflowTagIndexOperations, normalizeWorkflowTags } from "../workflow-tags.js";
|
|
11
11
|
import { asyncActivityWorkflowPrefix } from "./async-activity-completion.js";
|
|
12
12
|
import { forgetCommittedCheckpointBytes } from "./checkpoint-commit-snapshots.js";
|
|
13
|
+
import { commitFencedEngineWrite } from "./fenced-write.js";
|
|
13
14
|
import { streamWorkflowStates } from "./listing.js";
|
|
14
15
|
import { createTerminalCleanupTimerId } from "./state-utilities.js";
|
|
15
16
|
import {
|
|
@@ -131,7 +132,7 @@ function getWorkflowRetentionDeadline(internals, state) {
|
|
|
131
132
|
}
|
|
132
133
|
export async function purgeWorkflow(internals, state, cleanupWaiters) {
|
|
133
134
|
const deleteOperations = await collectWorkflowPurgeDeleteOperations(internals, state);
|
|
134
|
-
await internals
|
|
135
|
+
await commitFencedEngineWrite(internals, deleteOperations, [], () => Error(`Purge commit for workflow "${state.id}" lost its precondition.`));
|
|
135
136
|
clearPurgedWorkflowInMemoryState(internals, state.id, cleanupWaiters);
|
|
136
137
|
}
|
|
137
138
|
export async function collectWorkflowPurgeDeleteOperations(internals, state) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
KEYS,
|
|
3
3
|
requireStorageCapability,
|
|
4
|
-
storageConditionalBatch,
|
|
5
4
|
storageHas
|
|
6
5
|
} from "../../storage/interface.js";
|
|
7
6
|
import { assertScopedBulkWorkflowFilter } from "../bulk-workflow-filter.js";
|
|
@@ -24,6 +23,10 @@ import {
|
|
|
24
23
|
withBulkAuditEvent
|
|
25
24
|
} from "./bulk-operations-shared.js";
|
|
26
25
|
import { BulkDeleteRequiresTerminalWorkflowsError } from "./errors.js";
|
|
26
|
+
import {
|
|
27
|
+
assertLeaseHeldForEngineWork,
|
|
28
|
+
commitFencedEngineWriteAllowingPreconditionFailure
|
|
29
|
+
} from "./fenced-write.js";
|
|
27
30
|
import { BULK_OPERATION_BATCH_SIZE } from "./listing.js";
|
|
28
31
|
import { createTerminalCleanupTimerId } from "./state-utilities.js";
|
|
29
32
|
import { loadWorkflowState, runSerializedWorkflowStateWrite } from "./storage-io.js";
|
|
@@ -132,6 +135,7 @@ async function runBulkFailedWorkflowRetry(internals, filter, options = {}) {
|
|
|
132
135
|
return withBulkAuditEvent(internals, preparation, options, result, retried);
|
|
133
136
|
}
|
|
134
137
|
export async function retryFailedAll(internals, filter, options = {}) {
|
|
138
|
+
assertLeaseHeldForEngineWork(internals);
|
|
135
139
|
return runBulkFailedWorkflowRetry(internals, filter, options);
|
|
136
140
|
}
|
|
137
141
|
async function retryFailedWorkflow(internals, workflowId) {
|
|
@@ -194,12 +198,9 @@ async function reactivateFailedWorkflowFromCheckpointSerialized(internals, workf
|
|
|
194
198
|
throw Error(`Workflow concurrency admission for "${lastConcurrencyStateKey ?? workflowId}" changed too many times while retrying failed workflow "${workflowId}"`);
|
|
195
199
|
}
|
|
196
200
|
async function commitFailedWorkflowReactivation(internals, operations, conditions) {
|
|
197
|
-
if (conditions.length
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
requireStorageCapability(internals.storage, "conditionalBatch", "retry failed workflow");
|
|
202
|
-
return storageConditionalBatch(internals.storage, conditions, operations);
|
|
201
|
+
if (conditions.length > 0)
|
|
202
|
+
requireStorageCapability(internals.storage, "conditionalBatch", "retry failed workflow");
|
|
203
|
+
return commitFencedEngineWriteAllowingPreconditionFailure(internals, operations, conditions);
|
|
203
204
|
}
|
|
204
205
|
function buildReactivatedWorkflowState(internals, state) {
|
|
205
206
|
const reactivatedState = {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ContextOperationRequest } from '../context.ts';
|
|
2
|
+
import type { Engine } from './index.ts';
|
|
3
|
+
export declare function persistCheckpointForDataOperation<TWorkflows extends object, TActivities extends object>(engine: Engine<TWorkflows, TActivities>, workflowId: string, operation: ContextOperationRequest): Promise<void>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { HISTORY_CIRCUIT_BREAKER_REASON } from "../types.js";
|
|
2
|
+
import { validateAttributeValueSizes } from "./attributes-tags.js";
|
|
3
|
+
import { createTerminationCallbacks } from "./callback-creators-core.js";
|
|
4
|
+
import {
|
|
5
|
+
appendTimelineBatchOperations,
|
|
6
|
+
persistCheckpoint,
|
|
7
|
+
pruneCheckpointHistory
|
|
8
|
+
} from "./checkpoint-io.js";
|
|
9
|
+
import { getInternals } from "./internals.js";
|
|
10
|
+
import { swallowPromiseRejection } from "./strategy-helpers.js";
|
|
11
|
+
import { terminateWorkflow } from "./termination.js";
|
|
12
|
+
export function persistCheckpointForDataOperation(engine, workflowId, operation) {
|
|
13
|
+
return persistCheckpoint(getInternals(engine), workflowId, operation, void 0, {
|
|
14
|
+
appendTimelineBatchOperations: (id, checkpointOperation, step, timestamp, operations) => appendTimelineBatchOperations(getInternals(engine), id, checkpointOperation, step, timestamp, operations),
|
|
15
|
+
swallowPromiseRejection: (promise) => {
|
|
16
|
+
swallowPromiseRejection(promise);
|
|
17
|
+
},
|
|
18
|
+
validateAttributeValueSizes,
|
|
19
|
+
pruneCheckpointHistory: (id, step) => pruneCheckpointHistory(getInternals(engine), id, step),
|
|
20
|
+
dispatchEvent: (event) => {
|
|
21
|
+
engine.dispatchEvent(event);
|
|
22
|
+
},
|
|
23
|
+
enforceHistoryCircuitBreaker: (id) => terminateWorkflow(getInternals(engine), id, "timed-out", createTerminationCallbacks(engine), HISTORY_CIRCUIT_BREAKER_REASON)
|
|
24
|
+
}, { timeline: "preserve-pending" });
|
|
25
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
broadcast as broadcastFromInternals,
|
|
4
4
|
dispatchPendingUpdateReceived as dispatchPendingUpdateReceivedFromBroadcast
|
|
5
5
|
} from "./broadcast.js";
|
|
6
|
+
import { persistCheckpointForDataOperation } from "./callback-checkpoint-persistence.js";
|
|
6
7
|
import {
|
|
7
8
|
createBroadcastCallbacks,
|
|
8
9
|
createGuardCallbacks,
|
|
@@ -97,7 +98,9 @@ export function createConditionOperationCallbacks(engine) {
|
|
|
97
98
|
}
|
|
98
99
|
export function createDataOperationCallbacks(engine) {
|
|
99
100
|
return {
|
|
100
|
-
runOperationWithResult: (workflowId, operation, execute) => runOperationWithResultForEngine(engine, workflowId, operation, execute)
|
|
101
|
+
runOperationWithResult: (workflowId, operation, execute) => runOperationWithResultForEngine(engine, workflowId, operation, execute),
|
|
102
|
+
persistCheckpoint: (workflowId, operation) => persistCheckpointForDataOperation(engine, workflowId, operation),
|
|
103
|
+
getActivityOperationCallbacks: () => createActivityOperationCallbacks(engine)
|
|
101
104
|
};
|
|
102
105
|
}
|
|
103
106
|
export function createStateOperationCallbacks(engine) {
|
|
@@ -6,6 +6,9 @@ type PendingTimelineEntryValue = {
|
|
|
6
6
|
startedAt: number;
|
|
7
7
|
entry: WorkflowTimelineEntry;
|
|
8
8
|
};
|
|
9
|
+
export type PersistCheckpointOptions = {
|
|
10
|
+
timeline?: 'record-operation' | 'preserve-pending';
|
|
11
|
+
};
|
|
9
12
|
type PersistCheckpointCallbacks = {
|
|
10
13
|
appendTimelineBatchOperations: (workflowId: string, operation: ContextOperationRequest, step: number, timestamp: number, operations: BatchOperation[]) => PendingTimelineEntryValue;
|
|
11
14
|
swallowPromiseRejection: (promise: Promise<void>) => void;
|
|
@@ -26,7 +29,7 @@ type DevelopmentCheckpointCallbacks = {
|
|
|
26
29
|
};
|
|
27
30
|
export declare function appendTimelineBatchOperations(internals: EngineInternals, workflowId: string, operation: ContextOperationRequest, step: number, timestamp: number, operations: BatchOperation[]): PendingTimelineEntryValue;
|
|
28
31
|
/** Persist a workflow checkpoint, history entry, timeline record, and event log record. */
|
|
29
|
-
export declare function persistCheckpoint(internals: EngineInternals, workflowId: string, operation: ContextOperationRequest, workerCheckpointBytes: ArrayBuffer | undefined, callbacks: PersistCheckpointCallbacks): Promise<void>;
|
|
32
|
+
export declare function persistCheckpoint(internals: EngineInternals, workflowId: string, operation: ContextOperationRequest, workerCheckpointBytes: ArrayBuffer | undefined, callbacks: PersistCheckpointCallbacks, options?: PersistCheckpointOptions): Promise<void>;
|
|
30
33
|
/** Delete the single checkpoint history entry that overflows the retention limit. */
|
|
31
34
|
export declare function pruneCheckpointHistory(internals: EngineInternals, workflowId: string, currentStep: number): Promise<void>;
|
|
32
35
|
/** Validate checkpoint serialization in development mode and dispatch warnings. */
|
|
@@ -60,13 +60,13 @@ export function appendTimelineBatchOperations(internals, workflowId, operation,
|
|
|
60
60
|
entry
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
-
export async function persistCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks) {
|
|
63
|
+
export async function persistCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks, options = {}) {
|
|
64
64
|
if (internals.inlineStrategy?.getContext(workflowId))
|
|
65
|
-
await persistInlineCheckpoint(internals, workflowId, operation, callbacks);
|
|
65
|
+
await persistInlineCheckpoint(internals, workflowId, operation, callbacks, options);
|
|
66
66
|
else if (workerCheckpointBytes && workerCheckpointBytes.byteLength > 0)
|
|
67
|
-
await persistWorkerCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks);
|
|
67
|
+
await persistWorkerCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks, options);
|
|
68
68
|
}
|
|
69
|
-
async function persistInlineCheckpoint(internals, workflowId, operation, callbacks) {
|
|
69
|
+
async function persistInlineCheckpoint(internals, workflowId, operation, callbacks, options) {
|
|
70
70
|
const current = internals.checkpoints.get(workflowId), context = internals.inlineStrategy?.getContext(workflowId);
|
|
71
71
|
if (!current || !context)
|
|
72
72
|
return;
|
|
@@ -76,13 +76,13 @@ async function persistInlineCheckpoint(internals, workflowId, operation, callbac
|
|
|
76
76
|
...pendingAttributeChanges !== void 0 ? { searchAttributes: pendingAttributeChanges } : {}
|
|
77
77
|
}), pruned = pruneCheckpointReplayState(advanced, resolvePendingOperationStep(operation, context.stepIndex)), commit = createCheckpointCommit(internals, workflowId, pruned.checkpoint, serializeCheckpoint(attachTransientCheckpointReplayPayload(pruned.checkpoint, pruned.replayPayload)), pruned.replayPayload);
|
|
78
78
|
appendAttributeOperations(workflowId, commit, previousAttributes, pendingAttributeChanges, hasPendingAttributeChangesValue, callbacks);
|
|
79
|
-
await commitCheckpoint(internals, workflowId, operation, commit, callbacks);
|
|
79
|
+
await commitCheckpoint(internals, workflowId, operation, commit, callbacks, options);
|
|
80
80
|
if (hasPendingAttributeChangesValue)
|
|
81
81
|
callbacks.dispatchEvent(new AttributesChangedEvent(workflowId, { ...pendingAttributeChanges }));
|
|
82
82
|
}
|
|
83
|
-
async function persistWorkerCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks) {
|
|
83
|
+
async function persistWorkerCheckpoint(internals, workflowId, operation, workerCheckpointBytes, callbacks, options) {
|
|
84
84
|
const serialized = new Uint8Array(workerCheckpointBytes), checkpoint = deserializeCheckpoint(serialized), workerReplayPayload = readCheckpointReplayPayload(checkpoint), pruned = pruneCheckpointReplayState(checkpoint, resolvePendingOperationStep(operation, checkpoint.step)), replayPayload = mergeCheckpointReplayPayloads(workerReplayPayload, pruned.replayPayload), prunedSerialized = serializeCheckpoint(attachTransientCheckpointReplayPayload(pruned.checkpoint, replayPayload));
|
|
85
|
-
await commitCheckpoint(internals, workflowId, operation, createCheckpointCommit(internals, workflowId, pruned.checkpoint, prunedSerialized, replayPayload), callbacks);
|
|
85
|
+
await commitCheckpoint(internals, workflowId, operation, createCheckpointCommit(internals, workflowId, pruned.checkpoint, prunedSerialized, replayPayload), callbacks, options);
|
|
86
86
|
}
|
|
87
87
|
function createCheckpointCommit(internals, workflowId, checkpoint, serialized, replayPayload) {
|
|
88
88
|
const operations = [
|
|
@@ -116,9 +116,9 @@ function appendAttributeOperations(workflowId, commit, previousAttributes, pendi
|
|
|
116
116
|
});
|
|
117
117
|
commit.operations.push(...buildIndexOperations(workflowId, previousAttributes, commit.checkpoint.searchAttributes));
|
|
118
118
|
}
|
|
119
|
-
async function commitCheckpoint(internals, workflowId, operation, commit, callbacks) {
|
|
119
|
+
async function commitCheckpoint(internals, workflowId, operation, commit, callbacks, options) {
|
|
120
120
|
dispatchCheckpointSizeWarning(internals, workflowId, commit, callbacks);
|
|
121
|
-
const nextPendingTimelineEntry = callbacks.appendTimelineBatchOperations(workflowId, operation, commit.step, commit.timestamp, commit.operations), { newHead, timestamp } = appendCheckpointEventLog(internals, workflowId, commit), retentionWindow = internals.options.historyPolicy.retentionWindow, compaction = retentionWindow === null ? null : await appendCompactionOperations(internals.storage, workflowId, newHead.sequence, retentionWindow, commit.operations), pendingSideEffects = takePendingAtomicWorkflowCommitSideEffects(internals, workflowId);
|
|
121
|
+
const nextPendingTimelineEntry = options.timeline === "preserve-pending" ? preservePendingTimelineEntry(internals, workflowId, commit.operations) : callbacks.appendTimelineBatchOperations(workflowId, operation, commit.step, commit.timestamp, commit.operations), { newHead, timestamp } = appendCheckpointEventLog(internals, workflowId, commit), retentionWindow = internals.options.historyPolicy.retentionWindow, compaction = retentionWindow === null ? null : await appendCompactionOperations(internals.storage, workflowId, newHead.sequence, retentionWindow, commit.operations), pendingSideEffects = takePendingAtomicWorkflowCommitSideEffects(internals, workflowId);
|
|
122
122
|
if (pendingSideEffects !== void 0)
|
|
123
123
|
commit.operations.push(...pendingSideEffects.operations);
|
|
124
124
|
const storageSupportsConditionalBatch = internals.storage.capabilities().conditionalBatch, sideEffectConditions = checkpointSideEffectConditions(pendingSideEffects, storageSupportsConditionalBatch), conditions = buildCheckpointCommitConditions(workflowId, commit, storageSupportsConditionalBatch, sideEffectConditions);
|
|
@@ -129,7 +129,10 @@ async function commitCheckpoint(internals, workflowId, operation, commit, callba
|
|
|
129
129
|
clearPendingAtomicWorkflowCommitSideEffects(internals, workflowId);
|
|
130
130
|
if (commit.expectedSerialized !== void 0)
|
|
131
131
|
rememberCommittedCheckpointBytes(internals, workflowId, commit.serialized);
|
|
132
|
-
|
|
132
|
+
if (nextPendingTimelineEntry === void 0)
|
|
133
|
+
internals.pendingTimelineEntries.delete(workflowId);
|
|
134
|
+
else
|
|
135
|
+
internals.pendingTimelineEntries.set(workflowId, nextPendingTimelineEntry);
|
|
133
136
|
internals.checkpoints.set(workflowId, commit.checkpoint);
|
|
134
137
|
internals.eventLogHeads.set(workflowId, newHead);
|
|
135
138
|
if (compaction !== null)
|
|
@@ -146,6 +149,12 @@ async function commitCheckpoint(internals, workflowId, operation, commit, callba
|
|
|
146
149
|
if (historyEventLimitBreached(internals, newHead.sequence))
|
|
147
150
|
await callbacks.enforceHistoryCircuitBreaker(workflowId);
|
|
148
151
|
}
|
|
152
|
+
function preservePendingTimelineEntry(internals, workflowId, operations) {
|
|
153
|
+
const pendingTimelineOperation = buildPendingTimelineOperation(internals, workflowId);
|
|
154
|
+
if (pendingTimelineOperation)
|
|
155
|
+
operations.push(pendingTimelineOperation);
|
|
156
|
+
return internals.pendingTimelineEntries.get(workflowId);
|
|
157
|
+
}
|
|
149
158
|
function checkpointSideEffectConditions(pendingSideEffects, storageSupportsConditionalBatch) {
|
|
150
159
|
if (!storageSupportsConditionalBatch)
|
|
151
160
|
return [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Storage } from '../../storage/interface.ts';
|
|
2
2
|
import type { HumanReviewResult, ReviewRequest } from '../review/index.ts';
|
|
3
3
|
import type { CompletedReviewEntry, ReviewListFilter } from '../types.ts';
|
|
4
|
+
import type { EngineInternals } from './internals.ts';
|
|
4
5
|
type ReviewListFilterableEntry = {
|
|
5
6
|
workflowId?: string;
|
|
6
7
|
reviewType?: string;
|
|
@@ -9,6 +10,6 @@ export declare function matchesReviewListFilter(review: ReviewListFilterableEntr
|
|
|
9
10
|
export declare function completedReviewStoragePrefix(workflowId?: string): string;
|
|
10
11
|
export declare function completedReviewStorageKey(workflowId: string, reviewId: string): string;
|
|
11
12
|
export declare function listCompletedReviewsFromStorage(storage: Storage, filter: ReviewListFilter): Promise<CompletedReviewEntry[]>;
|
|
12
|
-
export declare function persistCompletedReviewRecord(
|
|
13
|
+
export declare function persistCompletedReviewRecord(internals: EngineInternals, reviewKey: string, reviewData: ReviewRequest, decisionResult: HumanReviewResult): Promise<void>;
|
|
13
14
|
export declare function deleteCompletedReviewsForWorkflow(storage: Storage, workflowId: string): Promise<void>;
|
|
14
15
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { encodeStorageKeyComponent } from "../../storage/interface.js";
|
|
2
2
|
import { encode } from "../codec.js";
|
|
3
|
+
import { commitFencedEngineWrite } from "./fenced-write.js";
|
|
3
4
|
import { parseCompletedReviewEntry, toCompletedReviewEntry } from "./review-list-entries.js";
|
|
4
5
|
export function matchesReviewListFilter(review, filter) {
|
|
5
6
|
if (filter.workflowId !== void 0 && review.workflowId !== filter.workflowId)
|
|
@@ -32,16 +33,16 @@ export async function listCompletedReviewsFromStorage(storage, filter) {
|
|
|
32
33
|
await appendCompletedReviews(reviews, storage.scan("review-decision:"), filter, (key) => scopedPrefix === null || !key.startsWith(scopedPrefix));
|
|
33
34
|
return reviews;
|
|
34
35
|
}
|
|
35
|
-
export async function persistCompletedReviewRecord(
|
|
36
|
+
export async function persistCompletedReviewRecord(internals, reviewKey, reviewData, decisionResult) {
|
|
36
37
|
const completedReview = toCompletedReviewEntry(reviewData, decisionResult);
|
|
37
|
-
await
|
|
38
|
+
await commitFencedEngineWrite(internals, [
|
|
38
39
|
{
|
|
39
40
|
type: "put",
|
|
40
41
|
key: completedReviewStorageKey(reviewData.workflowId, reviewData.reviewId),
|
|
41
42
|
value: encode(completedReview)
|
|
42
43
|
},
|
|
43
44
|
{ type: "delete", key: reviewKey }
|
|
44
|
-
]);
|
|
45
|
+
], [], () => Error(`Completed review commit for review "${reviewData.reviewId}" lost its precondition.`));
|
|
45
46
|
}
|
|
46
47
|
export async function deleteCompletedReviewsForWorkflow(storage, workflowId) {
|
|
47
48
|
const completedPrefix = completedReviewStoragePrefix(workflowId), deleteOperations = [];
|
|
@@ -6,6 +6,7 @@ import { WorkerPool } from "../../workers/pool.js";
|
|
|
6
6
|
import { optionalInlineDependencies } from "../inline-execution-strategy.context-options.js";
|
|
7
7
|
import { InlineExecutionStrategy } from "../inline-execution-strategy.js";
|
|
8
8
|
import {
|
|
9
|
+
DEFAULT_POLL_INTERVAL_MS,
|
|
9
10
|
DEFAULT_RETENTION_SWEEP_BATCH_SIZE,
|
|
10
11
|
DEFAULT_RETENTION_SWEEP_INTERVAL_MS
|
|
11
12
|
} from "../types.js";
|
|
@@ -96,7 +97,8 @@ function resolveNumericDefaults(options) {
|
|
|
96
97
|
return {
|
|
97
98
|
checkpointHistory: defaultTo(options?.checkpointHistory, 10),
|
|
98
99
|
checkpointSizeWarningThreshold: defaultTo(options?.checkpointSizeWarningThreshold, 65536),
|
|
99
|
-
maxNestingDepth: defaultTo(options?.maxNestingDepth, 10)
|
|
100
|
+
maxNestingDepth: defaultTo(options?.maxNestingDepth, 10),
|
|
101
|
+
schedulerPollIntervalMs: normalizePositiveSafeIntegerOption(options?.schedulerPollIntervalMs, "options.schedulerPollIntervalMs", DEFAULT_POLL_INTERVAL_MS) ?? DEFAULT_POLL_INTERVAL_MS
|
|
100
102
|
};
|
|
101
103
|
}
|
|
102
104
|
function resolveHistoryFields(options) {
|
|
@@ -27,6 +27,8 @@ export interface ResolvedOptions {
|
|
|
27
27
|
checkpointHistory: number;
|
|
28
28
|
checkpointSizeWarningThreshold: number;
|
|
29
29
|
maxNestingDepth: number;
|
|
30
|
+
/** Durable-timer scheduler poll interval (ms). */
|
|
31
|
+
schedulerPollIntervalMs: number;
|
|
30
32
|
broadcastEvents: boolean;
|
|
31
33
|
retention: NormalizedRetentionPolicy | null;
|
|
32
34
|
retentionSweepIntervalMs: number;
|
|
@@ -289,14 +289,14 @@ export declare class ActivityResolutionError extends WeftError<'ActivityResoluti
|
|
|
289
289
|
/**
|
|
290
290
|
* Thrown by {@link Engine.startOrSignal} when the target workflow already exists
|
|
291
291
|
* and is in a terminal state (completed, failed, cancelled, or timed out). A
|
|
292
|
-
* terminal run cannot accept a signal
|
|
293
|
-
*
|
|
294
|
-
*
|
|
292
|
+
* terminal run cannot accept a signal. By default `startOrSignal` surfaces this
|
|
293
|
+
* conflict instead of starting a fresh run under the same id or dropping the
|
|
294
|
+
* signal.
|
|
295
295
|
*
|
|
296
296
|
* Inspect `workflowId` to identify the conflicting run and `status` to see why
|
|
297
|
-
* it was rejected. To deliberately
|
|
298
|
-
*
|
|
299
|
-
*
|
|
297
|
+
* it was rejected. To deliberately reuse a stable id, call `startOrSignal` with
|
|
298
|
+
* `onTerminalConflict: 'start-new'`, an explicit `id`, and a deterministic
|
|
299
|
+
* `signalId`; otherwise choose a different id or idempotency key.
|
|
300
300
|
*
|
|
301
301
|
* @example
|
|
302
302
|
* ```ts
|
|
@@ -118,7 +118,7 @@ export class StartOrSignalConflictError extends WeftError {
|
|
|
118
118
|
workflowId;
|
|
119
119
|
status;
|
|
120
120
|
constructor(workflowId, status) {
|
|
121
|
-
super("StartOrSignalConflictError", `Workflow "${workflowId}" is already in terminal state "${status}" and cannot accept a startOrSignal:
|
|
121
|
+
super("StartOrSignalConflictError", `Workflow "${workflowId}" is already in terminal state "${status}" and cannot accept a startOrSignal. Use options.onTerminalConflict: 'start-new' with an explicit id and signalId, or choose a different id or idempotency key to start a new run.`);
|
|
122
122
|
this.workflowId = workflowId;
|
|
123
123
|
this.status = status;
|
|
124
124
|
}
|
|
@@ -129,7 +129,7 @@ export class WorkflowHandle extends EventTarget {
|
|
|
129
129
|
observer.error?.(event.error);
|
|
130
130
|
} else if (event instanceof WorkflowTimedOutEvent) {
|
|
131
131
|
terminalDelivered = !0;
|
|
132
|
-
observer.error?.(new WorkflowTimeoutError(event.workflowId, event.timeoutType, event.elapsed));
|
|
132
|
+
observer.error?.(new WorkflowTimeoutError(event.workflowId, event.timeoutType, event.elapsed, event.reason));
|
|
133
133
|
}
|
|
134
134
|
};
|
|
135
135
|
this.addEventListener("workflow:failed", errorHandler, { signal: controller.signal });
|
|
@@ -5,7 +5,7 @@ import type { TypedEventTarget, WeftEventMap } from '../events.ts';
|
|
|
5
5
|
import type { Interceptor } from '../interceptor.ts';
|
|
6
6
|
import { type ReviewRequest } from '../review/index.ts';
|
|
7
7
|
import { Scheduler } from '../scheduler.ts';
|
|
8
|
-
import { type AnyActivityDefinition, type AnyWorkflowDefinition, type AttributeFilterKey, type BulkCancelResult, type BulkDeleteResult, type BulkOperationCommitOptions, type BulkOperationDryRunOptions, type BulkOperationDryRunResult, type BulkRetryFailedResult, type BulkSignalAllCommitOptions, type BulkSignalAllDryRunOptions, type BulkSignalResult, type BulkTagResult, type CheckpointState, type CheckpointSummary, type CoordinatedUpdateResult, type DefaultActivityTypes, type DefaultWorkflowRegistry, type ForkOptions, type InferActivityEntries, type InferActivityEntry, type InferWorkflowEntries, type InferWorkflowEntry, type ListFilter, type ListOptions, type PaginatedResult, type PurgeResult, type QueryDefinition, type RegisteredWorkflowDefinition, type RetentionOverview, type ReviewListEntry, type ReviewListFilter, type ScheduleDefinition, type ScheduleFilter, type ScheduleOptions, type ScheduleSpec, type ScheduleSummary, type SearchAttributeValue, type SignalDefinition, type SignalDeliveryOptions, type
|
|
8
|
+
import { type AnyActivityDefinition, type AnyWorkflowDefinition, type AttributeFilterKey, type BulkCancelResult, type BulkDeleteResult, type BulkOperationCommitOptions, type BulkOperationDryRunOptions, type BulkOperationDryRunResult, type BulkRetryFailedResult, type BulkSignalAllCommitOptions, type BulkSignalAllDryRunOptions, type BulkSignalResult, type BulkTagResult, type CheckpointState, type CheckpointSummary, type CoordinatedUpdateResult, type DefaultActivityTypes, type DefaultWorkflowRegistry, type ForkOptions, type InferActivityEntries, type InferActivityEntry, type InferWorkflowEntries, type InferWorkflowEntry, type ListFilter, type ListOptions, type PaginatedResult, type PurgeResult, type QueryDefinition, type RegisteredWorkflowDefinition, type RetentionOverview, type ReviewListEntry, type ReviewListFilter, type ScheduleDefinition, type ScheduleFilter, type ScheduleOptions, type ScheduleSpec, type ScheduleSummary, type SearchAttributeValue, type SignalDefinition, type SignalDeliveryOptions, type StartOrSignalOptions, type StartOrSignalSignal, type StartWorkflowOptions, type SubmitReviewOptions, type TypedListFilter, type UpdateDefinition, type WorkflowEvent, type WorkflowInput, type WorkflowOutput, type WorkflowReplay, type WorkflowServices, type WorkflowServicesUnion, type WorkflowState, type WorkflowSummary, type WorkflowTimelineEntry } from '../types.ts';
|
|
9
9
|
import type { TimerEntry } from '../types/checkpoint.ts';
|
|
10
10
|
import type { WorkflowAlreadyRegistered } from '../types/workflow-builder.ts';
|
|
11
11
|
import { type AggregateOptions, type AggregateResult } from './aggregate.ts';
|
|
@@ -36,6 +36,25 @@ export { assertCompatiblePersistedDataVersion };
|
|
|
36
36
|
export declare const ENGINE_PARKED_WORKFLOW_COUNT_FOR_TESTING: unique symbol;
|
|
37
37
|
export declare const ENGINE_SIGNAL_WAITER_COUNT_FOR_TESTING: unique symbol;
|
|
38
38
|
export declare const ENGINE_SLEEP_RESOLVER_COUNT_FOR_TESTING: unique symbol;
|
|
39
|
+
/**
|
|
40
|
+
* The `name` of the `process` warning emitted when a lease-owning engine is
|
|
41
|
+
* disposed through the synchronous `[Symbol.dispose]()` path. Sync disposal can
|
|
42
|
+
* only fire the lease release in the background; use `await engine.shutdown()`,
|
|
43
|
+
* `await using`, or `await engine[Symbol.asyncDispose]()` when prompt rolling
|
|
44
|
+
* deploy handoff matters.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME } from '@lostgradient/weft';
|
|
49
|
+
*
|
|
50
|
+
* process.on('warning', (warning) => {
|
|
51
|
+
* if (warning.name === ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME) {
|
|
52
|
+
* // Alert on shutdown paths that can make lease handoff wait for leaseTtl.
|
|
53
|
+
* }
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare const ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME = "WeftEngineLeaseSynchronousDisposeWarning";
|
|
39
58
|
export { ENGINE_LEASE_LOST_WARNING_NAME };
|
|
40
59
|
/**
|
|
41
60
|
* Durable execution engine.
|
|
@@ -202,8 +221,9 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
|
|
|
202
221
|
* first signal commit in one batch and the freshly-launched run consumes the
|
|
203
222
|
* signal on its first drive. A non-terminal target (running, pending, or
|
|
204
223
|
* suspended) is signalled through the normal signal path; a terminal target
|
|
205
|
-
* throws {@link StartOrSignalConflictError}
|
|
206
|
-
*
|
|
224
|
+
* throws {@link StartOrSignalConflictError} unless
|
|
225
|
+
* `options.onTerminalConflict: 'start-new'` is supplied with an explicit
|
|
226
|
+
* workflow id and deterministic `signal.signalId`.
|
|
207
227
|
*
|
|
208
228
|
* Concurrent callers converge on one workflow and one delivered signal. Pass
|
|
209
229
|
* `options.idempotencyKey` to dedup independent callers (e.g. retried
|
|
@@ -213,8 +233,8 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
|
|
|
213
233
|
* are `options.id` and `options.idempotencyKey`. Requires a storage backend
|
|
214
234
|
* with `conditionalBatch`.
|
|
215
235
|
*/
|
|
216
|
-
startOrSignal<TName extends KnownWorkflowNames<TWorkflows>>(type: TName, input: WorkflowInput<TWorkflows, TName>, signal: StartOrSignalSignal, options?:
|
|
217
|
-
startOrSignal<TName extends string>(type: UnknownWorkflowNameWhenDefaultRegistryIsEmpty<TWorkflows, TName>, input: unknown, signal: StartOrSignalSignal, options?:
|
|
236
|
+
startOrSignal<TName extends KnownWorkflowNames<TWorkflows>>(type: TName, input: WorkflowInput<TWorkflows, TName>, signal: StartOrSignalSignal, options?: StartOrSignalOptions<WorkflowServices<TWorkflows, TName>>): Promise<StartOrSignalResult<WorkflowOutput<TWorkflows, TName>>>;
|
|
237
|
+
startOrSignal<TName extends string>(type: UnknownWorkflowNameWhenDefaultRegistryIsEmpty<TWorkflows, TName>, input: unknown, signal: StartOrSignalSignal, options?: StartOrSignalOptions): Promise<StartOrSignalResult>;
|
|
218
238
|
getHandle(workflowId: string): WorkflowHandle;
|
|
219
239
|
list<const TAttributeKeys extends readonly AttributeFilterKey[] = readonly AttributeFilterKey[]>(filter?: TypedListFilter<TAttributeKeys>, options?: ListOptions): Promise<PaginatedResult<WorkflowSummary>>;
|
|
220
240
|
aggregate(filter: ListFilter | undefined, options: AggregateOptions): Promise<AggregateResult>;
|
|
@@ -407,6 +427,18 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
|
|
|
407
427
|
timeout?: number;
|
|
408
428
|
idempotencyKey?: string;
|
|
409
429
|
}): Promise<CoordinatedUpdateResult>;
|
|
430
|
+
/**
|
|
431
|
+
* Awaited engine shutdown. Equivalent to
|
|
432
|
+
* `await engine[Symbol.asyncDispose]()` and useful in explicit signal handlers
|
|
433
|
+
* where `await using` syntax cannot own the process lifetime directly.
|
|
434
|
+
*
|
|
435
|
+
* Under `ownership: 'lease'`, this is the explicit prompt-handoff primitive:
|
|
436
|
+
* it drains queued inline starts, tears down in-memory write paths, and awaits
|
|
437
|
+
* lease release before resolving. Synchronous disposal remains immediate and
|
|
438
|
+
* can make the next engine wait for `leaseTtl` if the process exits before its
|
|
439
|
+
* background release completes.
|
|
440
|
+
*/
|
|
441
|
+
shutdown(): Promise<void>;
|
|
410
442
|
/**
|
|
411
443
|
* Synchronous teardown (`using engine = ...`). Pending inline launches that
|
|
412
444
|
* have not yet run are **discarded**, not executed. When you need queued
|
|
@@ -206,7 +206,7 @@ export {
|
|
|
206
206
|
shouldEmitEngineLeakWarningForTesting
|
|
207
207
|
} from "./engine-leak-warnings.js";
|
|
208
208
|
export { assertCompatiblePersistedDataVersion };
|
|
209
|
-
export const ENGINE_PARKED_WORKFLOW_COUNT_FOR_TESTING = Symbol("engineParkedWorkflowCountForTesting"), ENGINE_SIGNAL_WAITER_COUNT_FOR_TESTING = Symbol("engineSignalWaiterCountForTesting"), ENGINE_SLEEP_RESOLVER_COUNT_FOR_TESTING = Symbol("engineSleepResolverCountForTesting");
|
|
209
|
+
export const ENGINE_PARKED_WORKFLOW_COUNT_FOR_TESTING = Symbol("engineParkedWorkflowCountForTesting"), ENGINE_SIGNAL_WAITER_COUNT_FOR_TESTING = Symbol("engineSignalWaiterCountForTesting"), ENGINE_SLEEP_RESOLVER_COUNT_FOR_TESTING = Symbol("engineSleepResolverCountForTesting"), ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME = "WeftEngineLeaseSynchronousDisposeWarning";
|
|
210
210
|
|
|
211
211
|
export { ENGINE_LEASE_LOST_WARNING_NAME };
|
|
212
212
|
|
|
@@ -289,6 +289,7 @@ export class Engine extends EventTarget {
|
|
|
289
289
|
getInternals(this).scheduler = new Scheduler({
|
|
290
290
|
storage,
|
|
291
291
|
onTimerFired: (entry) => handleTimerFiredFromInternals(getInternals(this), entry, this.#createTimeOperationCallbacks()),
|
|
292
|
+
pollIntervalMs: resolvedOptions.schedulerPollIntervalMs,
|
|
292
293
|
getNow,
|
|
293
294
|
commitTimerCleanup: (operations) => commitFencedEngineWrite(getInternals(this), operations, [], () => Error("Timer cleanup lost its fenced CAS race"))
|
|
294
295
|
});
|
|
@@ -768,8 +769,13 @@ export class Engine extends EventTarget {
|
|
|
768
769
|
async submitCoordinatedUpdate(workflowId, name, payload, options) {
|
|
769
770
|
return submitCoordinatedUpdateFromInternals(getInternals(this), workflowId, name, payload, options, this.#createUpdateCallbacks());
|
|
770
771
|
}
|
|
772
|
+
async shutdown() {
|
|
773
|
+
return this[Symbol.asyncDispose]();
|
|
774
|
+
}
|
|
771
775
|
[Symbol.dispose]() {
|
|
772
776
|
const leaseManager = getInternals(this).leaseManager;
|
|
777
|
+
if (leaseManager !== null && leaseManager.currentEpochBytes() !== null)
|
|
778
|
+
process.emitWarning("engine ownership lease disposed synchronously; lease release is fire-and-forget, so exiting before the release completes can make the next instance wait for leaseTtl. Use await engine.shutdown(), await using, or await engine[Symbol.asyncDispose]() for prompt rolling-deploy handoff.", ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME);
|
|
773
779
|
disposeEngine(getInternals(this));
|
|
774
780
|
leaseManager?.release();
|
|
775
781
|
}
|
|
@@ -31,6 +31,7 @@ import type { Scheduler } from '../scheduler.ts';
|
|
|
31
31
|
import type { Checkpoint, StartWorkflowOptions } from '../types.ts';
|
|
32
32
|
import type { UpdateCoordinator } from '../updates.ts';
|
|
33
33
|
import type { WorkflowVersionTuple } from '../workflow-version-tuple.ts';
|
|
34
|
+
import type { ActivityHeartbeatKey } from './activity-heartbeat-tracking.ts';
|
|
34
35
|
import type { PendingTimelineEntry, QueuedInlineWorkflowExecutionStart, RegistrationEntry, ResolvedOptions, TrackedWaiterKeys, WorkflowResultWaiter } from './engine-internal-types.ts';
|
|
35
36
|
import type { EngineCleanupIntervalDisposalTracker } from './engine-leak-warnings.ts';
|
|
36
37
|
import type { WorkflowHandle, WorkflowHandleEngine } from './handles.ts';
|
|
@@ -148,7 +149,7 @@ export interface EngineInternals {
|
|
|
148
149
|
* {@link heartbeatDetails}. Inline-execution only; worker-executed activities run
|
|
149
150
|
* their function out of process and never observe this.
|
|
150
151
|
*/
|
|
151
|
-
lastHeartbeatDetailsByStep: Map<string, Map<
|
|
152
|
+
lastHeartbeatDetailsByStep: Map<string, Map<ActivityHeartbeatKey, unknown>>;
|
|
152
153
|
/**
|
|
153
154
|
* Per-run, non-serialized `services` value exposed to the workflow body as
|
|
154
155
|
* `ctx.services`. Set at `engine.start({ services })` and re-provided on
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { EngineLeaseAcquisitionTimeoutError, EngineLeaseCorruptedError } from "./lease-errors.js";
|
|
9
9
|
const DEFAULT_ACQUIRE_POLL_INTERVAL_MS = 1000;
|
|
10
10
|
export function createLeaseManager(options) {
|
|
11
|
-
const { storage, holderId, getNow, ttlMs, renewIntervalMs, waitTimeoutMs } = options, acquirePollIntervalMs = options.acquirePollIntervalMs ?? DEFAULT_ACQUIRE_POLL_INTERVAL_MS
|
|
11
|
+
const { storage, holderId, getNow, ttlMs, renewIntervalMs, waitTimeoutMs } = options, acquirePollIntervalMs = options.acquirePollIntervalMs ?? DEFAULT_ACQUIRE_POLL_INTERVAL_MS;
|
|
12
12
|
let stopped = !1, heldEpoch = null, heldEpochBytes = null, lastHolderBytes = null, renewalInterval = null, leaseLost = !1, inFlightRenewal = null;
|
|
13
13
|
const delay = options.delay ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
14
14
|
function reportLeaseLost(reason) {
|
|
@@ -81,7 +81,7 @@ export function createLeaseManager(options) {
|
|
|
81
81
|
committed = await storageConditionalBatch(storage, [{ key: KEYS.leaseHolder(), expectedValue: lastHolderBytes }], [{ type: "put", key: KEYS.leaseHolder(), value: holderBytes }]);
|
|
82
82
|
} catch {
|
|
83
83
|
const priorExpiry = decodeHolder(lastHolderBytes)?.expiresAt ?? 0;
|
|
84
|
-
if (getNow() >= priorExpiry
|
|
84
|
+
if (getNow() >= priorExpiry)
|
|
85
85
|
reportLeaseLost("renewal-unconfirmable");
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
@@ -24,7 +24,8 @@ export async function reprovideRecoveredServices(internals, state, failRun, onCo
|
|
|
24
24
|
const resolution = await resolver({
|
|
25
25
|
workflowId: state.id,
|
|
26
26
|
workflowType: state.type,
|
|
27
|
-
input: state.input
|
|
27
|
+
input: state.input,
|
|
28
|
+
launchOptions: launchOptionsFromWorkflowState(state)
|
|
28
29
|
});
|
|
29
30
|
if (resolution.status === "available") {
|
|
30
31
|
internals.workflowServices.set(state.id, resolution.services);
|
|
@@ -47,3 +48,9 @@ export function unavailableServicesError(workflowId, reason) {
|
|
|
47
48
|
function missingServicesResolverReason(workflowId) {
|
|
48
49
|
return `Workflow "${workflowId}" was started with services, but this engine has no resolveWorkflowServices option. Configure EngineOptions.resolveWorkflowServices before recovery so ctx.services can be re-provided.`;
|
|
49
50
|
}
|
|
51
|
+
function launchOptionsFromWorkflowState(state) {
|
|
52
|
+
return {
|
|
53
|
+
id: state.id,
|
|
54
|
+
...state.tags !== void 0 && state.tags.length > 0 ? { tags: [...state.tags] } : {}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module core/engine/lifecycle/start-or-signal-create
|
|
9
9
|
*/
|
|
10
|
-
import type {
|
|
10
|
+
import type { StartOrSignalOptions, StartOrSignalSignal } from '../../types.ts';
|
|
11
11
|
import { type StartOrSignalOutcome, type WorkflowHandle } from '../handles.ts';
|
|
12
12
|
import type { EngineInternals } from '../internals.ts';
|
|
13
13
|
import { type BuildIdempotentStartOperations } from './start-commit.ts';
|
|
@@ -55,4 +55,4 @@ export type StartOrSignalResult<TResult = unknown> = {
|
|
|
55
55
|
* (scan-then-park), and the caller's payload loses to the pre-buffered one by
|
|
56
56
|
* the same first-wins dedup.
|
|
57
57
|
*/
|
|
58
|
-
export declare function createWithSignalOrFallback(internals: EngineInternals, type: string, input: unknown, signalSpec: StartOrSignalSignal, signalId: string, options:
|
|
58
|
+
export declare function createWithSignalOrFallback(internals: EngineInternals, type: string, input: unknown, signalSpec: StartOrSignalSignal, signalId: string, options: StartOrSignalOptions | undefined, callbacks: StartOrSignalCallbacks): Promise<StartOrSignalResult>;
|
|
@@ -44,23 +44,25 @@ export async function createWithSignalOrFallback(internals, type, input, signalS
|
|
|
44
44
|
});
|
|
45
45
|
if (outcome.kind === "created")
|
|
46
46
|
return { handle: outcome.handle, outcome: "started" };
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
outcome: "signalled"
|
|
51
|
-
};
|
|
52
|
-
if (outcome.kind === "lost-caller-id") {
|
|
53
|
-
const resolved = await resolveCallerIdWinnerOrRetry(internals, outcome.id, signalSpec, signalId, callbacks);
|
|
54
|
-
if (resolved !== void 0)
|
|
55
|
-
return { handle: resolved, outcome: "signalled" };
|
|
56
|
-
} else {
|
|
57
|
-
const resolved = await plainCreateBufferedSignalOrResolve(internals, type, input, signalSpec, signalId, options, callbacks);
|
|
58
|
-
if (resolved !== void 0)
|
|
59
|
-
return resolved;
|
|
60
|
-
}
|
|
47
|
+
const resolved = await resolveCreateFallbackOutcome(internals, outcome, type, input, signalSpec, signalId, options, callbacks);
|
|
48
|
+
if (resolved !== void 0)
|
|
49
|
+
return resolved;
|
|
61
50
|
}
|
|
62
51
|
throw Error(`startOrSignal could not create workflow "${options?.id ?? "<generated>"}" after ${CALLER_ID_CREATE_MAX_ATTEMPTS} attempts: each concurrent same-id winner aborted before its durable commit.`);
|
|
63
52
|
}
|
|
53
|
+
async function resolveCreateFallbackOutcome(internals, outcome, type, input, signalSpec, signalId, options, callbacks) {
|
|
54
|
+
if (outcome.kind === "lost-keyed")
|
|
55
|
+
return {
|
|
56
|
+
handle: await resolveWinnerWithSignal(internals, outcome.id, signalSpec, signalId, callbacks, outcome.idempotencyKey),
|
|
57
|
+
outcome: "signalled"
|
|
58
|
+
};
|
|
59
|
+
const allowTerminalRestart = options?.onTerminalConflict === "start-new";
|
|
60
|
+
if (outcome.kind === "lost-caller-id") {
|
|
61
|
+
const resolved = await resolveCallerIdWinnerOrRetry(internals, outcome.id, signalSpec, signalId, callbacks, allowTerminalRestart);
|
|
62
|
+
return resolved === void 0 ? void 0 : { handle: resolved, outcome: "signalled" };
|
|
63
|
+
}
|
|
64
|
+
return plainCreateBufferedSignalOrResolve(internals, type, input, signalSpec, signalId, options, callbacks);
|
|
65
|
+
}
|
|
64
66
|
async function plainCreateBufferedSignalOrResolve(internals, type, input, signalSpec, signalId, options, callbacks) {
|
|
65
67
|
try {
|
|
66
68
|
return {
|
|
@@ -70,7 +72,7 @@ async function plainCreateBufferedSignalOrResolve(internals, type, input, signal
|
|
|
70
72
|
} catch (error) {
|
|
71
73
|
if (!(error instanceof WorkflowAlreadyExistsError))
|
|
72
74
|
throw error;
|
|
73
|
-
const resolved = await resolveCallerIdWinnerOrRetry(internals, error.workflowId, signalSpec, signalId, callbacks);
|
|
75
|
+
const resolved = await resolveCallerIdWinnerOrRetry(internals, error.workflowId, signalSpec, signalId, callbacks, options?.onTerminalConflict === "start-new");
|
|
74
76
|
return resolved === void 0 ? void 0 : { handle: resolved, outcome: "signalled" };
|
|
75
77
|
}
|
|
76
78
|
}
|
|
@@ -47,7 +47,7 @@ export declare function resolveExistingRunOrThrowPurged(internals: EngineInterna
|
|
|
47
47
|
* committing (storage failure, oversized payload, throwing start interceptor): no
|
|
48
48
|
* run exists, so return `undefined` and let the caller retry its own create.
|
|
49
49
|
*/
|
|
50
|
-
export declare function resolveCallerIdWinnerOrRetry(internals: EngineInternals, winnerId: string, signalSpec: StartOrSignalSignal, signalId: string, callbacks: StartOrSignalCallbacks): Promise<WorkflowHandle | undefined>;
|
|
50
|
+
export declare function resolveCallerIdWinnerOrRetry(internals: EngineInternals, winnerId: string, signalSpec: StartOrSignalSignal, signalId: string, callbacks: StartOrSignalCallbacks, allowTerminalRestart?: boolean): Promise<WorkflowHandle | undefined>;
|
|
51
51
|
/**
|
|
52
52
|
* For a workflow that already exists: signal it if non-terminal, conflict if
|
|
53
53
|
* terminal. Returns the handle on a successful signal, or `undefined` when the
|