@lostgradient/weft 0.2.1 → 0.3.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 +47 -22
- package/dist/cli/generated/operation-client.generated.d.ts +28 -1
- package/dist/cli/generated/operation-client.generated.js +2 -0
- package/dist/cli-main.js +79 -79
- package/dist/client/handle-delegation.d.ts +4 -0
- package/dist/client/handle-delegation.js +6 -0
- package/dist/client/http-client-requests.d.ts +2 -0
- package/dist/client/http-client-requests.js +3 -0
- package/dist/client/http-client.d.ts +4 -1
- package/dist/client/http-client.js +9 -1
- package/dist/client/interface.d.ts +57 -2
- package/dist/client/local.d.ts +4 -1
- package/dist/client/local.js +7 -0
- package/dist/client/start-body.d.ts +7 -1
- package/dist/client/start-body.js +13 -4
- package/dist/core/codec/extension-codec.js +4 -2
- package/dist/core/codec/index.d.ts +1 -0
- package/dist/core/codec/index.js +1 -0
- package/dist/core/codec/serializer-registry.d.ts +122 -0
- package/dist/core/codec/serializer-registry.js +51 -0
- package/dist/core/context/index.d.ts +9 -0
- package/dist/core/context/internals.d.ts +9 -0
- package/dist/core/context/internals.js +3 -0
- package/dist/core/context/run-operation.d.ts +16 -3
- package/dist/core/context/run-operation.js +16 -7
- package/dist/core/engine/bulk-operations.js +1 -1
- package/dist/core/engine/construction.d.ts +0 -1
- package/dist/core/engine/construction.js +10 -1
- package/dist/core/engine/disposal.js +12 -0
- package/dist/core/engine/engine-create-types.d.ts +0 -14
- package/dist/core/engine/engine-internal-types.d.ts +12 -0
- package/dist/core/engine/engine-leak-warnings.d.ts +6 -0
- package/dist/core/engine/engine-leak-warnings.js +4 -0
- package/dist/core/engine/engine-runtime-helpers.d.ts +17 -0
- package/dist/core/engine/engine-runtime-helpers.js +26 -5
- package/dist/core/engine/errors.d.ts +74 -0
- package/dist/core/engine/errors.js +25 -1
- package/dist/core/engine/handle-result.js +1 -1
- package/dist/core/engine/handles.d.ts +89 -40
- package/dist/core/engine/handles.js +25 -27
- package/dist/core/engine/index.d.ts +96 -4
- package/dist/core/engine/index.js +75 -4
- package/dist/core/engine/inline-launch-queue.d.ts +14 -0
- package/dist/core/engine/inline-launch-queue.js +32 -7
- package/dist/core/engine/internals.d.ts +18 -10
- package/dist/core/engine/lifecycle/fork-helpers.js +1 -7
- package/dist/core/engine/lifecycle/persist.js +5 -20
- package/dist/core/engine/lifecycle/resume.js +25 -4
- package/dist/core/engine/lifecycle/start-commit.d.ts +47 -0
- package/dist/core/engine/lifecycle/start-commit.js +27 -0
- package/dist/core/engine/lifecycle/start-exec.d.ts +30 -2
- package/dist/core/engine/lifecycle/start-exec.js +38 -0
- package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +79 -0
- package/dist/core/engine/lifecycle/start-or-signal-resolution.js +60 -0
- package/dist/core/engine/lifecycle/start-or-signal.d.ts +45 -0
- package/dist/core/engine/lifecycle/start-or-signal.js +141 -0
- package/dist/core/engine/lifecycle/start.d.ts +3 -3
- package/dist/core/engine/lifecycle/start.js +31 -37
- package/dist/core/engine/lifecycle.d.ts +3 -2
- package/dist/core/engine/lifecycle.js +9 -2
- package/dist/core/engine/listing.js +1 -1
- package/dist/core/engine/persisted-data-version.d.ts +5 -9
- package/dist/core/engine/persisted-data-version.js +4 -5
- package/dist/core/engine/schedule-handle.d.ts +45 -0
- package/dist/core/engine/schedule-handle.js +26 -0
- package/dist/core/engine/schedules.d.ts +1 -1
- package/dist/core/engine/schedules.js +7 -3
- package/dist/core/engine/second-instance-detector.d.ts +96 -0
- package/dist/core/engine/second-instance-detector.js +108 -0
- package/dist/core/engine/signals.d.ts +22 -0
- package/dist/core/engine/signals.js +15 -0
- package/dist/core/engine/termination/cleanup.d.ts +25 -0
- package/dist/core/engine/termination/cleanup.js +19 -1
- package/dist/core/engine/termination/complete.js +4 -3
- package/dist/core/engine/termination/suspend.d.ts +68 -0
- package/dist/core/engine/termination/suspend.js +41 -0
- package/dist/core/engine/termination.d.ts +4 -2
- package/dist/core/engine/termination.js +2 -0
- package/dist/core/engine/validation.js +25 -1
- package/dist/core/engine/workflow-feed.d.ts +5 -3
- package/dist/core/events/event-map.d.ts +2 -1
- package/dist/core/events/workflow-events.d.ts +23 -0
- package/dist/core/events/workflow-events.js +9 -0
- package/dist/core/list-filter-validation.js +2 -1
- package/dist/core/start-workflow-validation.d.ts +22 -0
- package/dist/core/start-workflow-validation.js +11 -1
- package/dist/core/step-context.d.ts +10 -6
- package/dist/core/step-context.js +7 -15
- package/dist/core/types/activity.d.ts +6 -3
- package/dist/core/types/identity.d.ts +8 -1
- package/dist/core/types/launch-metadata.d.ts +33 -0
- package/dist/core/types/launch-metadata.js +0 -0
- package/dist/core/types/message-handles.d.ts +25 -0
- package/dist/core/types/options.d.ts +48 -54
- package/dist/core/types/reviews.d.ts +2 -1
- package/dist/core/types/services-resolution.d.ts +47 -0
- package/dist/core/types/services-resolution.js +0 -0
- package/dist/core/types/state.d.ts +11 -11
- package/dist/core/types/workflow-builder.d.ts +5 -4
- package/dist/core/types/workflow-function.d.ts +17 -0
- package/dist/core/types/workflow-snapshot.d.ts +29 -0
- package/dist/core/types/workflow-snapshot.js +0 -0
- package/dist/core/types.d.ts +3 -0
- package/dist/core/types.js +3 -0
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +3 -1
- package/dist/diagnostics/doctor.js +6 -3
- package/dist/diagnostics/format.js +2 -2
- package/dist/diagnostics/types.d.ts +1 -0
- package/dist/diagnostics/version-check.js +6 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +10 -1
- package/dist/json-schema.js +1 -1
- package/dist/mcp/cli.js +35 -35
- package/dist/mcp/list-filter.js +2 -1
- package/dist/mcp/session.js +1 -0
- package/dist/observability/index.js +2 -2
- package/dist/server/handler.js +30 -30
- package/dist/server/index.js +33 -33
- package/dist/server/interactive-operations.js +1 -0
- package/dist/server/operations/resume-workflow.js +2 -2
- package/dist/server/operations/start-or-signal-workflow.d.ts +39 -0
- package/dist/server/operations/start-or-signal-workflow.js +140 -0
- package/dist/server/operations/start-workflow-options.d.ts +32 -0
- package/dist/server/operations/start-workflow-options.js +63 -0
- package/dist/server/operations/start-workflow.js +7 -69
- package/dist/server/operations/suspend-workflow.d.ts +13 -0
- package/dist/server/operations/suspend-workflow.js +36 -0
- package/dist/server/rest-binding.d.ts +18 -7
- package/dist/server/rest-bindings.js +12 -0
- package/dist/server/runtime/task-dispatch.js +5 -3
- package/dist/server/runtime/task-polling.d.ts +16 -2
- package/dist/server/runtime/task-polling.js +20 -5
- package/dist/server/runtime/websocket-worker.js +8 -0
- package/dist/server/serve-internals.d.ts +8 -0
- package/dist/server/serve-internals.js +4 -2
- package/dist/server/task-state.d.ts +8 -0
- package/dist/service-worker/index.js +28 -28
- package/dist/storage/capabilities.d.ts +10 -2
- package/dist/storage/capabilities.js +2 -2
- package/dist/storage/http.js +2 -2
- package/dist/storage/index.d.ts +6 -1
- package/dist/storage/indexeddb.js +1 -1
- package/dist/storage/interface.d.ts +26 -0
- 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/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon-value-mapping.d.ts +47 -0
- package/dist/storage/neon-value-mapping.js +11 -0
- package/dist/storage/neon.d.ts +108 -0
- package/dist/storage/neon.js +10 -0
- package/dist/storage/node-sqlite-loader.d.ts +71 -0
- package/dist/storage/node-sqlite-loader.js +41 -0
- package/dist/storage/node-sqlite.d.ts +1 -19
- package/dist/storage/node-sqlite.js +38 -32
- package/dist/storage/postgres-key-value-queries.d.ts +79 -0
- package/dist/storage/postgres-key-value-queries.js +63 -0
- package/dist/storage/resolve.d.ts +2 -165
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/storage-configuration.d.ts +209 -0
- package/dist/storage/storage-configuration.js +0 -0
- package/dist/storage/text-value-store.d.ts +9 -9
- package/dist/storage/turso.js +2 -2
- package/dist/storage/typed-storage.js +1 -1
- package/dist/storage/web-extension.js +1 -1
- package/dist/testing/index.js +33 -33
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/index.js +9 -5
- package/dist/worker/long-poll.js +4 -0
- package/dist/worker/protocol-messages.d.ts +20 -0
- package/dist/worker/protocol-schemas.d.ts +32 -0
- package/dist/worker/protocol-schemas.js +8 -4
- package/dist/worker/protocol-task-result.d.ts +28 -0
- package/dist/worker/protocol-task-result.js +76 -0
- package/dist/worker/protocol.d.ts +4 -15
- package/dist/worker/protocol.js +1 -1
- package/dist/worker/registry/fair-share.d.ts +29 -0
- package/dist/worker/registry/fair-share.js +30 -0
- package/dist/worker/registry/routing.d.ts +18 -0
- package/dist/worker/registry/routing.js +14 -0
- package/dist/worker/registry/types.d.ts +7 -0
- package/dist/worker/registry.d.ts +16 -1
- package/dist/worker/registry.js +24 -36
- package/package.json +17 -4
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { KEYS } from "../../../storage/interface.js";
|
|
2
2
|
import { createCheckpoint } from "../../checkpoint.js";
|
|
3
|
-
import { WorkflowStartedEvent } from "../../events.js";
|
|
4
3
|
import { assertPayloadWithinLimit } from "../../payload-size.js";
|
|
5
4
|
import { normalizeStorageTimestamp } from "../../scheduler.js";
|
|
6
5
|
import {
|
|
@@ -20,8 +19,12 @@ import {
|
|
|
20
19
|
normalizeStartWorkflowTags,
|
|
21
20
|
setWorkflowStartHeaders
|
|
22
21
|
} from "./shared.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
22
|
+
import { buildAndCommitStartBatch } from "./start-commit.js";
|
|
23
|
+
import {
|
|
24
|
+
assertDeferSupported,
|
|
25
|
+
beginExecutionAwaitingLiveness,
|
|
26
|
+
runWorkflowStartInterceptor
|
|
27
|
+
} from "./start-exec.js";
|
|
25
28
|
export async function start(internals, type, input, options, callbacks) {
|
|
26
29
|
return startWorkflow(internals, type, input, options, void 0, callbacks);
|
|
27
30
|
}
|
|
@@ -43,9 +46,6 @@ function prepareStartWorkflow(internals, options, callbacks) {
|
|
|
43
46
|
normalizedTags
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
|
-
async function persistStartBatch(internals, startOperations) {
|
|
47
|
-
await internals.storage.batch(startOperations);
|
|
48
|
-
}
|
|
49
49
|
function rollbackTransientStartState(internals, workflowId) {
|
|
50
50
|
forgetCommittedCheckpointBytes(internals, workflowId);
|
|
51
51
|
internals.checkpoints.delete(workflowId);
|
|
@@ -58,12 +58,13 @@ function assertServicesSupportedForMode(internals, options) {
|
|
|
58
58
|
if (options?.services !== void 0 && internals.inlineStrategy === null)
|
|
59
59
|
throw Error('options.services is only supported in inline execution mode; it cannot be serialized to a Worker. Remove services or use workflowExecutionMode: "inline".');
|
|
60
60
|
}
|
|
61
|
-
export async function startWorkflow(internals, type, input, options, additionalStartOperations, callbacks) {
|
|
61
|
+
export async function startWorkflow(internals, type, input, options, additionalStartOperations, callbacks, buildIdempotentStartOperations) {
|
|
62
62
|
const registration = internals.registrations.get(type);
|
|
63
63
|
if (!registration)
|
|
64
64
|
throw new WorkflowNotRegisteredError(type);
|
|
65
65
|
assertServicesSupportedForMode(internals, options);
|
|
66
66
|
const preparation = prepareStartWorkflow(internals, options, callbacks), { workflowId, callerProvidedId, parentHeaders, executionStateOwnerId, delayedStartTimer } = preparation;
|
|
67
|
+
assertDeferSupported(internals, options, Boolean(delayedStartTimer));
|
|
67
68
|
if (internals.pendingStarts.has(workflowId))
|
|
68
69
|
throw new WorkflowAlreadyExistsError(workflowId);
|
|
69
70
|
internals.pendingStarts.add(workflowId);
|
|
@@ -78,15 +79,32 @@ export async function startWorkflow(internals, type, input, options, additionalS
|
|
|
78
79
|
internals.checkpoints.set(workflowId, checkpoint);
|
|
79
80
|
setWorkflowStartHeaders(internals, workflowId, workflowStartHeaders, callbacks);
|
|
80
81
|
internals.workflowVersionTuples.set(workflowId, versionTuple);
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
await buildAndCommitStartBatch({
|
|
83
|
+
internals,
|
|
84
|
+
workflowId,
|
|
85
|
+
state,
|
|
86
|
+
checkpoint,
|
|
87
|
+
registration,
|
|
88
|
+
options,
|
|
89
|
+
delayedStartTimer,
|
|
90
|
+
persistedWorkflowStartHeaders,
|
|
91
|
+
additionalStartOperations,
|
|
92
|
+
callbacks
|
|
93
|
+
}, buildIdempotentStartOperations);
|
|
83
94
|
if (options?.services !== void 0) {
|
|
84
95
|
internals.workflowServices.set(workflowId, options.services);
|
|
85
96
|
internals.workflowsNeedingTerminalCleanup.add(workflowId);
|
|
86
97
|
}
|
|
87
98
|
const handle = createWorkflowHandle(internals, workflowId, callbacks);
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
await beginExecutionAwaitingLiveness(internals, {
|
|
100
|
+
type,
|
|
101
|
+
input,
|
|
102
|
+
checkpoint,
|
|
103
|
+
state,
|
|
104
|
+
registration,
|
|
105
|
+
options,
|
|
106
|
+
isDelayed: Boolean(delayedStartTimer)
|
|
107
|
+
}, workflowId, callbacks);
|
|
90
108
|
startSucceeded = !0;
|
|
91
109
|
return handle;
|
|
92
110
|
} finally {
|
|
@@ -112,42 +130,18 @@ export function resolveScheduledStartAt(internals, options, submissionTime, call
|
|
|
112
130
|
export function parseStartOptionDuration(_internals, duration, fieldName, _callbacks) {
|
|
113
131
|
return parseStartWorkflowDuration(duration, fieldName);
|
|
114
132
|
}
|
|
115
|
-
export function beginWorkflowExecution(internals, workflowId, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, _registration, callbacks) {
|
|
116
|
-
const nestingDepth = internals.pendingNestingDepth ?? 0;
|
|
117
|
-
internals.pendingNestingDepth = void 0;
|
|
118
|
-
if (internals.inlineStrategy !== null) {
|
|
119
|
-
callbacks.queueInlineWorkflowExecutionStart({
|
|
120
|
-
workflowId,
|
|
121
|
-
workflowType,
|
|
122
|
-
input,
|
|
123
|
-
checkpoint,
|
|
124
|
-
nestingDepth,
|
|
125
|
-
executionDeadline,
|
|
126
|
-
executionStateOwnerId
|
|
127
|
-
});
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
callbacks.dispatchEvent(new WorkflowStartedEvent(workflowId, workflowType, input));
|
|
131
|
-
startWorkflowExecution(internals, workflowId, workflowType, input, checkpoint, nestingDepth, executionDeadline, executionStateOwnerId, callbacks);
|
|
132
|
-
}
|
|
133
133
|
function buildInitialIdentitySlice(workflowId, type, input, versionTuple, executionStateOwnerId, delayedStartTimer, now, tags) {
|
|
134
134
|
return {
|
|
135
135
|
id: workflowId,
|
|
136
136
|
type,
|
|
137
137
|
status: delayedStartTimer ? "pending" : "running",
|
|
138
138
|
input,
|
|
139
|
-
|
|
139
|
+
versionTuple,
|
|
140
140
|
executionStateOwnerId,
|
|
141
141
|
createdAt: now,
|
|
142
142
|
...!delayedStartTimer && { startedAt: now },
|
|
143
143
|
updatedAt: now,
|
|
144
|
-
...tags !== void 0 && { tags }
|
|
145
|
-
...versionTuple.agentVersion !== void 0 && {
|
|
146
|
-
agentVersion: versionTuple.agentVersion
|
|
147
|
-
},
|
|
148
|
-
...versionTuple.toolVersions !== void 0 && {
|
|
149
|
-
toolVersions: versionTuple.toolVersions
|
|
150
|
-
}
|
|
144
|
+
...tags !== void 0 && { tags }
|
|
151
145
|
};
|
|
152
146
|
}
|
|
153
147
|
function resolveInitialExecutionDeadline(internals, options, delayedStartTimer, now, callbacks) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export { EMPTY_STORAGE_VALUE, createWorkflowHandle, loadTerminalCleanupTrackedState, loadWorkflowStartHeaders, normalizeStartWorkflowTags, processPendingUpdatesAfterReplay, setWorkflowStartHeaders, type LifecycleCallbacks, type RecoverAllOptions, } from './lifecycle/shared.ts';
|
|
2
2
|
export { createWorkflowVersionTuple, derivePreparedExecutionState, prepareResumeState, throwVersionMismatch, workflowStateWithVersionTuple, workflowVersionTupleFromState, } from './lifecycle/persist.ts';
|
|
3
|
-
export {
|
|
3
|
+
export { createInitialCheckpoint, createInitialWorkflowState, parseStartOptionDuration, resolveScheduledStartAt, start, startWorkflow, } from './lifecycle/start.ts';
|
|
4
|
+
export { startOrSignal, startWithIdempotency, type StartOrSignalCallbacks, } from './lifecycle/start-or-signal.ts';
|
|
4
5
|
export { buildInitialSearchAttributeOperations, buildStartBatchOperations, validateSearchAttributes, } from './lifecycle/start-batch.ts';
|
|
5
|
-
export { runWorkflowStartInterceptor, startWorkflowExecution } from './lifecycle/start-exec.ts';
|
|
6
|
+
export { beginWorkflowExecution, runWorkflowStartInterceptor, startWorkflowExecution, } from './lifecycle/start-exec.ts';
|
|
6
7
|
export { buildForkBatchOperations, buildForkSearchAttributes, createForkLineage, createForkedWorkflowState, } from './lifecycle/fork-helpers.ts';
|
|
7
8
|
export { fork, launchWorkflowFromCheckpoint, recoverAll, resume } from './lifecycle/transition.ts';
|
|
8
9
|
export { resumeWorkflowFromStorage } from './lifecycle/resume.ts';
|
|
@@ -16,7 +16,6 @@ export {
|
|
|
16
16
|
workflowVersionTupleFromState
|
|
17
17
|
} from "./lifecycle/persist.js";
|
|
18
18
|
export {
|
|
19
|
-
beginWorkflowExecution,
|
|
20
19
|
createInitialCheckpoint,
|
|
21
20
|
createInitialWorkflowState,
|
|
22
21
|
parseStartOptionDuration,
|
|
@@ -24,12 +23,20 @@ export {
|
|
|
24
23
|
start,
|
|
25
24
|
startWorkflow
|
|
26
25
|
} from "./lifecycle/start.js";
|
|
26
|
+
export {
|
|
27
|
+
startOrSignal,
|
|
28
|
+
startWithIdempotency
|
|
29
|
+
} from "./lifecycle/start-or-signal.js";
|
|
27
30
|
export {
|
|
28
31
|
buildInitialSearchAttributeOperations,
|
|
29
32
|
buildStartBatchOperations,
|
|
30
33
|
validateSearchAttributes
|
|
31
34
|
} from "./lifecycle/start-batch.js";
|
|
32
|
-
export {
|
|
35
|
+
export {
|
|
36
|
+
beginWorkflowExecution,
|
|
37
|
+
runWorkflowStartInterceptor,
|
|
38
|
+
startWorkflowExecution
|
|
39
|
+
} from "./lifecycle/start-exec.js";
|
|
33
40
|
export {
|
|
34
41
|
buildForkBatchOperations,
|
|
35
42
|
buildForkSearchAttributes,
|
|
@@ -88,7 +88,7 @@ function summaryFromState(state, attributeFailureCategory) {
|
|
|
88
88
|
type: state.type,
|
|
89
89
|
status: state.status,
|
|
90
90
|
...state.tags !== void 0 && { tags: state.tags },
|
|
91
|
-
version: state.
|
|
91
|
+
version: state.versionTuple.workflowVersion,
|
|
92
92
|
createdAt: state.createdAt,
|
|
93
93
|
updatedAt: state.updatedAt,
|
|
94
94
|
...state.executionDeadline !== void 0 && { executionDeadline: state.executionDeadline },
|
|
@@ -12,13 +12,9 @@ import type { Storage as WeftStorage } from '../../storage/interface.ts';
|
|
|
12
12
|
* 3. Sentinel is absent. Only stamp the storage when it carries no user
|
|
13
13
|
* workflow data. Stamping a database that already holds workflow records,
|
|
14
14
|
* schedules, checkpoints, or any other `wf:` / `op:` / `schedule:` / `ev:`
|
|
15
|
-
* prefixed key would silently classify
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* sentinel, fail with `PersistedDataIncompatibleError(null, …)` so the
|
|
20
|
-
* operator can choose explicitly whether to wipe and start fresh.
|
|
15
|
+
* prefixed key would silently classify unversioned data as schema-current
|
|
16
|
+
* and risk replaying incompatible records. When user data is already present
|
|
17
|
+
* without a sentinel, fail with `PersistedDataIncompatibleError(null, …)` so
|
|
18
|
+
* the operator can choose explicitly whether to wipe and start fresh.
|
|
21
19
|
*/
|
|
22
|
-
export declare function assertCompatiblePersistedDataVersion(storage: WeftStorage
|
|
23
|
-
allowLegacyData?: boolean;
|
|
24
|
-
}): Promise<void>;
|
|
20
|
+
export declare function assertCompatiblePersistedDataVersion(storage: WeftStorage): Promise<void>;
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
PersistedDataIncompatibleError
|
|
5
5
|
} from "../persisted-data-incompatible-error.js";
|
|
6
6
|
const SCHEMA_VERSION_PATTERN = /^(?:0|[1-9]\d*)$/, USER_DATA_PREFIXES = ["wf:", "op:", "schedule:", "ev:", "sig:", "upd:", "idx:"];
|
|
7
|
-
export async function assertCompatiblePersistedDataVersion(storage
|
|
7
|
+
export async function assertCompatiblePersistedDataVersion(storage) {
|
|
8
8
|
const raw = await storage.get(PERSISTED_DATA_SCHEMA_VERSION_KEY);
|
|
9
9
|
if (raw !== null) {
|
|
10
10
|
const text = new TextDecoder().decode(raw);
|
|
@@ -17,9 +17,8 @@ export async function assertCompatiblePersistedDataVersion(storage, options = {}
|
|
|
17
17
|
throw new PersistedDataIncompatibleError(parsed, CURRENT_PERSISTED_DATA_SCHEMA_VERSION);
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
for (const
|
|
22
|
-
|
|
23
|
-
throw new PersistedDataIncompatibleError(null, CURRENT_PERSISTED_DATA_SCHEMA_VERSION);
|
|
20
|
+
for (const prefix of USER_DATA_PREFIXES)
|
|
21
|
+
for await (const _entry of storage.scan(prefix, { limit: 1 }))
|
|
22
|
+
throw new PersistedDataIncompatibleError(null, CURRENT_PERSISTED_DATA_SCHEMA_VERSION);
|
|
24
23
|
await storage.put(PERSISTED_DATA_SCHEMA_VERSION_KEY, new TextEncoder().encode(String(CURRENT_PERSISTED_DATA_SCHEMA_VERSION)));
|
|
25
24
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ScheduleSpec, ScheduleSummary } from '../types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Narrow engine view a {@link ScheduleHandle} delegates to. The full
|
|
4
|
+
* {@link Engine} implements it; the handle only depends on these schedule
|
|
5
|
+
* lifecycle operations.
|
|
6
|
+
*/
|
|
7
|
+
export interface ScheduleHandleEngine {
|
|
8
|
+
pauseSchedule(scheduleId: string): Promise<void>;
|
|
9
|
+
resumeSchedule(scheduleId: string): Promise<void>;
|
|
10
|
+
cancelSchedule(scheduleId: string): Promise<void>;
|
|
11
|
+
updateSchedule(scheduleId: string, newSpec: string | ScheduleSpec): Promise<void>;
|
|
12
|
+
getSchedule(scheduleId: string): Promise<ScheduleSummary | null>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Handle to a recurring schedule created by {@link Engine.schedule}. Use
|
|
16
|
+
* `handle.pause()`, `handle.resume()`, `handle.cancel()`, or
|
|
17
|
+
* `handle.update(cronExpression)` to manage the schedule lifecycle.
|
|
18
|
+
* `handle.describe()` returns the current {@link ScheduleSummary}.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { workflow, Engine, ScheduleHandle } from '@lostgradient/weft';
|
|
23
|
+
*
|
|
24
|
+
* const engine = new Engine();
|
|
25
|
+
* engine.register(workflow({ name: 'daily-report' }).execute(async function* () { return 'ok'; }));
|
|
26
|
+
*
|
|
27
|
+
* const handle = await engine.schedule('daily-report', null, '0 9 * * *');
|
|
28
|
+
* const typedHandle: ScheduleHandle = handle;
|
|
29
|
+
* await handle.pause();
|
|
30
|
+
* const summary = await handle.describe();
|
|
31
|
+
* void typedHandle;
|
|
32
|
+
* console.log(summary.status); // 'paused'
|
|
33
|
+
* await handle.cancel();
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare class ScheduleHandle {
|
|
37
|
+
#private;
|
|
38
|
+
readonly id: string;
|
|
39
|
+
constructor(id: string, engine: ScheduleHandleEngine);
|
|
40
|
+
pause(): Promise<void>;
|
|
41
|
+
resume(): Promise<void>;
|
|
42
|
+
cancel(): Promise<void>;
|
|
43
|
+
update(newSpec: string | ScheduleSpec): Promise<void>;
|
|
44
|
+
describe(): Promise<ScheduleSummary>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class ScheduleHandle {
|
|
2
|
+
id;
|
|
3
|
+
#engine;
|
|
4
|
+
constructor(id, engine) {
|
|
5
|
+
this.id = id;
|
|
6
|
+
this.#engine = engine;
|
|
7
|
+
}
|
|
8
|
+
async pause() {
|
|
9
|
+
await this.#engine.pauseSchedule(this.id);
|
|
10
|
+
}
|
|
11
|
+
async resume() {
|
|
12
|
+
await this.#engine.resumeSchedule(this.id);
|
|
13
|
+
}
|
|
14
|
+
async cancel() {
|
|
15
|
+
await this.#engine.cancelSchedule(this.id);
|
|
16
|
+
}
|
|
17
|
+
async update(newSpec) {
|
|
18
|
+
await this.#engine.updateSchedule(this.id, newSpec);
|
|
19
|
+
}
|
|
20
|
+
async describe() {
|
|
21
|
+
const schedule = await this.#engine.getSchedule(this.id);
|
|
22
|
+
if (!schedule)
|
|
23
|
+
throw Error(`Schedule "${this.id}" not found`);
|
|
24
|
+
return schedule;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BatchOperation } from '../../storage/interface.ts';
|
|
2
2
|
import type { PaginatedResult, ScheduleFilter, ScheduleOptions, ScheduleSpec, ScheduleState, ScheduleSummary, WorkflowState } from '../types.ts';
|
|
3
|
-
import { ScheduleHandle } from './handles.ts';
|
|
4
3
|
import type { EngineInternals } from './internals.ts';
|
|
4
|
+
import { ScheduleHandle } from './schedule-handle.ts';
|
|
5
5
|
export { handleScheduleTimer } from './schedule-timer.ts';
|
|
6
6
|
export type RefreshedScheduleState = {
|
|
7
7
|
state: ScheduleState;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { KEYS } from "../../storage/interface.js";
|
|
2
2
|
import { decode, encode } from "../codec.js";
|
|
3
3
|
import { WorkflowNotRegisteredError } from "./errors.js";
|
|
4
|
-
import { ScheduleHandle } from "./
|
|
4
|
+
import { ScheduleHandle } from "./schedule-handle.js";
|
|
5
5
|
import { getNextScheduleOccurrence } from "./schedule-occurrence.js";
|
|
6
6
|
import {
|
|
7
7
|
clearScheduleCurrentWorkflow,
|
|
@@ -123,11 +123,15 @@ export async function updateSchedule(internals, scheduleId, newSpec) {
|
|
|
123
123
|
};
|
|
124
124
|
await writeScheduleState(internals, updatedState, { includeTimer: state.status === "active" });
|
|
125
125
|
}
|
|
126
|
+
function scheduledRunOccupiesSlot(currentWorkflowState) {
|
|
127
|
+
const status = currentWorkflowState?.status;
|
|
128
|
+
return status === "running" || status === "pending" || status === "suspended";
|
|
129
|
+
}
|
|
126
130
|
export async function refreshScheduledWorkflowState(internals, state, callbacks) {
|
|
127
131
|
if (!state.currentWorkflowId)
|
|
128
132
|
return { state, currentWorkflowState: null };
|
|
129
133
|
const currentWorkflowState = await callbacks.loadWorkflowState(state.currentWorkflowId);
|
|
130
|
-
if (currentWorkflowState
|
|
134
|
+
if (scheduledRunOccupiesSlot(currentWorkflowState))
|
|
131
135
|
return { state, currentWorkflowState };
|
|
132
136
|
await internals.storage.delete(KEYS.scheduleRun(state.currentWorkflowId));
|
|
133
137
|
return {
|
|
@@ -141,7 +145,7 @@ export async function startScheduledRun(_internals, state, callbacks) {
|
|
|
141
145
|
return workflowId;
|
|
142
146
|
}
|
|
143
147
|
function hasActiveScheduledWorkflow(currentWorkflowState) {
|
|
144
|
-
return currentWorkflowState
|
|
148
|
+
return scheduledRunOccupiesSlot(currentWorkflowState);
|
|
145
149
|
}
|
|
146
150
|
async function applyBlockedScheduleOccurrence(state, hasActiveWorkflow, callbacks) {
|
|
147
151
|
if (!hasActiveWorkflow)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort, warn-only detection of a SECOND engine instance writing to the
|
|
3
|
+
* same durable store — a smoke alarm for singleton-deployment misconfiguration
|
|
4
|
+
* (an autoscaler accidentally set above one replica, or overlapping rolling
|
|
5
|
+
* deploys), NOT a correctness mechanism.
|
|
6
|
+
*
|
|
7
|
+
* **This is liveness, not fencing.** Weft's supported model is one engine process
|
|
8
|
+
* per durable store (see the recovery-and-deploys guide); fenced ownership is a
|
|
9
|
+
* future `MultiEngine` capability that does not exist yet. This detector never
|
|
10
|
+
* blocks boot, gates recovery, refuses a write, or claims ownership. It only
|
|
11
|
+
* observes whether another instance's heartbeat is *advancing while this instance
|
|
12
|
+
* is also running* and emits a warning if so. It does not prevent duplicate
|
|
13
|
+
* execution — infrastructure-level enforcement (`replicas: 1` + a `Recreate`
|
|
14
|
+
* deploy strategy, or a single systemd unit) is the real control.
|
|
15
|
+
*
|
|
16
|
+
* **Why liveness, not a boot check.** A boot-time check cannot distinguish a
|
|
17
|
+
* rolling-deploy handoff from a genuine second instance: both leave a recent
|
|
18
|
+
* heartbeat record in the store. Only observing a *foreign* heartbeat advance
|
|
19
|
+
* across several of our own ticks separates a live peer (autoscaling=2 → both
|
|
20
|
+
* heartbeats advance forever → both warn) from a dead-but-recent predecessor (a
|
|
21
|
+
* clean `Recreate` deploy → old heartbeat never advances after handoff → quiet).
|
|
22
|
+
*
|
|
23
|
+
* **Advance is measured by sequence, not wall clock.** Each heartbeat carries a
|
|
24
|
+
* per-instance monotonic `sequence`; a peer counts as advancing only when its
|
|
25
|
+
* `sequence` grows between two of *our* ticks. A peer's sequence cannot increase
|
|
26
|
+
* unless it is alive and ticking in our own time frame, so detection never
|
|
27
|
+
* compares clocks across hosts — it stays correct even if a peer's clock is
|
|
28
|
+
* frozen, skewed, or stepped backward. `heartbeatAt` exists only for the boot
|
|
29
|
+
* staleness sweep that garbage-collects long-dead instances' keys.
|
|
30
|
+
*
|
|
31
|
+
* Each engine writes its own heartbeat under `liveness:<instanceId>` and scans
|
|
32
|
+
* the `liveness:` prefix to observe peers. Per-instance keys (not one shared,
|
|
33
|
+
* clobbered key) keep every heartbeat independently observable and the sequence
|
|
34
|
+
* monotonic per writer.
|
|
35
|
+
*
|
|
36
|
+
* @module core/engine/second-instance-detector
|
|
37
|
+
*/
|
|
38
|
+
import { type Storage } from '../../storage/interface.ts';
|
|
39
|
+
import type { EngineCleanupIntervalDisposalTracker } from './engine-leak-warnings.ts';
|
|
40
|
+
/** Options for {@link createSecondInstanceDetector}. */
|
|
41
|
+
export type SecondInstanceDetectorOptions = {
|
|
42
|
+
storage: Storage;
|
|
43
|
+
/** This engine's unique instance id. */
|
|
44
|
+
instanceId: string;
|
|
45
|
+
/** Wall-clock source (ms), injected so tests can advance time deterministically. */
|
|
46
|
+
getNow: () => number;
|
|
47
|
+
/**
|
|
48
|
+
* Heartbeat interval in ms. The staleness window is derived from this, so the
|
|
49
|
+
* interval also sets how long a deploy overlap must last before it warns.
|
|
50
|
+
*/
|
|
51
|
+
intervalMs: number;
|
|
52
|
+
/**
|
|
53
|
+
* Emit a warning. Defaults to `process.emitWarning(message, WARNING_NAME)`,
|
|
54
|
+
* so the emitted `Warning.name` is `WeftSecondInstanceWarning` and consumers
|
|
55
|
+
* can filter on `warning.name` rather than scraping the message. Injected for
|
|
56
|
+
* tests; the seam is message-only because the name is a fixed constant.
|
|
57
|
+
*/
|
|
58
|
+
warn?: (message: string) => void;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The `name` of the emitted warning. A stable, filterable identifier on the
|
|
62
|
+
* `Warning` object — consumers subscribe to the process `warning` event and
|
|
63
|
+
* match `warning.name === WARNING_NAME` (see the singleton-deployment guide).
|
|
64
|
+
*/
|
|
65
|
+
export declare const SECOND_INSTANCE_WARNING_NAME = "WeftSecondInstanceWarning";
|
|
66
|
+
/**
|
|
67
|
+
* A running detector. `tick()` runs one heartbeat round (exposed for tests and
|
|
68
|
+
* driven by an interval in production); `stop()` clears the interval and
|
|
69
|
+
* best-effort removes this instance's heartbeat so the next boot starts quiet.
|
|
70
|
+
*/
|
|
71
|
+
export type SecondInstanceDetector = {
|
|
72
|
+
/** Run one heartbeat round: observe peers, then write our own heartbeat. */
|
|
73
|
+
tick(): Promise<void>;
|
|
74
|
+
/** Stop the interval and best-effort delete this instance's heartbeat key. */
|
|
75
|
+
stop(): Promise<void>;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Create a best-effort second-instance detector. Does not start an interval
|
|
79
|
+
* itself — the engine owns timer lifecycle so disposal can clear it through the
|
|
80
|
+
* same path as its other intervals. Call {@link SecondInstanceDetector.tick} on
|
|
81
|
+
* an interval and {@link SecondInstanceDetector.stop} on dispose.
|
|
82
|
+
*/
|
|
83
|
+
export declare function createSecondInstanceDetector(options: SecondInstanceDetectorOptions): SecondInstanceDetector;
|
|
84
|
+
/**
|
|
85
|
+
* Build the `setInterval` callback that drives a detector tick. `resolveDetector`
|
|
86
|
+
* returns the live detector, or `null` when the engine has been garbage-collected
|
|
87
|
+
* or disposed. When it returns `null` the tick SELF-CLEARS its own interval (via
|
|
88
|
+
* `tracker.secondInstanceDetectionInterval`) and returns — mirroring
|
|
89
|
+
* {@link createCleanupIntervalTick}. This is the prompt cleanup path: a leaked
|
|
90
|
+
* engine's first post-GC tick clears the timer immediately, rather than relying
|
|
91
|
+
* on the `FinalizationRegistry` backstop, whose callbacks are not guaranteed to
|
|
92
|
+
* run promptly (or at all). Extracted so the skip/clear guard is directly
|
|
93
|
+
* testable without a timer. A tick failure is swallowed: the detector is a smoke
|
|
94
|
+
* alarm, never a correctness path, so it must not surface as an unhandled rejection.
|
|
95
|
+
*/
|
|
96
|
+
export declare function createSecondInstanceDetectionTick(resolveDetector: () => SecondInstanceDetector | null, tracker: EngineCleanupIntervalDisposalTracker): () => void;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { KEYS } from "../../storage/interface.js";
|
|
2
|
+
const textEncoder = new TextEncoder, textDecoder = new TextDecoder;
|
|
3
|
+
function encodeHeartbeat(heartbeat) {
|
|
4
|
+
return textEncoder.encode(JSON.stringify(heartbeat));
|
|
5
|
+
}
|
|
6
|
+
async function bestEffort(operation) {
|
|
7
|
+
try {
|
|
8
|
+
await operation();
|
|
9
|
+
} catch {}
|
|
10
|
+
}
|
|
11
|
+
function isUsableHeartbeat(candidate) {
|
|
12
|
+
const { instanceId, heartbeatAt, sequence } = candidate;
|
|
13
|
+
return typeof instanceId === "string" && instanceId.length > 0 && typeof heartbeatAt === "number" && Number.isFinite(heartbeatAt) && typeof sequence === "number" && Number.isInteger(sequence) && sequence >= 0;
|
|
14
|
+
}
|
|
15
|
+
function decodeHeartbeat(raw) {
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = JSON.parse(textDecoder.decode(raw));
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
23
|
+
return null;
|
|
24
|
+
const candidate = parsed;
|
|
25
|
+
return isUsableHeartbeat(candidate) ? candidate : null;
|
|
26
|
+
}
|
|
27
|
+
export const SECOND_INSTANCE_WARNING_NAME = "WeftSecondInstanceWarning";
|
|
28
|
+
const STALE_SWEEP_WINDOW_MULTIPLE = 10, ADVANCE_TICKS_BEFORE_WARN = 2;
|
|
29
|
+
export function createSecondInstanceDetector(options) {
|
|
30
|
+
const { storage, instanceId, getNow, intervalMs } = options, warn = options.warn ?? ((message) => process.emitWarning(message, SECOND_INSTANCE_WARNING_NAME)), stalenessWindowMs = intervalMs * (ADVANCE_TICKS_BEFORE_WARN + 1);
|
|
31
|
+
let sequence = 0, swept = !1, stopped = !1;
|
|
32
|
+
const observed = new Map, warnedInstanceIds = new Set;
|
|
33
|
+
async function readPeers() {
|
|
34
|
+
const peers = [];
|
|
35
|
+
for await (const [key, value] of storage.scan(KEYS.livenessPrefix())) {
|
|
36
|
+
const heartbeat = decodeHeartbeat(value);
|
|
37
|
+
if (heartbeat !== null && heartbeat.instanceId !== instanceId)
|
|
38
|
+
peers.push({ key, heartbeat });
|
|
39
|
+
}
|
|
40
|
+
return peers;
|
|
41
|
+
}
|
|
42
|
+
async function sweepStaleHeartbeats(peers, now) {
|
|
43
|
+
const staleBefore = now - stalenessWindowMs * STALE_SWEEP_WINDOW_MULTIPLE;
|
|
44
|
+
for (const peer of peers)
|
|
45
|
+
if (peer.heartbeat.heartbeatAt < staleBefore)
|
|
46
|
+
await bestEffort(() => storage.delete(peer.key));
|
|
47
|
+
}
|
|
48
|
+
function evaluatePeers(peers) {
|
|
49
|
+
const seenThisTick = new Set;
|
|
50
|
+
for (const { key, heartbeat } of peers) {
|
|
51
|
+
if (KEYS.liveness(heartbeat.instanceId) !== key)
|
|
52
|
+
continue;
|
|
53
|
+
seenThisTick.add(heartbeat.instanceId);
|
|
54
|
+
const previous = observed.get(heartbeat.instanceId), advances = previous !== void 0 && heartbeat.sequence > previous.sequence ? previous.advances + 1 : 0;
|
|
55
|
+
observed.set(heartbeat.instanceId, { sequence: heartbeat.sequence, advances });
|
|
56
|
+
if (advances >= ADVANCE_TICKS_BEFORE_WARN && !warnedInstanceIds.has(heartbeat.instanceId)) {
|
|
57
|
+
warnedInstanceIds.add(heartbeat.instanceId);
|
|
58
|
+
warn(`another engine instance (${heartbeat.instanceId}) is writing to this durable store. Weft supports one engine process per store; running two can cause duplicate workflow execution. Enforce a single instance at the infrastructure layer (for example, one replica with a Recreate deploy strategy).`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const knownInstanceId of Array.from(observed.keys()))
|
|
62
|
+
if (!seenThisTick.has(knownInstanceId))
|
|
63
|
+
observed.delete(knownInstanceId);
|
|
64
|
+
}
|
|
65
|
+
let tickInFlight = !1;
|
|
66
|
+
async function tick() {
|
|
67
|
+
if (stopped || tickInFlight)
|
|
68
|
+
return;
|
|
69
|
+
tickInFlight = !0;
|
|
70
|
+
let wroteHeartbeat = !1;
|
|
71
|
+
try {
|
|
72
|
+
const now = getNow(), peers = await readPeers();
|
|
73
|
+
if (!swept) {
|
|
74
|
+
swept = !0;
|
|
75
|
+
await sweepStaleHeartbeats(peers, now);
|
|
76
|
+
}
|
|
77
|
+
evaluatePeers(peers);
|
|
78
|
+
if (stopped)
|
|
79
|
+
return;
|
|
80
|
+
sequence += 1;
|
|
81
|
+
const heartbeat = { instanceId, heartbeatAt: now, sequence };
|
|
82
|
+
wroteHeartbeat = !0;
|
|
83
|
+
await bestEffort(() => storage.put(KEYS.liveness(instanceId), encodeHeartbeat(heartbeat)));
|
|
84
|
+
} finally {
|
|
85
|
+
tickInFlight = !1;
|
|
86
|
+
if (wroteHeartbeat && stopped)
|
|
87
|
+
await bestEffort(() => storage.delete(KEYS.liveness(instanceId)));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function stop() {
|
|
91
|
+
stopped = !0;
|
|
92
|
+
await bestEffort(() => storage.delete(KEYS.liveness(instanceId)));
|
|
93
|
+
}
|
|
94
|
+
return { tick, stop };
|
|
95
|
+
}
|
|
96
|
+
export function createSecondInstanceDetectionTick(resolveDetector, tracker) {
|
|
97
|
+
return function secondInstanceDetectionTick() {
|
|
98
|
+
const detector = resolveDetector();
|
|
99
|
+
if (detector === null) {
|
|
100
|
+
if (tracker.secondInstanceDetectionInterval !== null) {
|
|
101
|
+
clearInterval(tracker.secondInstanceDetectionInterval);
|
|
102
|
+
tracker.secondInstanceDetectionInterval = null;
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
detector.tick().catch(() => {});
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BatchOperation } from '../../storage/interface.ts';
|
|
1
2
|
import type { ComposedWorkflowInterceptor } from '../interceptor.ts';
|
|
2
3
|
import type { SignalDeliveryOptions, WorkflowState } from '../types.ts';
|
|
3
4
|
import type { EngineInternals } from './internals.ts';
|
|
@@ -31,6 +32,27 @@ export type ConsumedSignalResult = {
|
|
|
31
32
|
export declare function signal(internals: EngineInternals, workflowId: string, name: string, payload: unknown, callbacks: SignalCallbacks, options?: SignalDeliveryOptions): Promise<void>;
|
|
32
33
|
export declare function releaseSignalWaiter(internals: EngineInternals, workflowId: string, waiterKey: string, expectedResolve?: () => void): void;
|
|
33
34
|
export declare function bufferSignalPayloads(internals: EngineInternals, workflowId: string, deliveries: BufferedSignalDelivery[], callbacks: SignalCallbacks, defaultOptions?: SignalDeliveryOptions): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Build the durable operations for a single keyed signal so it can be folded
|
|
37
|
+
* into a workflow's create batch by {@link startOrSignal}. Writes the same pair
|
|
38
|
+
* the live signal path writes — the `sig:` payload (consumed on first drive by
|
|
39
|
+
* `processWaitSignalOperation`) and the `sigres:` accepted-response marker — so a
|
|
40
|
+
* concurrent caller that falls back to the standard signal path dedups against
|
|
41
|
+
* the SAME `signalId`. The accepted-response marker is consumption-independent:
|
|
42
|
+
* even after the winning run consumes the `sig:` payload, a late loser finds the
|
|
43
|
+
* `sigres:` key and short-circuits instead of re-delivering, which is what
|
|
44
|
+
* guarantees "one signal per signalId" across the create and signal paths.
|
|
45
|
+
*
|
|
46
|
+
* Returns both the put operations and the CAS condition (the `sig:` key must be
|
|
47
|
+
* absent) so the caller can gate the create batch on it.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildCreateBatchSignalOperations(internals: EngineInternals, workflowId: string, signalName: string, payload: unknown, signalId: string): {
|
|
50
|
+
operations: BatchOperation[];
|
|
51
|
+
condition: {
|
|
52
|
+
key: string;
|
|
53
|
+
expectedValue: null;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
34
56
|
export declare function hasBufferedSignal(internals: EngineInternals, workflowId: string, signalName: string): Promise<boolean>;
|
|
35
57
|
export declare function consumeSignal(internals: EngineInternals, workflowId: string, signalName: string): Promise<ConsumedSignalResult>;
|
|
36
58
|
/** Register a waiter key in a workflow-keyed reverse index. */
|
|
@@ -83,6 +83,21 @@ function createExplicitSignalOperations(internals, workflowId, deliveries, signa
|
|
|
83
83
|
value: encodePayloadWithinLimit(payload, internals.options.payloadSizePolicy.maxBytes, "signal payload")
|
|
84
84
|
}));
|
|
85
85
|
}
|
|
86
|
+
export function buildCreateBatchSignalOperations(internals, workflowId, signalName, payload, signalId) {
|
|
87
|
+
validateSignalId(signalId);
|
|
88
|
+
const signalKey = KEYS.signal(workflowId, signalName, signalId), acceptedResponseKey = KEYS.signalAcceptedResponse(workflowId, signalName, signalId);
|
|
89
|
+
return {
|
|
90
|
+
operations: [
|
|
91
|
+
{
|
|
92
|
+
type: "put",
|
|
93
|
+
key: signalKey,
|
|
94
|
+
value: encodePayloadWithinLimit(payload, internals.options.payloadSizePolicy.maxBytes, "signal payload")
|
|
95
|
+
},
|
|
96
|
+
{ type: "put", key: acceptedResponseKey, value: encode(SIGNAL_ACCEPTED_RESPONSE) }
|
|
97
|
+
],
|
|
98
|
+
condition: { key: signalKey, expectedValue: null }
|
|
99
|
+
};
|
|
100
|
+
}
|
|
86
101
|
function appendTerminalCleanupOperation(internals, workflowId, operations) {
|
|
87
102
|
if (internals.workflowsNeedingTerminalCleanup.has(workflowId))
|
|
88
103
|
return;
|
|
@@ -17,12 +17,37 @@ export type TerminationCallbacks = {
|
|
|
17
17
|
cleanupReviews: (workflowId: string) => Promise<void>;
|
|
18
18
|
};
|
|
19
19
|
export declare const TERMINAL_WORKFLOW_STATUSES: ReadonlySet<WorkflowStatus>;
|
|
20
|
+
/**
|
|
21
|
+
* Non-terminal statuses a workflow can be *forcibly* terminated from — cancel/
|
|
22
|
+
* timeout (`terminateWorkflow`) or system fail (`failWorkflow`). Both read this
|
|
23
|
+
* single constant so they cannot drift. `'suspended'` is included (a paused run
|
|
24
|
+
* must still be reachable by cancel/fail, else the CAS no-ops and the run is
|
|
25
|
+
* stranded); `completeWorkflow` deliberately excludes it (a suspended run's
|
|
26
|
+
* generator is evicted, so it can never reach normal completion).
|
|
27
|
+
*/
|
|
28
|
+
export declare const FORCIBLY_TERMINABLE_STATUSES: readonly ["running", "pending", "suspended"];
|
|
20
29
|
/**
|
|
21
30
|
* Remove any pending signal, update, and sleep waiters for a workflow. This
|
|
22
31
|
* prevents memory leaks and ensures that cancelled/completed/failed workflows
|
|
23
32
|
* cannot accept new signals, updates, or resolve orphaned sleep timers.
|
|
24
33
|
*/
|
|
25
34
|
export declare function cleanupWaiters(internals: EngineInternals, workflowId: string, callbacks: Pick<TerminationCallbacks, 'swallowPromiseRejection'>): void;
|
|
35
|
+
/**
|
|
36
|
+
* Evict only the in-flight OPERATION waiters for a workflow that is being
|
|
37
|
+
* suspended (signal/update/review waiters, review escalations, and sleep
|
|
38
|
+
* resolvers), WITHOUT resolving them and WITHOUT touching the non-serialized
|
|
39
|
+
* `services`, headers, type, or nesting-depth bookkeeping.
|
|
40
|
+
*
|
|
41
|
+
* Suspend parks the inline run (evicting its context/generator), so a signal or
|
|
42
|
+
* update arriving afterwards must NOT wake the dormant operation loop and drive
|
|
43
|
+
* the gone generator — it should buffer durably and be replayed when the
|
|
44
|
+
* workflow resumes. Deleting (not resolving) each waiter leaves the loop dormant
|
|
45
|
+
* and severs that wake path; the durable signal/sleep state in storage is
|
|
46
|
+
* untouched, so resume replays it. This is the suspend-specific complement to
|
|
47
|
+
* {@link cleanupWaiters}, which is for terminal transitions and additionally
|
|
48
|
+
* drops `services` and resolves sleep resolvers — both wrong for a pause.
|
|
49
|
+
*/
|
|
50
|
+
export declare function evictSuspendedWorkflowWaiters(internals: EngineInternals, workflowId: string, callbacks: Pick<TerminationCallbacks, 'swallowPromiseRejection'>): void;
|
|
26
51
|
/**
|
|
27
52
|
* Remove durable records keyed by `workflowId` that otherwise leak after a
|
|
28
53
|
* workflow reaches a terminal state.
|