@lostgradient/weft 0.9.0 → 0.11.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 +12 -4
- package/dist/alerting/alert-manager.d.ts +3 -1
- package/dist/alerting/alert-manager.js +5 -2
- package/dist/cli-main.js +63 -63
- package/dist/core/checkpoint/serialization.js +2 -2
- package/dist/core/context/index.d.ts +2 -1
- package/dist/core/context/index.js +8 -0
- package/dist/core/context/operation-request.d.ts +2 -0
- package/dist/core/context/parallel-cache-entry.d.ts +1 -1
- package/dist/core/context/parallel-cache-entry.js +12 -8
- package/dist/core/context/parallel-operations.d.ts +1 -1
- package/dist/core/context/parallel-operations.js +50 -8
- package/dist/core/engine/async-activity-completion.d.ts +24 -54
- package/dist/core/engine/async-activity-completion.js +25 -90
- package/dist/core/engine/async-activity-records.d.ts +106 -0
- package/dist/core/engine/async-activity-records.js +124 -0
- package/dist/core/engine/bulk-operations-purge.js +2 -1
- package/dist/core/engine/callback-creators-core.js +1 -0
- package/dist/core/engine/construction.d.ts +1 -3
- package/dist/core/engine/construction.js +2 -5
- package/dist/core/engine/deferred-consume-envelope.d.ts +10 -1
- package/dist/core/engine/deferred-consume-envelope.js +9 -1
- package/dist/core/engine/disposal.js +4 -0
- package/dist/core/engine/engine-internal-types.d.ts +1 -0
- package/dist/core/engine/engine-leak-warnings.d.ts +2 -0
- package/dist/core/engine/engine-leak-warnings.js +7 -1
- package/dist/core/engine/engine-runtime-helpers.d.ts +8 -0
- package/dist/core/engine/engine-runtime-helpers.js +9 -0
- package/dist/core/engine/errors.js +1 -1
- package/dist/core/engine/index.d.ts +18 -5
- package/dist/core/engine/index.js +41 -10
- package/dist/core/engine/inline-parking.js +14 -1
- package/dist/core/engine/internals.d.ts +23 -2
- package/dist/core/engine/lifecycle/recovered-services.d.ts +4 -2
- package/dist/core/engine/lifecycle/recovered-services.js +37 -7
- package/dist/core/engine/lifecycle/resume.d.ts +2 -2
- package/dist/core/engine/lifecycle/resume.js +39 -7
- package/dist/core/engine/lifecycle/shared.d.ts +34 -0
- package/dist/core/engine/lifecycle/transition.d.ts +1 -1
- package/dist/core/engine/lifecycle/transition.js +3 -3
- package/dist/core/engine/lifecycle.d.ts +1 -1
- package/dist/core/engine/memo-durable-activity.js +7 -9
- package/dist/core/engine/operations-activity.js +1 -1
- package/dist/core/engine/operations-coordination.d.ts +15 -1
- package/dist/core/engine/operations-coordination.js +43 -10
- package/dist/core/engine/operations-time.js +8 -28
- package/dist/core/engine/ownership-options.d.ts +1 -0
- package/dist/core/engine/ownership-options.js +14 -0
- package/dist/core/engine/retention.js +4 -0
- package/dist/core/engine/schedule-run-metadata.d.ts +3 -0
- package/dist/core/engine/schedule-run-metadata.js +29 -0
- package/dist/core/engine/schedule-run.js +19 -4
- package/dist/core/engine/schedules.js +4 -3
- package/dist/core/engine/sleep-timer-acknowledgements.d.ts +15 -0
- package/dist/core/engine/sleep-timer-acknowledgements.js +144 -0
- package/dist/core/engine/sub-operation.js +6 -13
- package/dist/core/engine/termination/cleanup.js +6 -1
- package/dist/core/types/options.d.ts +12 -0
- package/dist/core/types/services-resolution.d.ts +8 -3
- package/dist/core/types/workflow-builder.d.ts +2 -2
- package/dist/core/types/workflow-context.d.ts +27 -0
- package/dist/index.d.ts +2 -2
- package/dist/mcp/cli.js +17 -17
- package/dist/server/authorization.d.ts +3 -3
- package/dist/server/fault-to-json-rpc.d.ts +2 -1
- package/dist/server/handler.js +22 -22
- package/dist/server/index.js +17 -17
- package/dist/server/json-rpc-dispatch.d.ts +2 -2
- package/dist/server/json-rpc-parse.d.ts +1 -1
- package/dist/server/json-rpc-protocol.d.ts +2 -2
- package/dist/server/json-rpc-websocket.d.ts +1 -1
- package/dist/server/operation-fault.d.ts +3 -1
- package/dist/server/operations/async-activity.js +2 -2
- package/dist/server/principal.d.ts +2 -2
- package/dist/server/stdio-session.d.ts +4 -4
- package/dist/service-worker/index.js +13 -13
- package/dist/service-worker/scheduler.js +1 -0
- package/dist/storage/compressed-storage.js +1 -1
- package/dist/storage/http.js +2 -2
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/indexeddb.js +1 -1
- package/dist/storage/interface.d.ts +1 -0
- package/dist/storage/interface.js +1 -1
- package/dist/storage/lazy-postgres-pool.d.ts +38 -0
- package/dist/storage/lazy-postgres-pool.js +38 -0
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon-batch.d.ts +8 -8
- package/dist/storage/neon.d.ts +26 -79
- package/dist/storage/neon.js +3 -3
- package/dist/storage/postgres-key-value-queries.d.ts +1 -1
- package/dist/storage/postgres-key-value-queries.js +1 -1
- package/dist/storage/postgres-key-value-storage.d.ts +110 -0
- package/dist/storage/postgres-key-value-storage.js +204 -0
- package/dist/storage/postgres.d.ts +59 -0
- package/dist/storage/postgres.js +15 -0
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/testing.js +1 -1
- package/dist/storage/turso.js +1 -1
- package/dist/storage/typed-storage.js +1 -1
- package/dist/storage/web-extension.js +1 -1
- package/dist/testing/index.js +17 -17
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +17 -4
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { KEYS, encodeStorageKeyComponent } from "../../storage/interface.js";
|
|
2
|
+
import { decode, encode } from "../codec.js";
|
|
3
|
+
import { ActivityAsyncPendingEvent } from "../events.js";
|
|
4
|
+
import { commitFencedEngineWrite } from "./fenced-write.js";
|
|
5
|
+
const ASYNC_ACTIVITY_TOKEN_PREFIX = "async-act:v1";
|
|
6
|
+
export const ASYNC_ACTIVITY_KEY_PREFIX = "async-act:v1:";
|
|
7
|
+
export function asyncActivityWorkflowPrefix(workflowId) {
|
|
8
|
+
return `${ASYNC_ACTIVITY_KEY_PREFIX}${encodeStorageKeyComponent(workflowId)}:`;
|
|
9
|
+
}
|
|
10
|
+
function isPersistedAsyncActivity(value) {
|
|
11
|
+
if (typeof value !== "object" || value === null)
|
|
12
|
+
return !1;
|
|
13
|
+
const record = value;
|
|
14
|
+
return record.version === 1 && typeof record.token === "string" && typeof record.workflowId === "string" && typeof record.activityName === "string" && typeof record.operationId === "string" && typeof record.step === "number" && typeof record.attempt === "number" && typeof record.createdAt === "number";
|
|
15
|
+
}
|
|
16
|
+
function isPersistedOperationOutcome(value) {
|
|
17
|
+
if (typeof value !== "object" || value === null)
|
|
18
|
+
return !1;
|
|
19
|
+
const record = value;
|
|
20
|
+
if (record.status === "completed")
|
|
21
|
+
return "value" in record;
|
|
22
|
+
if (record.status === "failed")
|
|
23
|
+
return typeof record.error === "string";
|
|
24
|
+
return !1;
|
|
25
|
+
}
|
|
26
|
+
function isPersistedAsyncActivityResolution(value) {
|
|
27
|
+
if (typeof value !== "object" || value === null)
|
|
28
|
+
return !1;
|
|
29
|
+
const record = value;
|
|
30
|
+
return record.version === 1 && record.kind === "resolution" && typeof record.token === "string" && typeof record.workflowId === "string" && isPersistedOperationOutcome(record.outcome);
|
|
31
|
+
}
|
|
32
|
+
export function deriveAsyncActivityToken(workflowId, step, attempt) {
|
|
33
|
+
return `${ASYNC_ACTIVITY_TOKEN_PREFIX}:${workflowId}:${step}:${attempt}`;
|
|
34
|
+
}
|
|
35
|
+
function buildPersistPendingAsyncActivityOperation(pending) {
|
|
36
|
+
const record = {
|
|
37
|
+
version: 1,
|
|
38
|
+
token: pending.token,
|
|
39
|
+
workflowId: pending.workflowId,
|
|
40
|
+
activityName: pending.activityName,
|
|
41
|
+
operationId: pending.operationId,
|
|
42
|
+
step: pending.step,
|
|
43
|
+
attempt: pending.attempt,
|
|
44
|
+
createdAt: pending.createdAt
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
type: "put",
|
|
48
|
+
key: KEYS.asyncActivity(pending.workflowId, pending.token),
|
|
49
|
+
value: encode(record)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function buildAsyncActivityAcknowledgementOperations(pending, outcome) {
|
|
53
|
+
const record = {
|
|
54
|
+
version: 1,
|
|
55
|
+
kind: "resolution",
|
|
56
|
+
token: pending.token,
|
|
57
|
+
workflowId: pending.workflowId,
|
|
58
|
+
outcome
|
|
59
|
+
};
|
|
60
|
+
return [
|
|
61
|
+
{ type: "delete", key: KEYS.asyncActivity(pending.workflowId, pending.token) },
|
|
62
|
+
{
|
|
63
|
+
type: "put",
|
|
64
|
+
key: KEYS.asyncActivityResolution(pending.workflowId, pending.token),
|
|
65
|
+
value: encode(record)
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
export async function registerPendingAsyncActivity(internals, pending) {
|
|
70
|
+
const alreadyRegistered = internals.pendingAsyncActivities.has(pending.token);
|
|
71
|
+
internals.pendingAsyncActivities.set(pending.token, pending);
|
|
72
|
+
await commitFencedEngineWrite(internals, [buildPersistPendingAsyncActivityOperation(pending)], [], () => Error(`Async activity registration for token "${pending.token}" lost its precondition.`));
|
|
73
|
+
if (!alreadyRegistered)
|
|
74
|
+
internals.engine.dispatchEvent(new ActivityAsyncPendingEvent(pending.token, pending.operationId, pending.workflowId, pending.activityName, pending.attempt));
|
|
75
|
+
}
|
|
76
|
+
export async function recoverPendingAsyncActivities(internals) {
|
|
77
|
+
for await (const [, bytes] of internals.storage.scan(ASYNC_ACTIVITY_KEY_PREFIX)) {
|
|
78
|
+
const decoded = decode(bytes);
|
|
79
|
+
if (isPersistedAsyncActivity(decoded)) {
|
|
80
|
+
internals.pendingAsyncActivities.set(decoded.token, {
|
|
81
|
+
token: decoded.token,
|
|
82
|
+
workflowId: decoded.workflowId,
|
|
83
|
+
activityName: decoded.activityName,
|
|
84
|
+
operationId: decoded.operationId,
|
|
85
|
+
step: decoded.step,
|
|
86
|
+
attempt: decoded.attempt,
|
|
87
|
+
createdAt: decoded.createdAt
|
|
88
|
+
});
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (isPersistedAsyncActivityResolution(decoded))
|
|
92
|
+
queuePendingAsyncActivityResolution(internals, decoded.workflowId, {
|
|
93
|
+
token: decoded.token,
|
|
94
|
+
outcome: decoded.outcome,
|
|
95
|
+
timelineStatus: decoded.outcome.status,
|
|
96
|
+
timelineOutput: decoded.outcome.status === "completed" ? decoded.outcome.value : decoded.outcome.error
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function shouldBufferPendingAsyncActivityResolution(internals, workflowId) {
|
|
101
|
+
return internals.inlineStrategy !== null && !internals.inlineStrategy.hasGenerator(workflowId);
|
|
102
|
+
}
|
|
103
|
+
export function queuePendingAsyncActivityResolution(internals, workflowId, resolution) {
|
|
104
|
+
internals.pendingAsyncActivityResolutions ??= new Map;
|
|
105
|
+
const queued = internals.pendingAsyncActivityResolutions.get(workflowId) ?? [];
|
|
106
|
+
queued.push(resolution);
|
|
107
|
+
internals.pendingAsyncActivityResolutions.set(workflowId, queued);
|
|
108
|
+
}
|
|
109
|
+
export function takePendingAsyncActivityResolution(internals, workflowId, token) {
|
|
110
|
+
internals.pendingAsyncActivityResolutions ??= new Map;
|
|
111
|
+
const queued = internals.pendingAsyncActivityResolutions.get(workflowId);
|
|
112
|
+
if (queued === void 0)
|
|
113
|
+
return;
|
|
114
|
+
const index = queued.findIndex((resolution) => resolution.token === token);
|
|
115
|
+
if (index === -1)
|
|
116
|
+
return;
|
|
117
|
+
const resolution = queued[index];
|
|
118
|
+
if (resolution === void 0)
|
|
119
|
+
return;
|
|
120
|
+
queued.splice(index, 1);
|
|
121
|
+
if (queued.length === 0)
|
|
122
|
+
internals.pendingAsyncActivityResolutions.delete(workflowId);
|
|
123
|
+
return resolution;
|
|
124
|
+
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { decode } from "../codec.js";
|
|
9
9
|
import { buildIndexOperations } from "../search-attributes.js";
|
|
10
10
|
import { buildWorkflowTagIndexOperations, normalizeWorkflowTags } from "../workflow-tags.js";
|
|
11
|
-
import { asyncActivityWorkflowPrefix } from "./async-activity-
|
|
11
|
+
import { asyncActivityWorkflowPrefix } from "./async-activity-records.js";
|
|
12
12
|
import { forgetCommittedCheckpointBytes } from "./checkpoint-commit-snapshots.js";
|
|
13
13
|
import { commitFencedEngineWrite } from "./fenced-write.js";
|
|
14
14
|
import { streamWorkflowStates } from "./listing.js";
|
|
@@ -192,6 +192,7 @@ function buildBaseWorkflowDeleteKeys(state) {
|
|
|
192
192
|
KEYS.workflowHeaders(state.id),
|
|
193
193
|
KEYS.terminalCleanupNeeded(state.id),
|
|
194
194
|
KEYS.workflowConcurrencyHolder(state.id),
|
|
195
|
+
KEYS.scheduleRun(state.id),
|
|
195
196
|
KEYS.workflowHasServices(state.id),
|
|
196
197
|
KEYS.finalizerState(state.id),
|
|
197
198
|
KEYS.teardownOwed(state.id),
|
|
@@ -76,6 +76,7 @@ export function createLifecycleCallbacks(engine) {
|
|
|
76
76
|
swallowPromiseRejection: (promise) => swallowPromiseRejection(promise),
|
|
77
77
|
enforceHistoryCircuitBreaker: (workflowId) => terminateWorkflow(getInternals(engine), workflowId, "timed-out", createTerminationCallbacks(engine), HISTORY_CIRCUIT_BREAKER_REASON),
|
|
78
78
|
failWorkflowForUnavailableServices: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
|
|
79
|
+
failWorkflowForRecoveryHook: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
|
|
79
80
|
failWorkflowForCheckpointDecodeError: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system")
|
|
80
81
|
};
|
|
81
82
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { AlertManager } from '../../alerting/alert-manager.ts';
|
|
2
1
|
import type { Storage as WeftStorage } from '../../storage/interface.ts';
|
|
3
2
|
import { ActivityWorkerDispatcher } from '../../workers/activity-worker-dispatcher.ts';
|
|
4
3
|
import type { ComposedWorkflowInterceptor, Interceptor } from '../interceptor.ts';
|
|
5
|
-
import { type AnyActivityDefinition, type AnyWorkflowDefinition, type
|
|
4
|
+
import { type AnyActivityDefinition, type AnyWorkflowDefinition, type RegisteredWorkflowDefinition } from '../types.ts';
|
|
6
5
|
import type { WorkflowLogRecord } from '../types/workflow-log.ts';
|
|
7
6
|
import type { EngineConstructorOptions, ExecutionStrategyBundle, RegistrationEntry, ResolvedOptions } from './engine-internal-types.ts';
|
|
8
7
|
export type KnownWorkflowNames<TWorkflows extends object> = Extract<keyof TWorkflows, string>;
|
|
@@ -60,5 +59,4 @@ export declare function createExecutionStrategyBundle(parameters: {
|
|
|
60
59
|
getLogSink?: () => ((record: WorkflowLogRecord) => void) | undefined;
|
|
61
60
|
}): ExecutionStrategyBundle;
|
|
62
61
|
export declare function createActivityWorkerDispatcher(activityExecution: EngineConstructorOptions['activityExecution']): ActivityWorkerDispatcher | null;
|
|
63
|
-
export declare function createAlertManagerForEngine(engine: EventTarget, alerts: EngineOptions['alerts'] | undefined, getNow: () => number): AlertManager | null;
|
|
64
62
|
export {};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { AlertManager } from "../../alerting/alert-manager.js";
|
|
2
1
|
import { CompressedStorage } from "../../storage/compressed-storage.js";
|
|
3
2
|
import { MemoryStorage } from "../../storage/memory.js";
|
|
4
3
|
import { ActivityWorkerDispatcher } from "../../workers/activity-worker-dispatcher.js";
|
|
@@ -16,7 +15,7 @@ import {
|
|
|
16
15
|
DEFAULT_WORKER_TURN_TIMEOUT_MS,
|
|
17
16
|
MIN_WORKER_PROTOCOL_MESSAGE_BYTES
|
|
18
17
|
} from "../worker-protocol.js";
|
|
19
|
-
import { resolveOwnershipFields } from "./ownership-options.js";
|
|
18
|
+
import { resolveBackgroundTaskMode, resolveOwnershipFields } from "./ownership-options.js";
|
|
20
19
|
import {
|
|
21
20
|
normalizeHistoryPolicy,
|
|
22
21
|
normalizePayloadSizePolicy,
|
|
@@ -122,6 +121,7 @@ export function resolveEngineOptions(storage, options, getNow) {
|
|
|
122
121
|
getNow,
|
|
123
122
|
resolveWorkflowServices: options?.resolveWorkflowServices ?? null,
|
|
124
123
|
onLog: options?.onLog ?? null,
|
|
124
|
+
backgroundTaskMode: resolveBackgroundTaskMode(options),
|
|
125
125
|
...resolveBooleanDefaults(options),
|
|
126
126
|
...resolveNumericDefaults(options),
|
|
127
127
|
...resolveRetentionFields(options),
|
|
@@ -235,6 +235,3 @@ export function createActivityWorkerDispatcher(activityExecution) {
|
|
|
235
235
|
smol: activityExecution.smol ?? !1
|
|
236
236
|
}));
|
|
237
237
|
}
|
|
238
|
-
export function createAlertManagerForEngine(engine, alerts, getNow) {
|
|
239
|
-
return alerts ? new AlertManager(engine, alerts, getNow) : null;
|
|
240
|
-
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* nested coordinator surfaces up to its parent.
|
|
26
26
|
*/
|
|
27
27
|
declare const DEFERRED_CONSUME_BRAND: unique symbol;
|
|
28
|
+
declare const KEYED_RACE_RESULT_BRAND: unique symbol;
|
|
28
29
|
/**
|
|
29
30
|
* A branch result whose real value is produced by a single deferred consume that
|
|
30
31
|
* only the winning coordinator performs.
|
|
@@ -34,6 +35,12 @@ export type DeferredConsumeEnvelope = {
|
|
|
34
35
|
/** Perform the single destructive consume and return the consumed payload. */
|
|
35
36
|
readonly finalize: () => Promise<unknown>;
|
|
36
37
|
};
|
|
38
|
+
type KeyedRaceResultEnvelope = {
|
|
39
|
+
readonly [KEYED_RACE_RESULT_BRAND]: true;
|
|
40
|
+
readonly key: string;
|
|
41
|
+
readonly value: unknown;
|
|
42
|
+
};
|
|
43
|
+
export declare function createKeyedRaceResultEnvelope(key: string, value: unknown): KeyedRaceResultEnvelope;
|
|
37
44
|
/** Wrap a deferred consume into a branded envelope. */
|
|
38
45
|
export declare function createDeferredConsumeEnvelope(finalize: () => Promise<unknown>): DeferredConsumeEnvelope;
|
|
39
46
|
/** Detect an envelope by its brand symbol only (never structurally). */
|
|
@@ -43,7 +50,9 @@ export declare function isDeferredConsumeEnvelope(value: unknown): value is Defe
|
|
|
43
50
|
* cache. A winning `wait-signal` branch surfaces a {@link DeferredConsumeEnvelope}
|
|
44
51
|
* whose `finalize()` performs the single consume; a nested `ctx.all` branch
|
|
45
52
|
* surfaces an ARRAY that may hold envelopes at arbitrary positions, so arrays are
|
|
46
|
-
* walked and each element finalized.
|
|
53
|
+
* walked and each element finalized. A nested `ctx.raceKeyed()` winner is walked
|
|
54
|
+
* through its value while retaining its key. Any other value passes through
|
|
55
|
+
* untouched.
|
|
47
56
|
*
|
|
48
57
|
* This runs only on the WINNING path (race winner, or every branch of a settled
|
|
49
58
|
* `ctx.all`), so finalizing here is exactly the linearization point of "this
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
const DEFERRED_CONSUME_BRAND = Symbol("weft.deferredConsume");
|
|
1
|
+
const DEFERRED_CONSUME_BRAND = Symbol("weft.deferredConsume"), KEYED_RACE_RESULT_BRAND = Symbol("weft.keyedRaceResult");
|
|
2
|
+
export function createKeyedRaceResultEnvelope(key, value) {
|
|
3
|
+
return { [KEYED_RACE_RESULT_BRAND]: !0, key, value };
|
|
4
|
+
}
|
|
5
|
+
function isKeyedRaceResultEnvelope(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && value[KEYED_RACE_RESULT_BRAND] === !0;
|
|
7
|
+
}
|
|
2
8
|
export function createDeferredConsumeEnvelope(finalize) {
|
|
3
9
|
return { [DEFERRED_CONSUME_BRAND]: !0, finalize };
|
|
4
10
|
}
|
|
@@ -10,5 +16,7 @@ export async function finalizeAndUnwrap(value) {
|
|
|
10
16
|
return value.finalize();
|
|
11
17
|
if (Array.isArray(value))
|
|
12
18
|
return Promise.all(value.map((element) => finalizeAndUnwrap(element)));
|
|
19
|
+
if (isKeyedRaceResultEnvelope(value))
|
|
20
|
+
return { key: value.key, value: await finalizeAndUnwrap(value.value) };
|
|
13
21
|
return value;
|
|
14
22
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { disposeEngineCleanupInterval } from "./engine-runtime-helpers.js";
|
|
2
2
|
import { EngineDisposedError } from "./errors.js";
|
|
3
3
|
import { disposeQueuedInlineWorkflowStarts } from "./inline-launch-queue.js";
|
|
4
|
+
import { rejectAllSleepTimerAcknowledgements } from "./sleep-timer-acknowledgements.js";
|
|
4
5
|
export function disposeEngine(internals) {
|
|
5
6
|
internals.disposed = !0;
|
|
6
7
|
internals.alertManager?.[Symbol.dispose]();
|
|
@@ -30,6 +31,7 @@ export function disposeEngine(internals) {
|
|
|
30
31
|
internals.handleCache.clear();
|
|
31
32
|
for (const waiter of internals.resultResolvers.values())
|
|
32
33
|
waiter.reject(new EngineDisposedError);
|
|
34
|
+
rejectAllSleepTimerAcknowledgements(internals, new EngineDisposedError);
|
|
33
35
|
internals.resultResolvers.clear();
|
|
34
36
|
internals.updateWaiters.clear();
|
|
35
37
|
internals.updateWaitersByWorkflow.clear();
|
|
@@ -46,6 +48,8 @@ export function disposeEngine(internals) {
|
|
|
46
48
|
internals.pendingWebhooks.clear();
|
|
47
49
|
internals.sleepResolvers.clear();
|
|
48
50
|
internals.sleepResolversByWorkflow.clear();
|
|
51
|
+
internals.sleepTimerAcknowledgementWaiters.clear();
|
|
52
|
+
internals.durableInlineOperations.clear();
|
|
49
53
|
internals.sleepTimersFiredWithoutResolver.clear();
|
|
50
54
|
internals.checkpoints.clear();
|
|
51
55
|
internals.pendingExecutionStateOwnerId = void 0;
|
|
@@ -24,6 +24,7 @@ export interface RegistrationEntry {
|
|
|
24
24
|
export interface ResolvedOptions {
|
|
25
25
|
storage: WeftStorage;
|
|
26
26
|
development: boolean;
|
|
27
|
+
backgroundTaskMode: 'automatic' | 'manual';
|
|
27
28
|
checkpointHistory: number;
|
|
28
29
|
checkpointSizeWarningThreshold: number;
|
|
29
30
|
maxNestingDepth: number;
|
|
@@ -15,6 +15,8 @@ export type EngineCleanupIntervalDisposalTracker = {
|
|
|
15
15
|
testToken: symbol | undefined;
|
|
16
16
|
};
|
|
17
17
|
export declare const engineCleanupIntervalFinalizer: FinalizationRegistry<EngineCleanupIntervalDisposalTracker>;
|
|
18
|
+
/** Test-only hook for the finalizer callback's synchronous warning gate. */
|
|
19
|
+
export declare function finalizeEngineCleanupIntervalTrackerForTesting(tracker: EngineCleanupIntervalDisposalTracker): void;
|
|
18
20
|
export declare function shouldEmitEngineLeakWarning(): boolean;
|
|
19
21
|
/** Test-only override for the engine leak-warning environment gate. */
|
|
20
22
|
export declare function setEngineLeakWarningOverrideForTesting(value: boolean | undefined): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
let engineLeakWarningOverrideForTesting, engineLeakCollectionCountForTesting = 0, nextEngineLeakWarningTokenForTesting;
|
|
2
2
|
const engineLeakWarningTokensForTesting = new Set;
|
|
3
|
-
|
|
3
|
+
function finalizeEngineCleanupIntervalTracker(tracker) {
|
|
4
4
|
engineLeakCollectionCountForTesting++;
|
|
5
5
|
if (tracker.cleanupInterval !== null) {
|
|
6
6
|
clearInterval(tracker.cleanupInterval);
|
|
@@ -15,7 +15,13 @@ export const engineCleanupIntervalFinalizer = new FinalizationRegistry((tracker)
|
|
|
15
15
|
engineLeakWarningTokensForTesting.add(tracker.testToken);
|
|
16
16
|
process.emitWarning("WeftEngineLeakWarning: A Weft Engine was garbage-collected without calling [Symbol.dispose](). Use `using`, `await using`, or call engine[Symbol.dispose]() to clear background timers and release runtime resources.");
|
|
17
17
|
}
|
|
18
|
+
}
|
|
19
|
+
export const engineCleanupIntervalFinalizer = new FinalizationRegistry((tracker) => {
|
|
20
|
+
finalizeEngineCleanupIntervalTracker(tracker);
|
|
18
21
|
});
|
|
22
|
+
export function finalizeEngineCleanupIntervalTrackerForTesting(tracker) {
|
|
23
|
+
finalizeEngineCleanupIntervalTracker(tracker);
|
|
24
|
+
}
|
|
19
25
|
export function shouldEmitEngineLeakWarning() {
|
|
20
26
|
if (engineLeakWarningOverrideForTesting !== void 0)
|
|
21
27
|
return engineLeakWarningOverrideForTesting;
|
|
@@ -4,6 +4,13 @@ import type { Engine } from './index.ts';
|
|
|
4
4
|
import { type EngineInternals } from './internals.ts';
|
|
5
5
|
import type { SecondInstanceDetector } from './second-instance-detector.ts';
|
|
6
6
|
export declare function isActivityDefinition(value: unknown): value is AnyActivityDefinition;
|
|
7
|
+
type EngineCreateBackgroundTaskOptions = {
|
|
8
|
+
backgroundTasks?: 'automatic' | 'manual' | undefined;
|
|
9
|
+
recover?: boolean | undefined;
|
|
10
|
+
startScheduler?: boolean | undefined;
|
|
11
|
+
};
|
|
12
|
+
export declare function validateEngineCreateBackgroundTaskOptions(options: EngineCreateBackgroundTaskOptions): void;
|
|
13
|
+
export declare function shouldStartEngineScheduler(options: EngineCreateBackgroundTaskOptions, backgroundTaskMode: 'automatic' | 'manual'): boolean;
|
|
7
14
|
export declare function createQueuedInlineWorkflowStartHandler<TWorkflows extends object, TActivities extends object>(weakEngine: WeakRef<Engine<TWorkflows, TActivities>>, channel: MessageChannel): () => void;
|
|
8
15
|
/**
|
|
9
16
|
* Drain pending inline launches for `engine` before teardown. Built with the
|
|
@@ -23,3 +30,4 @@ export declare function createCleanupIntervalTick<TWorkflows extends object, TAc
|
|
|
23
30
|
*/
|
|
24
31
|
export declare function createSecondInstanceDetectorResolver<TWorkflows extends object, TActivities extends object>(weakEngine: WeakRef<Engine<TWorkflows, TActivities>>): () => SecondInstanceDetector | null;
|
|
25
32
|
export declare function disposeEngineCleanupInterval(internals: EngineInternals): void;
|
|
33
|
+
export {};
|
|
@@ -12,6 +12,15 @@ import { swallowPromiseRejection } from "./strategy-helpers.js";
|
|
|
12
12
|
export function isActivityDefinition(value) {
|
|
13
13
|
return typeof value === "function" && typeof value.name === "string" && "execute" in value && typeof value.execute === "function";
|
|
14
14
|
}
|
|
15
|
+
export function validateEngineCreateBackgroundTaskOptions(options) {
|
|
16
|
+
if (options.backgroundTasks === "manual" && options.startScheduler === !0)
|
|
17
|
+
throw Error('startScheduler cannot be true when backgroundTasks is "manual"');
|
|
18
|
+
}
|
|
19
|
+
export function shouldStartEngineScheduler(options, backgroundTaskMode) {
|
|
20
|
+
if (backgroundTaskMode === "manual")
|
|
21
|
+
return !1;
|
|
22
|
+
return options.startScheduler ?? options.recover !== !1;
|
|
23
|
+
}
|
|
15
24
|
function inlineLaunchQueueCallbacksForEngine(engine) {
|
|
16
25
|
const lifecycleCallbacks = createLifecycleCallbacks(engine);
|
|
17
26
|
return {
|
|
@@ -43,7 +43,7 @@ export class WorkflowTypeNotRegisteredForRecoveryError extends WeftError {
|
|
|
43
43
|
const missingWorkflowCount = parameters.missingWorkflows.length, missingTypes = [
|
|
44
44
|
...new Set(parameters.missingWorkflows.map((workflow) => workflow.type))
|
|
45
45
|
].toSorted(), registeredTypes = [...parameters.registeredTypes].toSorted(), summarizedTypes = summarizeMissingWorkflowTypes(missingTypes);
|
|
46
|
-
super("WorkflowTypeNotRegisteredForRecoveryError", `Cannot recover ${missingWorkflowCount} running workflow(s): workflow type(s) not registered: ${summarizedTypes}. Register the missing workflow types before calling \`recoverAll()\`, or pass ` + "`{ acknowledgeUnknownWorkflowTypes: true }` (dangerous \u2014 see
|
|
46
|
+
super("WorkflowTypeNotRegisteredForRecoveryError", `Cannot recover ${missingWorkflowCount} running workflow(s): workflow type(s) not registered: ${summarizedTypes}. Register the missing workflow types before calling \`recoverAll()\`, or pass ` + "`{ acknowledgeUnknownWorkflowTypes: true }` (dangerous \u2014 see " + "https://github.com/stevekinney/weft/blob/main/documentation/guides/recovery-and-deploys.md#acknowledging-drift-acknowledgeunknownworkflowtypes).");
|
|
47
47
|
this.registeredTypes = registeredTypes;
|
|
48
48
|
this.missingTypes = missingTypes;
|
|
49
49
|
this.missingWorkflowSamples = parameters.missingWorkflows.slice(0, MISSING_WORKFLOW_SAMPLE_LIMIT).map((workflow) => ({ ...workflow }));
|
|
@@ -21,12 +21,12 @@ import { ScheduleHandle } from './schedule-handle.ts';
|
|
|
21
21
|
import { type WorkflowFeedListener, type WorkflowFeedRecord, type WorkflowFeedSelector } from './workflow-feed.ts';
|
|
22
22
|
export { ActivityReconciliationCapabilityError, ActivityReconciliationConflictError, ActivityReconciliationIndeterminateError, } from './activity-reconciliation.ts';
|
|
23
23
|
export { AsyncActivityTokenNotFoundError } from './async-activity-completion.ts';
|
|
24
|
-
export type { PendingAsyncActivity } from './async-activity-
|
|
24
|
+
export type { PendingAsyncActivity } from './async-activity-records.ts';
|
|
25
25
|
export type { PendingTimelineEntry, RegistrationEntry, ResolvedOptions, TrackedWaiterKeys, WorkflowResultWaiter, } from './engine-internal-types.ts';
|
|
26
26
|
export { ActivityResolutionError, BulkDeleteRequiresTerminalWorkflowsError, BulkOperationConfirmationError, EngineCreateNameMismatchError, EngineDisposedError, IdempotencyKeyPurgedError, PersistedDataIncompatibleError, StartOrSignalConflictError, WorkflowAlreadyExistsError, WorkflowConcurrencyLimitExceededError, WorkflowNotFoundError, WorkflowNotRegisteredError, WorkflowSuspendNotSupportedError, WorkflowTeardownPendingError, WorkflowTypeNotRegisteredForRecoveryError, } from './errors.ts';
|
|
27
27
|
export { HANDLE_RESULT_PROMISE, WorkflowHandle } from './handles.ts';
|
|
28
28
|
export { EngineLeaseAcquisitionTimeoutError, EngineLeaseCorruptedError, EngineLeaseNotHeldError, } from './lease-errors.ts';
|
|
29
|
-
export type { RecoverAllOptions } from './lifecycle.ts';
|
|
29
|
+
export type { RecoverAllOptions, RecoveredWorkflowInfo } from './lifecycle.ts';
|
|
30
30
|
export { ScheduleHandle } from './schedule-handle.ts';
|
|
31
31
|
export type { WorkflowFeedListener, WorkflowFeedRecord, WorkflowFeedSelector, } from './workflow-feed.ts';
|
|
32
32
|
export type { EngineCreateOptions } from './engine-create-types.ts';
|
|
@@ -220,9 +220,12 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
|
|
|
220
220
|
* (signal-with-start). With an absent target, the workflow record and the
|
|
221
221
|
* first signal commit in one batch and the freshly-launched run consumes the
|
|
222
222
|
* signal on its first drive. A non-terminal target (running, pending, or
|
|
223
|
-
* suspended) is signalled through
|
|
224
|
-
*
|
|
225
|
-
*
|
|
223
|
+
* suspended) is signalled through a workflow-serialized signal path. If
|
|
224
|
+
* terminal completion wins that handoff, the target is treated as terminal:
|
|
225
|
+
* the default path throws {@link StartOrSignalConflictError}, while
|
|
226
|
+
* `options.onTerminalConflict: 'start-new'` transfers the signal to the
|
|
227
|
+
* replacement run. A terminal target throws {@link StartOrSignalConflictError}
|
|
228
|
+
* unless `options.onTerminalConflict: 'start-new'` is supplied with an explicit
|
|
226
229
|
* workflow id and deterministic `signal.signalId`.
|
|
227
230
|
*
|
|
228
231
|
* Concurrent callers converge on one workflow and one delivered signal. Pass
|
|
@@ -239,6 +242,16 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
|
|
|
239
242
|
list<const TAttributeKeys extends readonly AttributeFilterKey[] = readonly AttributeFilterKey[]>(filter?: TypedListFilter<TAttributeKeys>, options?: ListOptions): Promise<PaginatedResult<WorkflowSummary>>;
|
|
240
243
|
aggregate(filter: ListFilter | undefined, options: AggregateOptions): Promise<AggregateResult>;
|
|
241
244
|
getRetentionOverview(): RetentionOverview;
|
|
245
|
+
/**
|
|
246
|
+
* Run one host-driven maintenance cycle. This fires due durable timers,
|
|
247
|
+
* deletes expired update responses, applies configured retention, and
|
|
248
|
+
* re-evaluates alert rules without relying on process-local intervals.
|
|
249
|
+
*
|
|
250
|
+
* Use with `backgroundTasks: 'manual'` from a serverless alarm, Cron trigger,
|
|
251
|
+
* or another externally scheduled wake-up. Concurrent calls are safe, but a
|
|
252
|
+
* host should await each cycle before scheduling another.
|
|
253
|
+
*/
|
|
254
|
+
runMaintenance(now?: number): Promise<void>;
|
|
242
255
|
purge(filter?: ListFilter): Promise<PurgeResult>;
|
|
243
256
|
cancelAll(filter: ListFilter, options: BulkOperationDryRunOptions): Promise<BulkOperationDryRunResult>;
|
|
244
257
|
cancelAll(filter: ListFilter, options?: BulkOperationCommitOptions): Promise<BulkCancelResult>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AlertManager } from "../../alerting/alert-manager.js";
|
|
1
2
|
import {
|
|
2
3
|
KEYS,
|
|
3
4
|
requireStorageCapability
|
|
@@ -19,9 +20,9 @@ import {
|
|
|
19
20
|
} from "./aggregate.js";
|
|
20
21
|
import {
|
|
21
22
|
completeAsyncActivity as completeAsyncActivityFromInternals,
|
|
22
|
-
failAsyncActivity as failAsyncActivityFromInternals
|
|
23
|
-
recoverPendingAsyncActivities
|
|
23
|
+
failAsyncActivity as failAsyncActivityFromInternals
|
|
24
24
|
} from "./async-activity-completion.js";
|
|
25
|
+
import { recoverPendingAsyncActivities } from "./async-activity-records.js";
|
|
25
26
|
import { broadcast as broadcastFromInternals } from "./broadcast.js";
|
|
26
27
|
import {
|
|
27
28
|
cancelAll as cancelAllWorkflows,
|
|
@@ -52,7 +53,6 @@ import {
|
|
|
52
53
|
import {
|
|
53
54
|
copyWorkflowDefinition,
|
|
54
55
|
createActivityWorkerDispatcher,
|
|
55
|
-
createAlertManagerForEngine,
|
|
56
56
|
createExecutionStrategyBundle,
|
|
57
57
|
definitionEntries,
|
|
58
58
|
resolveEngineInterceptors,
|
|
@@ -70,9 +70,15 @@ import {
|
|
|
70
70
|
createQueuedInlineWorkflowStartHandler,
|
|
71
71
|
createSecondInstanceDetectorResolver,
|
|
72
72
|
drainQueuedInlineWorkflowStartsForEngine,
|
|
73
|
-
isActivityDefinition
|
|
73
|
+
isActivityDefinition,
|
|
74
|
+
shouldStartEngineScheduler,
|
|
75
|
+
validateEngineCreateBackgroundTaskOptions
|
|
74
76
|
} from "./engine-runtime-helpers.js";
|
|
75
|
-
import {
|
|
77
|
+
import {
|
|
78
|
+
EngineCreateNameMismatchError,
|
|
79
|
+
EngineDisposedError,
|
|
80
|
+
StartOrSignalConflictError
|
|
81
|
+
} from "./errors.js";
|
|
76
82
|
import { assertLeaseHeldForEngineWork, commitFencedEngineWrite } from "./fenced-write.js";
|
|
77
83
|
import { recordFinalizerState } from "./finalizer-state.js";
|
|
78
84
|
import {
|
|
@@ -143,7 +149,11 @@ import {
|
|
|
143
149
|
createSecondInstanceDetector
|
|
144
150
|
} from "./second-instance-detector.js";
|
|
145
151
|
import { signal as signalWorkflow } from "./signals.js";
|
|
146
|
-
import {
|
|
152
|
+
import {
|
|
153
|
+
loadScheduleState,
|
|
154
|
+
loadWorkflowState,
|
|
155
|
+
runSerializedWorkflowStateWrite
|
|
156
|
+
} from "./storage-io.js";
|
|
147
157
|
import {
|
|
148
158
|
feedOperationResult,
|
|
149
159
|
getComposedWorkflowInterceptor,
|
|
@@ -161,6 +171,7 @@ import {
|
|
|
161
171
|
submitCoordinatedUpdate as submitCoordinatedUpdateFromInternals,
|
|
162
172
|
update as updateFromInternals
|
|
163
173
|
} from "./updates.js";
|
|
174
|
+
import { isTerminalWorkflowStatus } from "./validation.js";
|
|
164
175
|
import { coerceScheduleId } from "./validation/schedule.js";
|
|
165
176
|
import {
|
|
166
177
|
replayWorkflowFeed,
|
|
@@ -222,6 +233,7 @@ function scheduleDefinitionFromInternals(internals, definition) {
|
|
|
222
233
|
|
|
223
234
|
export class Engine extends EventTarget {
|
|
224
235
|
static async create(options) {
|
|
236
|
+
validateEngineCreateBackgroundTaskOptions(options);
|
|
225
237
|
const engine = new Engine(options);
|
|
226
238
|
try {
|
|
227
239
|
await assertCompatiblePersistedDataVersion(getInternals(engine).storage);
|
|
@@ -238,7 +250,7 @@ export class Engine extends EventTarget {
|
|
|
238
250
|
await engine.#acquireLeaseIfConfigured();
|
|
239
251
|
if (options.recover !== !1)
|
|
240
252
|
await engine.recoverAll(options.acknowledgeUnknownWorkflowTypes !== void 0 ? { acknowledgeUnknownWorkflowTypes: options.acknowledgeUnknownWorkflowTypes } : {});
|
|
241
|
-
if (options.
|
|
253
|
+
if (shouldStartEngineScheduler(options, getInternals(engine).options.backgroundTaskMode))
|
|
242
254
|
getInternals(engine).scheduler.start();
|
|
243
255
|
} catch (error) {
|
|
244
256
|
await engine[Symbol.asyncDispose]();
|
|
@@ -277,6 +289,8 @@ export class Engine extends EventTarget {
|
|
|
277
289
|
getInternals(this).updateWaitersByWorkflow = new Map;
|
|
278
290
|
getInternals(this).sleepResolvers = new Map;
|
|
279
291
|
getInternals(this).sleepResolversByWorkflow = new Map;
|
|
292
|
+
getInternals(this).sleepTimerAcknowledgementWaiters = new Map;
|
|
293
|
+
getInternals(this).durableInlineOperations = new Map;
|
|
280
294
|
getInternals(this).sleepTimersFiredWithoutResolver = new Map;
|
|
281
295
|
getInternals(this).interceptors = resolveEngineInterceptors(options);
|
|
282
296
|
getInternals(this).composedWorkflowInterceptor = void 0;
|
|
@@ -341,7 +355,7 @@ export class Engine extends EventTarget {
|
|
|
341
355
|
cleanupInterval: null,
|
|
342
356
|
secondInstanceDetectionInterval: null,
|
|
343
357
|
testToken: consumeNextEngineLeakWarningTokenForTesting()
|
|
344
|
-
}, cleanupInterval = setInterval(createCleanupIntervalTick(weakEngine, cleanupIntervalDisposalTracker), 60000);
|
|
358
|
+
}, cleanupInterval = resolvedOptions.backgroundTaskMode === "automatic" ? setInterval(createCleanupIntervalTick(weakEngine, cleanupIntervalDisposalTracker), 60000) : null;
|
|
345
359
|
cleanupIntervalDisposalTracker.cleanupInterval = cleanupInterval;
|
|
346
360
|
getInternals(this).cleanupInterval = cleanupInterval;
|
|
347
361
|
getInternals(this).cleanupIntervalDisposalTracker = cleanupIntervalDisposalTracker;
|
|
@@ -364,7 +378,7 @@ export class Engine extends EventTarget {
|
|
|
364
378
|
getInternals(this).workflowVisibilityWatermarkExpiresAt = void 0;
|
|
365
379
|
getInternals(this).activityWorkerDispatcher = createActivityWorkerDispatcher(options?.activityExecution);
|
|
366
380
|
getInternals(this).strategy.onMessage(this.#handleStrategyMessage.bind(this));
|
|
367
|
-
getInternals(this).alertManager =
|
|
381
|
+
getInternals(this).alertManager = options?.alerts ? new AlertManager(this, options.alerts, getNow, resolvedOptions.backgroundTaskMode === "automatic") : null;
|
|
368
382
|
this.#ensureRetentionSweepInterval();
|
|
369
383
|
this.#startSecondInstanceDetection();
|
|
370
384
|
}
|
|
@@ -565,7 +579,12 @@ export class Engine extends EventTarget {
|
|
|
565
579
|
#createStartOrSignalCallbacks() {
|
|
566
580
|
return {
|
|
567
581
|
...this.#createLifecycleCallbacks(),
|
|
568
|
-
signalExistingWorkflow: (workflowId, signalName, payload, signalId) => this
|
|
582
|
+
signalExistingWorkflow: (workflowId, signalName, payload, signalId) => runSerializedWorkflowStateWrite(getInternals(this), workflowId, async () => {
|
|
583
|
+
const state = await loadWorkflowState(getInternals(this), workflowId);
|
|
584
|
+
if (state && isTerminalWorkflowStatus(state.status))
|
|
585
|
+
throw new StartOrSignalConflictError(workflowId, state.status);
|
|
586
|
+
await this.signal(workflowId, signalName, payload, { signalId });
|
|
587
|
+
})
|
|
569
588
|
};
|
|
570
589
|
}
|
|
571
590
|
getHandle(workflowId) {
|
|
@@ -586,6 +605,18 @@ export class Engine extends EventTarget {
|
|
|
586
605
|
getRetentionOverview() {
|
|
587
606
|
return getRetentionOverviewSnapshot(getInternals(this), (type) => resolveWorkflowTypeRetention(getInternals(this), type));
|
|
588
607
|
}
|
|
608
|
+
async runMaintenance(now = getInternals(this).options.getNow()) {
|
|
609
|
+
const internals = getInternals(this);
|
|
610
|
+
await internals.scheduler.tick(now);
|
|
611
|
+
try {
|
|
612
|
+
await internals.updateCoordinator.cleanupExpiredResponses();
|
|
613
|
+
} catch (error) {
|
|
614
|
+
this.#createTerminationCallbacks().handleCleanupError("cleanupExpiredResponses", error);
|
|
615
|
+
}
|
|
616
|
+
if (this.#hasConfiguredRetention())
|
|
617
|
+
await this.#runRetentionSweep();
|
|
618
|
+
internals.alertManager?.tick();
|
|
619
|
+
}
|
|
589
620
|
async purge(filter) {
|
|
590
621
|
return purgeWorkflows(getInternals(this), filter, (workflowId) => cleanupWaitersFromTermination(getInternals(this), workflowId, this.#createTerminationCallbacks()));
|
|
591
622
|
}
|
|
@@ -2,6 +2,10 @@ import { hasExposedAccessors, hasUpdateHandlers } from "../context/context-prese
|
|
|
2
2
|
import {
|
|
3
3
|
resumeWorkflowFromStorage as resumeWorkflowFromStorageFromLifecycle
|
|
4
4
|
} from "./lifecycle.js";
|
|
5
|
+
import {
|
|
6
|
+
recordDurableInlineOperation,
|
|
7
|
+
rejectSleepTimerAcknowledgements
|
|
8
|
+
} from "./sleep-timer-acknowledgements.js";
|
|
5
9
|
import {
|
|
6
10
|
completeWorkflow as completeWorkflowFromTermination,
|
|
7
11
|
failWorkflow as failWorkflowFromTermination
|
|
@@ -53,7 +57,7 @@ export async function getParkedWorkflowResumeDisposition(internals, workflowId,
|
|
|
53
57
|
return "corrupt";
|
|
54
58
|
return "resumable";
|
|
55
59
|
}
|
|
56
|
-
|
|
60
|
+
async function dispatchStrategyMessage(internals, message, callbacks) {
|
|
57
61
|
switch (message.type) {
|
|
58
62
|
case "completed":
|
|
59
63
|
await completeWorkflowFromTermination(internals, message.workflowId, message.result, callbacks.createTerminationCallbacks());
|
|
@@ -68,6 +72,7 @@ export async function handleStrategyMessage(internals, message, callbacks) {
|
|
|
68
72
|
case "checkpoint": {
|
|
69
73
|
const operation = callbacks.translateOperationRequest(message.operationRequest);
|
|
70
74
|
await callbacks.persistCheckpoint(message.workflowId, operation, message.checkpoint);
|
|
75
|
+
recordDurableInlineOperation(internals, message.workflowId, operation);
|
|
71
76
|
callbacks.validateDevelopmentCheckpoint(message.workflowId);
|
|
72
77
|
if (await callbacks.evaluateConstraints(message.workflowId))
|
|
73
78
|
break;
|
|
@@ -85,3 +90,11 @@ export async function handleStrategyMessage(internals, message, callbacks) {
|
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
}
|
|
93
|
+
export async function handleStrategyMessage(internals, message, callbacks) {
|
|
94
|
+
try {
|
|
95
|
+
await dispatchStrategyMessage(internals, message, callbacks);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
rejectSleepTimerAcknowledgements(internals, message.workflowId, error);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -22,6 +22,7 @@ import type { AlertManager } from '../../alerting/alert-manager.ts';
|
|
|
22
22
|
import type { Storage as WeftStorage } from '../../storage/interface.ts';
|
|
23
23
|
import type { ActivityWorkerDispatcher } from '../../workers/activity-worker-dispatcher.ts';
|
|
24
24
|
import type { ActivityRegistry } from '../activity-registry.ts';
|
|
25
|
+
import type { ContextOperationRequest } from '../context.ts';
|
|
25
26
|
import type { EventHeadRecord } from '../event-log.ts';
|
|
26
27
|
import type { ExecutionStrategy } from '../execution-strategy.ts';
|
|
27
28
|
import type { InlineExecutionStrategy } from '../inline-execution-strategy.ts';
|
|
@@ -39,6 +40,17 @@ import type { WorkflowFeedListener } from './index.ts';
|
|
|
39
40
|
import type { LeaseManager } from './lease-manager.ts';
|
|
40
41
|
import type { ScheduleHandleEngine } from './schedule-handle.ts';
|
|
41
42
|
import type { SecondInstanceDetector } from './second-instance-detector.ts';
|
|
43
|
+
export type SleepTimerAcknowledgementWaiter = {
|
|
44
|
+
fireAt: number;
|
|
45
|
+
operationId: string;
|
|
46
|
+
reject: (error: Error) => void;
|
|
47
|
+
resolve: () => void;
|
|
48
|
+
};
|
|
49
|
+
export type DurableInlineOperation = {
|
|
50
|
+
operationId: string;
|
|
51
|
+
scheduledFireAt?: number;
|
|
52
|
+
type: ContextOperationRequest['type'];
|
|
53
|
+
};
|
|
42
54
|
type EngineRuntime = WorkflowHandleEngine & ScheduleHandleEngine & {
|
|
43
55
|
start(type: string, input: unknown, options?: StartWorkflowOptions): Promise<WorkflowHandle>;
|
|
44
56
|
};
|
|
@@ -102,6 +114,15 @@ export interface EngineInternals {
|
|
|
102
114
|
fireAt: number;
|
|
103
115
|
}>;
|
|
104
116
|
sleepResolversByWorkflow: Map<string, Set<string>>;
|
|
117
|
+
/**
|
|
118
|
+
* Fired sleep timers awaiting proof that the awakened inline workflow reached
|
|
119
|
+
* its next durable checkpoint or terminal state. External schedulers must not
|
|
120
|
+
* delete a timer before these waiters settle, or a Service Worker eviction can
|
|
121
|
+
* lose the only durable wake-up between resolver settlement and checkpointing.
|
|
122
|
+
*/
|
|
123
|
+
sleepTimerAcknowledgementWaiters: Map<string, Set<SleepTimerAcknowledgementWaiter>>;
|
|
124
|
+
/** Most recently persisted inline operation, used to reject stale sleep-timer callbacks. */
|
|
125
|
+
durableInlineOperations: Map<string, DurableInlineOperation>;
|
|
105
126
|
/**
|
|
106
127
|
* Per-workflow maps of `operationId` → fired timer `fireAt` for sleep timers
|
|
107
128
|
* the scheduler tick fired before the resolver was registered. Keyed by
|
|
@@ -186,13 +207,13 @@ export interface EngineInternals {
|
|
|
186
207
|
* by their durable task token. Mirrored to storage (`KEYS.asyncActivity`) and
|
|
187
208
|
* reloaded by `recoverAll()`. See `async-activity-completion.ts`.
|
|
188
209
|
*/
|
|
189
|
-
pendingAsyncActivities: Map<string, import('./async-activity-
|
|
210
|
+
pendingAsyncActivities: Map<string, import('./async-activity-records.ts').PendingAsyncActivity>;
|
|
190
211
|
/**
|
|
191
212
|
* Completed or failed async-activity tokens that were consumed before inline
|
|
192
213
|
* recovery adopted the workflow generator. Replay drains these by workflow id
|
|
193
214
|
* when it reaches the same deterministic async-activity token again.
|
|
194
215
|
*/
|
|
195
|
-
pendingAsyncActivityResolutions: Map<string, import('./async-activity-
|
|
216
|
+
pendingAsyncActivityResolutions: Map<string, import('./async-activity-records.ts').PendingAsyncActivityResolution[]>;
|
|
196
217
|
pendingStarts: Set<string>;
|
|
197
218
|
pendingScheduleCreations: Set<string>;
|
|
198
219
|
workflowsNeedingTerminalCleanup: Set<string>;
|