@lostgradient/weft 0.10.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 +10 -2
- package/dist/cli-main.js +62 -62
- package/dist/core/checkpoint/serialization.js +2 -2
- package/dist/core/engine/disposal.js +4 -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/index.d.ts +6 -3
- package/dist/core/engine/index.js +19 -3
- package/dist/core/engine/inline-parking.js +14 -1
- package/dist/core/engine/internals.d.ts +21 -0
- package/dist/core/engine/operations-time.js +8 -28
- 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/termination/cleanup.js +3 -0
- package/dist/mcp/cli.js +17 -17
- package/dist/server/handler.js +12 -12
- package/dist/server/index.js +17 -17
- package/dist/service-worker/index.js +12 -12
- package/dist/service-worker/scheduler.js +1 -0
- package/dist/storage/lazy-postgres-pool.d.ts +38 -0
- package/dist/storage/lazy-postgres-pool.js +38 -0
- 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/testing/index.js +26 -26
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +16 -3
|
@@ -29,7 +29,7 @@ export function validateCheckpointShape(value) {
|
|
|
29
29
|
validateAccumulatedResultReplayWatermark(record);
|
|
30
30
|
validateWorkerReplaySignatures(record);
|
|
31
31
|
validateWorkerReplayFailures(record);
|
|
32
|
-
|
|
32
|
+
dropRetiredPendingSignals(record);
|
|
33
33
|
assertRecordField(record, "searchAttributes");
|
|
34
34
|
assertStringField(record, "version");
|
|
35
35
|
assertNumberField(record, "createdAt");
|
|
@@ -148,7 +148,7 @@ function validateAccumulatedResultReplayWatermark(record) {
|
|
|
148
148
|
if (typeof watermark !== "number" || !Number.isSafeInteger(watermark) || watermark < 0)
|
|
149
149
|
throw Error('Invalid checkpoint: invalid "accumulatedResultReplayWatermark" (expected non-negative safe integer)');
|
|
150
150
|
}
|
|
151
|
-
function
|
|
151
|
+
function dropRetiredPendingSignals(record) {
|
|
152
152
|
delete record.pendingSignals;
|
|
153
153
|
}
|
|
154
154
|
function assertCurrentSchemaVersion(record) {
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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
|
|
@@ -74,7 +74,11 @@ import {
|
|
|
74
74
|
shouldStartEngineScheduler,
|
|
75
75
|
validateEngineCreateBackgroundTaskOptions
|
|
76
76
|
} from "./engine-runtime-helpers.js";
|
|
77
|
-
import {
|
|
77
|
+
import {
|
|
78
|
+
EngineCreateNameMismatchError,
|
|
79
|
+
EngineDisposedError,
|
|
80
|
+
StartOrSignalConflictError
|
|
81
|
+
} from "./errors.js";
|
|
78
82
|
import { assertLeaseHeldForEngineWork, commitFencedEngineWrite } from "./fenced-write.js";
|
|
79
83
|
import { recordFinalizerState } from "./finalizer-state.js";
|
|
80
84
|
import {
|
|
@@ -145,7 +149,11 @@ import {
|
|
|
145
149
|
createSecondInstanceDetector
|
|
146
150
|
} from "./second-instance-detector.js";
|
|
147
151
|
import { signal as signalWorkflow } from "./signals.js";
|
|
148
|
-
import {
|
|
152
|
+
import {
|
|
153
|
+
loadScheduleState,
|
|
154
|
+
loadWorkflowState,
|
|
155
|
+
runSerializedWorkflowStateWrite
|
|
156
|
+
} from "./storage-io.js";
|
|
149
157
|
import {
|
|
150
158
|
feedOperationResult,
|
|
151
159
|
getComposedWorkflowInterceptor,
|
|
@@ -163,6 +171,7 @@ import {
|
|
|
163
171
|
submitCoordinatedUpdate as submitCoordinatedUpdateFromInternals,
|
|
164
172
|
update as updateFromInternals
|
|
165
173
|
} from "./updates.js";
|
|
174
|
+
import { isTerminalWorkflowStatus } from "./validation.js";
|
|
166
175
|
import { coerceScheduleId } from "./validation/schedule.js";
|
|
167
176
|
import {
|
|
168
177
|
replayWorkflowFeed,
|
|
@@ -280,6 +289,8 @@ export class Engine extends EventTarget {
|
|
|
280
289
|
getInternals(this).updateWaitersByWorkflow = new Map;
|
|
281
290
|
getInternals(this).sleepResolvers = new Map;
|
|
282
291
|
getInternals(this).sleepResolversByWorkflow = new Map;
|
|
292
|
+
getInternals(this).sleepTimerAcknowledgementWaiters = new Map;
|
|
293
|
+
getInternals(this).durableInlineOperations = new Map;
|
|
283
294
|
getInternals(this).sleepTimersFiredWithoutResolver = new Map;
|
|
284
295
|
getInternals(this).interceptors = resolveEngineInterceptors(options);
|
|
285
296
|
getInternals(this).composedWorkflowInterceptor = void 0;
|
|
@@ -568,7 +579,12 @@ export class Engine extends EventTarget {
|
|
|
568
579
|
#createStartOrSignalCallbacks() {
|
|
569
580
|
return {
|
|
570
581
|
...this.#createLifecycleCallbacks(),
|
|
571
|
-
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
|
+
})
|
|
572
588
|
};
|
|
573
589
|
}
|
|
574
590
|
getHandle(workflowId) {
|
|
@@ -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
|
|
@@ -4,6 +4,11 @@ import { encode } from "../codec.js";
|
|
|
4
4
|
import { buildTimerBatchOperations, normalizeStorageTimestamp } from "../scheduler.js";
|
|
5
5
|
import { commitFencedEngineWrite } from "./fenced-write.js";
|
|
6
6
|
import { reprovideRecoveredServices } from "./lifecycle/recovered-services.js";
|
|
7
|
+
import {
|
|
8
|
+
acknowledgeSupersededSleepTimers,
|
|
9
|
+
handleSleepTimerWithAcknowledgement,
|
|
10
|
+
resolveSleepTimer
|
|
11
|
+
} from "./sleep-timer-acknowledgements.js";
|
|
7
12
|
import { buildWorkflowVisibilityIndexTransition } from "./workflow-indexes.js";
|
|
8
13
|
export function createDelayedStartTimerEntry(_internals, workflowId, scheduledStartAt, options, callbacks) {
|
|
9
14
|
return {
|
|
@@ -26,6 +31,8 @@ function sleepTimerFiredEarly(internals, workflowId, operation) {
|
|
|
26
31
|
workflowMarkers.delete(operation.operationId);
|
|
27
32
|
if (workflowMarkers.size === 0)
|
|
28
33
|
internals.sleepTimersFiredWithoutResolver.delete(workflowId);
|
|
34
|
+
if (markedFireAt < operation.scheduledFireAt)
|
|
35
|
+
acknowledgeSupersededSleepTimers(internals, workflowId, operation.scheduledFireAt);
|
|
29
36
|
return markedFireAt >= operation.scheduledFireAt;
|
|
30
37
|
}
|
|
31
38
|
export async function processSleepOperation(internals, workflowId, operation, callbacks) {
|
|
@@ -166,7 +173,7 @@ export async function handleTimerFired(internals, entry, callbacks) {
|
|
|
166
173
|
return;
|
|
167
174
|
}
|
|
168
175
|
if (entry.kind === "sleep")
|
|
169
|
-
|
|
176
|
+
await handleSleepTimerWithAcknowledgement(internals, entry, callbacks.loadWorkflowState);
|
|
170
177
|
else if (entry.kind === "wait-condition")
|
|
171
178
|
resolveConditionTimer(internals, entry);
|
|
172
179
|
else if (entry.kind === "execution-deadline")
|
|
@@ -189,30 +196,3 @@ async function handleReviewTimer(internals, entry, callbacks) {
|
|
|
189
196
|
return;
|
|
190
197
|
await handler(entry);
|
|
191
198
|
}
|
|
192
|
-
function resolveSleepTimer(internals, entry) {
|
|
193
|
-
const operationId = entry.id.replace("sleep:", ""), resolverKey = `${entry.workflowId}:${operationId}`, resolver = internals.sleepResolvers.get(resolverKey);
|
|
194
|
-
if (!resolver) {
|
|
195
|
-
let workflowMarkers = internals.sleepTimersFiredWithoutResolver.get(entry.workflowId);
|
|
196
|
-
if (!workflowMarkers) {
|
|
197
|
-
workflowMarkers = new Map;
|
|
198
|
-
internals.sleepTimersFiredWithoutResolver.set(entry.workflowId, workflowMarkers);
|
|
199
|
-
}
|
|
200
|
-
const existing = workflowMarkers.get(operationId);
|
|
201
|
-
if (existing === void 0 || entry.fireAt > existing)
|
|
202
|
-
workflowMarkers.set(operationId, entry.fireAt);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
if (entry.fireAt < resolver.fireAt)
|
|
206
|
-
return;
|
|
207
|
-
internals.sleepResolvers.delete(resolverKey);
|
|
208
|
-
untrackSleepResolver(internals, entry.workflowId, operationId);
|
|
209
|
-
resolver.resolve();
|
|
210
|
-
}
|
|
211
|
-
function untrackSleepResolver(internals, workflowId, operationId) {
|
|
212
|
-
const workflowOperations = internals.sleepResolversByWorkflow.get(workflowId);
|
|
213
|
-
if (!workflowOperations)
|
|
214
|
-
return;
|
|
215
|
-
workflowOperations.delete(operationId);
|
|
216
|
-
if (workflowOperations.size === 0)
|
|
217
|
-
internals.sleepResolversByWorkflow.delete(workflowId);
|
|
218
|
-
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ContextOperationRequest } from '../context.ts';
|
|
2
|
+
import type { TimerEntry, WorkflowState } from '../types.ts';
|
|
3
|
+
import type { EngineInternals } from './internals.ts';
|
|
4
|
+
export type SleepTimerAcknowledgement = {
|
|
5
|
+
cancel: () => void;
|
|
6
|
+
promise: Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
export declare function handleSleepTimerWithAcknowledgement(internals: EngineInternals, entry: TimerEntry, loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>): Promise<void>;
|
|
9
|
+
export declare function createSleepTimerAcknowledgement(internals: EngineInternals, workflowId: string, operationId: string, fireAt: number): SleepTimerAcknowledgement;
|
|
10
|
+
export declare function recordDurableInlineOperation(internals: EngineInternals, workflowId: string, operation: ContextOperationRequest): void;
|
|
11
|
+
export declare function settleSleepTimerAcknowledgements(internals: EngineInternals, workflowId: string, disposition: 'suspended' | 'terminal'): void;
|
|
12
|
+
export declare function acknowledgeSupersededSleepTimers(internals: EngineInternals, workflowId: string, currentDeadline: number): void;
|
|
13
|
+
export declare function rejectSleepTimerAcknowledgements(internals: EngineInternals, workflowId: string, error: unknown): void;
|
|
14
|
+
export declare function rejectAllSleepTimerAcknowledgements(internals: EngineInternals, error: Error): void;
|
|
15
|
+
export declare function resolveSleepTimer(internals: EngineInternals, entry: TimerEntry): boolean;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export async function handleSleepTimerWithAcknowledgement(internals, entry, loadWorkflowState) {
|
|
2
|
+
const operationId = entry.id.replace("sleep:", "");
|
|
3
|
+
if (await shouldIgnoreUnclaimedSleepTimer(internals, entry, operationId, loadWorkflowState))
|
|
4
|
+
return;
|
|
5
|
+
const acknowledgement = internals.inlineStrategy === null ? null : createSleepTimerAcknowledgement(internals, entry.workflowId, operationId, entry.fireAt);
|
|
6
|
+
if (!resolveSleepTimer(internals, entry)) {
|
|
7
|
+
acknowledgement?.cancel();
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
await acknowledgement?.promise;
|
|
11
|
+
}
|
|
12
|
+
async function shouldIgnoreUnclaimedSleepTimer(internals, entry, operationId, loadWorkflowState) {
|
|
13
|
+
const resolverKey = `${entry.workflowId}:${operationId}`;
|
|
14
|
+
if (internals.sleepResolvers.has(resolverKey) || internals.inlineStrategy === null)
|
|
15
|
+
return !1;
|
|
16
|
+
const durableOperation = internals.durableInlineOperations.get(entry.workflowId);
|
|
17
|
+
if (durableOperationBelongsToAnotherOperation(durableOperation, operationId))
|
|
18
|
+
return !0;
|
|
19
|
+
if (workflowReplayIsAdvancing(internals, entry.workflowId))
|
|
20
|
+
return !1;
|
|
21
|
+
if ((await loadWorkflowState(entry.workflowId))?.status !== "running")
|
|
22
|
+
return !0;
|
|
23
|
+
throw Error(`Sleep timer "${entry.id}" fired before workflow "${entry.workflowId}" was ready to consume it.`);
|
|
24
|
+
}
|
|
25
|
+
function durableOperationBelongsToAnotherOperation(durableOperation, operationId) {
|
|
26
|
+
if (durableOperation === void 0)
|
|
27
|
+
return !1;
|
|
28
|
+
if (durableOperation.type !== "sleep")
|
|
29
|
+
return !0;
|
|
30
|
+
return durableOperation.operationId !== operationId;
|
|
31
|
+
}
|
|
32
|
+
function workflowReplayIsAdvancing(internals, workflowId) {
|
|
33
|
+
const inlineStrategy = internals.inlineStrategy;
|
|
34
|
+
if (inlineStrategy === null)
|
|
35
|
+
return !1;
|
|
36
|
+
return inlineStrategy.waitForWorkflowAdvance(workflowId) !== void 0 || inlineStrategy.waitForWorkflowTurn(workflowId) !== void 0;
|
|
37
|
+
}
|
|
38
|
+
function removeWaiter(internals, workflowId, waiter) {
|
|
39
|
+
const workflowWaiters = internals.sleepTimerAcknowledgementWaiters.get(workflowId);
|
|
40
|
+
if (!workflowWaiters)
|
|
41
|
+
return;
|
|
42
|
+
workflowWaiters.delete(waiter);
|
|
43
|
+
if (workflowWaiters.size === 0)
|
|
44
|
+
internals.sleepTimerAcknowledgementWaiters.delete(workflowId);
|
|
45
|
+
}
|
|
46
|
+
export function createSleepTimerAcknowledgement(internals, workflowId, operationId, fireAt) {
|
|
47
|
+
const { promise, reject, resolve } = Promise.withResolvers(), waiter = { fireAt, operationId, reject, resolve };
|
|
48
|
+
let workflowWaiters = internals.sleepTimerAcknowledgementWaiters.get(workflowId);
|
|
49
|
+
if (!workflowWaiters) {
|
|
50
|
+
workflowWaiters = new Set;
|
|
51
|
+
internals.sleepTimerAcknowledgementWaiters.set(workflowId, workflowWaiters);
|
|
52
|
+
}
|
|
53
|
+
workflowWaiters.add(waiter);
|
|
54
|
+
return {
|
|
55
|
+
promise,
|
|
56
|
+
cancel: () => {
|
|
57
|
+
removeWaiter(internals, workflowId, waiter);
|
|
58
|
+
resolve();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function recordDurableInlineOperation(internals, workflowId, operation) {
|
|
63
|
+
const durableOperation = {
|
|
64
|
+
operationId: operation.operationId,
|
|
65
|
+
type: operation.type,
|
|
66
|
+
...operation.type === "sleep" && { scheduledFireAt: operation.scheduledFireAt }
|
|
67
|
+
};
|
|
68
|
+
internals.durableInlineOperations.set(workflowId, durableOperation);
|
|
69
|
+
const workflowWaiters = internals.sleepTimerAcknowledgementWaiters.get(workflowId);
|
|
70
|
+
if (!workflowWaiters)
|
|
71
|
+
return;
|
|
72
|
+
for (const waiter of workflowWaiters) {
|
|
73
|
+
const stillWaitingOnThisSleep = durableOperation.type === "sleep" && durableOperation.operationId === waiter.operationId, supersededByLaterDeadline = stillWaitingOnThisSleep && durableOperation.scheduledFireAt !== void 0 && waiter.fireAt < durableOperation.scheduledFireAt;
|
|
74
|
+
if (stillWaitingOnThisSleep && !supersededByLaterDeadline)
|
|
75
|
+
continue;
|
|
76
|
+
removeWaiter(internals, workflowId, waiter);
|
|
77
|
+
waiter.resolve();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function settleSleepTimerAcknowledgements(internals, workflowId, disposition) {
|
|
81
|
+
internals.durableInlineOperations.delete(workflowId);
|
|
82
|
+
if (disposition === "suspended") {
|
|
83
|
+
rejectSleepTimerAcknowledgements(internals, workflowId, Error(`Workflow "${workflowId}" was suspended before its sleep timer was acknowledged.`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const workflowWaiters = internals.sleepTimerAcknowledgementWaiters.get(workflowId);
|
|
87
|
+
if (!workflowWaiters)
|
|
88
|
+
return;
|
|
89
|
+
internals.sleepTimerAcknowledgementWaiters.delete(workflowId);
|
|
90
|
+
for (const waiter of workflowWaiters)
|
|
91
|
+
waiter.resolve();
|
|
92
|
+
}
|
|
93
|
+
export function acknowledgeSupersededSleepTimers(internals, workflowId, currentDeadline) {
|
|
94
|
+
const workflowWaiters = internals.sleepTimerAcknowledgementWaiters.get(workflowId);
|
|
95
|
+
if (!workflowWaiters)
|
|
96
|
+
return;
|
|
97
|
+
for (const waiter of workflowWaiters) {
|
|
98
|
+
if (waiter.fireAt >= currentDeadline)
|
|
99
|
+
continue;
|
|
100
|
+
removeWaiter(internals, workflowId, waiter);
|
|
101
|
+
waiter.resolve();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function rejectSleepTimerAcknowledgements(internals, workflowId, error) {
|
|
105
|
+
const workflowWaiters = internals.sleepTimerAcknowledgementWaiters.get(workflowId);
|
|
106
|
+
if (!workflowWaiters)
|
|
107
|
+
return;
|
|
108
|
+
internals.sleepTimerAcknowledgementWaiters.delete(workflowId);
|
|
109
|
+
const rejection = error instanceof Error ? error : Error(String(error));
|
|
110
|
+
for (const waiter of workflowWaiters)
|
|
111
|
+
waiter.reject(rejection);
|
|
112
|
+
}
|
|
113
|
+
export function rejectAllSleepTimerAcknowledgements(internals, error) {
|
|
114
|
+
for (const workflowId of internals.sleepTimerAcknowledgementWaiters.keys())
|
|
115
|
+
rejectSleepTimerAcknowledgements(internals, workflowId, error);
|
|
116
|
+
}
|
|
117
|
+
export function resolveSleepTimer(internals, entry) {
|
|
118
|
+
const operationId = entry.id.replace("sleep:", ""), resolverKey = `${entry.workflowId}:${operationId}`, resolver = internals.sleepResolvers.get(resolverKey);
|
|
119
|
+
if (!resolver) {
|
|
120
|
+
let workflowMarkers = internals.sleepTimersFiredWithoutResolver.get(entry.workflowId);
|
|
121
|
+
if (!workflowMarkers) {
|
|
122
|
+
workflowMarkers = new Map;
|
|
123
|
+
internals.sleepTimersFiredWithoutResolver.set(entry.workflowId, workflowMarkers);
|
|
124
|
+
}
|
|
125
|
+
const existing = workflowMarkers.get(operationId);
|
|
126
|
+
if (existing === void 0 || entry.fireAt > existing)
|
|
127
|
+
workflowMarkers.set(operationId, entry.fireAt);
|
|
128
|
+
return !0;
|
|
129
|
+
}
|
|
130
|
+
if (entry.fireAt < resolver.fireAt)
|
|
131
|
+
return !1;
|
|
132
|
+
internals.sleepResolvers.delete(resolverKey);
|
|
133
|
+
untrackSleepResolver(internals, entry.workflowId, operationId);
|
|
134
|
+
resolver.resolve();
|
|
135
|
+
return !0;
|
|
136
|
+
}
|
|
137
|
+
function untrackSleepResolver(internals, workflowId, operationId) {
|
|
138
|
+
const workflowOperations = internals.sleepResolversByWorkflow.get(workflowId);
|
|
139
|
+
if (!workflowOperations)
|
|
140
|
+
return;
|
|
141
|
+
workflowOperations.delete(operationId);
|
|
142
|
+
if (workflowOperations.size === 0)
|
|
143
|
+
internals.sleepResolversByWorkflow.delete(workflowId);
|
|
144
|
+
}
|
|
@@ -2,6 +2,7 @@ import { KEYS, encodeStorageKeyComponent, storageHas } from "../../../storage/in
|
|
|
2
2
|
import { CleanupWarningEvent } from "../../events.js";
|
|
3
3
|
import { asyncActivityWorkflowPrefix } from "../async-activity-records.js";
|
|
4
4
|
import { forgetCommittedCheckpointBytes } from "../checkpoint-commit-snapshots.js";
|
|
5
|
+
import { settleSleepTimerAcknowledgements } from "../sleep-timer-acknowledgements.js";
|
|
5
6
|
import { parseTerminalCleanupTimerId, workflowFeedListenerKey } from "../state-utilities.js";
|
|
6
7
|
import { releaseWorkflowConcurrencySlot } from "../workflow-concurrency.js";
|
|
7
8
|
export const TERMINAL_WORKFLOW_STATUSES = new Set([
|
|
@@ -102,6 +103,7 @@ export function cleanupWaiters(internals, workflowId, callbacks) {
|
|
|
102
103
|
internals.workflowTypeByWorkflowId.delete(workflowId);
|
|
103
104
|
}
|
|
104
105
|
export function evictSuspendedWorkflowWaiters(internals, workflowId, callbacks) {
|
|
106
|
+
settleSleepTimerAcknowledgements(internals, workflowId, "suspended");
|
|
105
107
|
for (const kind of TRACKED_WAITER_KINDS)
|
|
106
108
|
cleanupTrackedWaiter(internals, workflowId, kind);
|
|
107
109
|
evictSleepResolversWithoutResolving(internals, workflowId);
|
|
@@ -148,6 +150,7 @@ export async function cleanupWorkflowStorage(internals, workflowId, includeOutpu
|
|
|
148
150
|
await flush();
|
|
149
151
|
}
|
|
150
152
|
export function cleanupTerminalWorkflowMemory(internals, workflowId, callbacks) {
|
|
153
|
+
settleSleepTimerAcknowledgements(internals, workflowId, "terminal");
|
|
151
154
|
internals.workflowsNeedingTerminalCleanup.delete(workflowId);
|
|
152
155
|
forgetCommittedCheckpointBytes(internals, workflowId);
|
|
153
156
|
internals.checkpoints.delete(workflowId);
|