@zucker-framework/task 1.0.0 → 1.0.5
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 +11 -0
- package/dist/index.d.mts +55 -1
- package/dist/index.d.ts +55 -1
- package/dist/index.js +161 -2
- package/dist/index.mjs +157 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Single-process supervisor
|
|
2
|
+
|
|
3
|
+
`SingleProcessTaskSupervisor` receives `initialize`, `loadPending`, `run`, `abort`, concurrency and scan interval. It deduplicates pending/running keys and marks startup or manually submitted recovery keys for `run(id, true)`. Register the configured instance once at the composition root. Initialization and scanning failures preserve their separate failure paths; shutdown clears queued work and aborts running work without a drain wait.
|
|
4
|
+
|
|
5
|
+
The consumer owns persistent states, startup state transitions, retry eligibility, progress, billing and checkpoints. This primitive has no database schema, lease, fencing or multi-executor guarantee. It must not be used to justify overlapping executors.
|
|
6
|
+
|
|
7
|
+
## Execution heartbeat
|
|
8
|
+
|
|
9
|
+
`runWithExecutionHeartbeat({ signal, heartbeat, intervalMs, failureThreshold }, work)` scopes serial heartbeat attempts to one callback. A successful heartbeat resets the consecutive failure count. At the configured threshold the work signal aborts with `ExecutionHeartbeatError`; the parent signal is unaffected, and the failure remains an error even when work swallows its abort. Caller cancellation forwards its original reason. Work must honor cancellation; the scope does not detach or duplicate it.
|
|
10
|
+
|
|
11
|
+
The scope clears its timer/listener and awaits an already-running heartbeat before returning. The consumer owns all database writes and decides how heartbeat failure differs from user cancellation or process shutdown. No state table, lease, retry, worker or execution topology is introduced.
|
package/dist/index.d.mts
CHANGED
|
@@ -601,4 +601,58 @@ declare class TaskOutboxDispatcher<TPayload = unknown> {
|
|
|
601
601
|
runOnce(): Promise<TaskOutboxDispatchResult>;
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
-
|
|
604
|
+
interface SingleProcessTaskSupervisorOptions {
|
|
605
|
+
concurrency: number;
|
|
606
|
+
scanIntervalMs: number;
|
|
607
|
+
/** Perform consumer-owned startup recovery and return keys that require resumption. */
|
|
608
|
+
initialize?: () => Promise<readonly string[]>;
|
|
609
|
+
loadPending: () => Promise<readonly string[]>;
|
|
610
|
+
run: (id: string, recovery: boolean) => Promise<void>;
|
|
611
|
+
abort: (id: string) => void;
|
|
612
|
+
onScanError?: (error: unknown) => void;
|
|
613
|
+
onExecutionError?: (id: string, error: unknown) => void | Promise<void>;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* One-process FIFO scheduling, deduplication and recovery polling. Persistent
|
|
617
|
+
* state belongs to the consumer. This provides no lease, fencing or multi-executor safety.
|
|
618
|
+
* Shutdown aborts active callbacks immediately and deliberately does not drain them.
|
|
619
|
+
*/
|
|
620
|
+
declare class SingleProcessTaskSupervisor {
|
|
621
|
+
private readonly options;
|
|
622
|
+
private readonly queued;
|
|
623
|
+
private readonly running;
|
|
624
|
+
private readonly interrupted;
|
|
625
|
+
private queue;
|
|
626
|
+
private scanTimer?;
|
|
627
|
+
private stopped;
|
|
628
|
+
private started;
|
|
629
|
+
constructor(options: SingleProcessTaskSupervisorOptions);
|
|
630
|
+
onApplicationBootstrap(): Promise<void>;
|
|
631
|
+
submit(id: string): void;
|
|
632
|
+
submitRecovery(id: string): void;
|
|
633
|
+
waitForIdle(): Promise<void>;
|
|
634
|
+
onApplicationShutdown(): Promise<void>;
|
|
635
|
+
private drain;
|
|
636
|
+
private scanPending;
|
|
637
|
+
private scheduleScan;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
declare class ExecutionHeartbeatError extends Error {
|
|
641
|
+
readonly consecutiveFailures: number;
|
|
642
|
+
constructor(consecutiveFailures: number, options?: ErrorOptions);
|
|
643
|
+
}
|
|
644
|
+
interface ExecutionHeartbeatOptions {
|
|
645
|
+
signal?: AbortSignal;
|
|
646
|
+
intervalMs: number;
|
|
647
|
+
failureThreshold: number;
|
|
648
|
+
heartbeat: () => Promise<void>;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* A single execution's heartbeat and cancellation scope. No lease, persistent
|
|
652
|
+
* state transition or retry policy is implied. Heartbeats never overlap; the
|
|
653
|
+
* final in-flight heartbeat settles before this scope returns. Work must honor
|
|
654
|
+
* its signal, and a heartbeat abort cannot be reported as successful work.
|
|
655
|
+
*/
|
|
656
|
+
declare function runWithExecutionHeartbeat<T>(options: ExecutionHeartbeatOptions, work: (signal: AbortSignal) => Promise<T>): Promise<T>;
|
|
657
|
+
|
|
658
|
+
export { type AppendTaskOutboxInput, type ClaimTaskInput, type ClaimTaskOutboxInput, type CreateTaskInput, type CreateTaskResult, type DurableTaskAttemptContext, type DurableTaskHandler, DurableTaskLeaseLostError, DurableTaskNotClaimableError, DurableTaskNotFoundError, DurableTaskRetryScheduledError, type DurableWorkerHeartbeatScheduler, type DurableWorkerRunResult, DurableWorkerRuntime, type DurableWorkerRuntimeLogger, type DurableWorkerRuntimeOptions, ExecutionHeartbeatError, type ExecutionHeartbeatOptions, ExponentialTaskRetryPolicy, type ExponentialTaskRetryPolicyOptions, type FindExpiredTaskLeasesInput, type FindReadyQueuedTasksInput, type HeartbeatTaskInput, InMemoryTaskOutboxRepository, InMemoryTaskRepository, InvalidTaskTransitionError, type ListOwnedTaskSummariesInput, type OwnedTaskTimelineEvent, type PostgresQueryResult, type PostgresTaskClient, PostgresTaskOutboxRepository, PostgresTaskRedriver, PostgresTaskRepository, PostgresTaskSubmitter, PostgresTaskSummaryReader, PostgresTaskTimelineReader, type PostgresTaskTransactionRunner, type ReadyTaskPublisher, type RecoverExpiredTaskLeaseInput, type RedriveFailedTaskInput, type ReportTaskProgressInput, SingleProcessTaskSupervisor, type SingleProcessTaskSupervisorOptions, type SubmitTaskBatchOptions, type SubmitTaskInput, type SubmitTaskOptions, type SubmitTaskOutboxInput, type SubmitTaskResult, TASK_STATUSES, TASK_TIMELINE_EVENT_TYPES, type TaskErrorSummary, type TaskFailureCategory, TaskIdempotencyConflictError, type TaskLeaseReconcileResult, TaskLeaseReconciler, type TaskLeaseReconcilerOptions, TaskOutboxConflictError, type TaskOutboxDispatchResult, TaskOutboxDispatcher, type TaskOutboxDispatcherOptions, type TaskOutboxRecord, type TaskOutboxRepository, type TaskOutboxStatus, type TaskQueuePublisher, type TaskQueueReconcileResult, TaskQueueReconciler, type TaskQueueReconcilerOptions, type TaskRecord, type TaskRedriveResult, type TaskRepository, type TaskResourceRef, type TaskRetryContext, type TaskRetryDecision, type TaskRetryPolicy, type TaskStatus, type TaskSummary, type TaskSummaryError, type TaskTimelineEvent, type TaskTimelineEventType, type TransitionTaskInput, UnsupportedDurableTaskKindError, assertTaskTransition, canTransitionTask, createTaskFingerprint, isTerminalTaskStatus, requiresTaskLease, runWithExecutionHeartbeat };
|
package/dist/index.d.ts
CHANGED
|
@@ -601,4 +601,58 @@ declare class TaskOutboxDispatcher<TPayload = unknown> {
|
|
|
601
601
|
runOnce(): Promise<TaskOutboxDispatchResult>;
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
-
|
|
604
|
+
interface SingleProcessTaskSupervisorOptions {
|
|
605
|
+
concurrency: number;
|
|
606
|
+
scanIntervalMs: number;
|
|
607
|
+
/** Perform consumer-owned startup recovery and return keys that require resumption. */
|
|
608
|
+
initialize?: () => Promise<readonly string[]>;
|
|
609
|
+
loadPending: () => Promise<readonly string[]>;
|
|
610
|
+
run: (id: string, recovery: boolean) => Promise<void>;
|
|
611
|
+
abort: (id: string) => void;
|
|
612
|
+
onScanError?: (error: unknown) => void;
|
|
613
|
+
onExecutionError?: (id: string, error: unknown) => void | Promise<void>;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* One-process FIFO scheduling, deduplication and recovery polling. Persistent
|
|
617
|
+
* state belongs to the consumer. This provides no lease, fencing or multi-executor safety.
|
|
618
|
+
* Shutdown aborts active callbacks immediately and deliberately does not drain them.
|
|
619
|
+
*/
|
|
620
|
+
declare class SingleProcessTaskSupervisor {
|
|
621
|
+
private readonly options;
|
|
622
|
+
private readonly queued;
|
|
623
|
+
private readonly running;
|
|
624
|
+
private readonly interrupted;
|
|
625
|
+
private queue;
|
|
626
|
+
private scanTimer?;
|
|
627
|
+
private stopped;
|
|
628
|
+
private started;
|
|
629
|
+
constructor(options: SingleProcessTaskSupervisorOptions);
|
|
630
|
+
onApplicationBootstrap(): Promise<void>;
|
|
631
|
+
submit(id: string): void;
|
|
632
|
+
submitRecovery(id: string): void;
|
|
633
|
+
waitForIdle(): Promise<void>;
|
|
634
|
+
onApplicationShutdown(): Promise<void>;
|
|
635
|
+
private drain;
|
|
636
|
+
private scanPending;
|
|
637
|
+
private scheduleScan;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
declare class ExecutionHeartbeatError extends Error {
|
|
641
|
+
readonly consecutiveFailures: number;
|
|
642
|
+
constructor(consecutiveFailures: number, options?: ErrorOptions);
|
|
643
|
+
}
|
|
644
|
+
interface ExecutionHeartbeatOptions {
|
|
645
|
+
signal?: AbortSignal;
|
|
646
|
+
intervalMs: number;
|
|
647
|
+
failureThreshold: number;
|
|
648
|
+
heartbeat: () => Promise<void>;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* A single execution's heartbeat and cancellation scope. No lease, persistent
|
|
652
|
+
* state transition or retry policy is implied. Heartbeats never overlap; the
|
|
653
|
+
* final in-flight heartbeat settles before this scope returns. Work must honor
|
|
654
|
+
* its signal, and a heartbeat abort cannot be reported as successful work.
|
|
655
|
+
*/
|
|
656
|
+
declare function runWithExecutionHeartbeat<T>(options: ExecutionHeartbeatOptions, work: (signal: AbortSignal) => Promise<T>): Promise<T>;
|
|
657
|
+
|
|
658
|
+
export { type AppendTaskOutboxInput, type ClaimTaskInput, type ClaimTaskOutboxInput, type CreateTaskInput, type CreateTaskResult, type DurableTaskAttemptContext, type DurableTaskHandler, DurableTaskLeaseLostError, DurableTaskNotClaimableError, DurableTaskNotFoundError, DurableTaskRetryScheduledError, type DurableWorkerHeartbeatScheduler, type DurableWorkerRunResult, DurableWorkerRuntime, type DurableWorkerRuntimeLogger, type DurableWorkerRuntimeOptions, ExecutionHeartbeatError, type ExecutionHeartbeatOptions, ExponentialTaskRetryPolicy, type ExponentialTaskRetryPolicyOptions, type FindExpiredTaskLeasesInput, type FindReadyQueuedTasksInput, type HeartbeatTaskInput, InMemoryTaskOutboxRepository, InMemoryTaskRepository, InvalidTaskTransitionError, type ListOwnedTaskSummariesInput, type OwnedTaskTimelineEvent, type PostgresQueryResult, type PostgresTaskClient, PostgresTaskOutboxRepository, PostgresTaskRedriver, PostgresTaskRepository, PostgresTaskSubmitter, PostgresTaskSummaryReader, PostgresTaskTimelineReader, type PostgresTaskTransactionRunner, type ReadyTaskPublisher, type RecoverExpiredTaskLeaseInput, type RedriveFailedTaskInput, type ReportTaskProgressInput, SingleProcessTaskSupervisor, type SingleProcessTaskSupervisorOptions, type SubmitTaskBatchOptions, type SubmitTaskInput, type SubmitTaskOptions, type SubmitTaskOutboxInput, type SubmitTaskResult, TASK_STATUSES, TASK_TIMELINE_EVENT_TYPES, type TaskErrorSummary, type TaskFailureCategory, TaskIdempotencyConflictError, type TaskLeaseReconcileResult, TaskLeaseReconciler, type TaskLeaseReconcilerOptions, TaskOutboxConflictError, type TaskOutboxDispatchResult, TaskOutboxDispatcher, type TaskOutboxDispatcherOptions, type TaskOutboxRecord, type TaskOutboxRepository, type TaskOutboxStatus, type TaskQueuePublisher, type TaskQueueReconcileResult, TaskQueueReconciler, type TaskQueueReconcilerOptions, type TaskRecord, type TaskRedriveResult, type TaskRepository, type TaskResourceRef, type TaskRetryContext, type TaskRetryDecision, type TaskRetryPolicy, type TaskStatus, type TaskSummary, type TaskSummaryError, type TaskTimelineEvent, type TaskTimelineEventType, type TransitionTaskInput, UnsupportedDurableTaskKindError, assertTaskTransition, canTransitionTask, createTaskFingerprint, isTerminalTaskStatus, requiresTaskLease, runWithExecutionHeartbeat };
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ __export(index_exports, {
|
|
|
26
26
|
DurableTaskNotFoundError: () => DurableTaskNotFoundError,
|
|
27
27
|
DurableTaskRetryScheduledError: () => DurableTaskRetryScheduledError,
|
|
28
28
|
DurableWorkerRuntime: () => DurableWorkerRuntime,
|
|
29
|
+
ExecutionHeartbeatError: () => ExecutionHeartbeatError,
|
|
29
30
|
ExponentialTaskRetryPolicy: () => ExponentialTaskRetryPolicy,
|
|
30
31
|
InMemoryTaskOutboxRepository: () => InMemoryTaskOutboxRepository,
|
|
31
32
|
InMemoryTaskRepository: () => InMemoryTaskRepository,
|
|
@@ -36,6 +37,7 @@ __export(index_exports, {
|
|
|
36
37
|
PostgresTaskSubmitter: () => PostgresTaskSubmitter,
|
|
37
38
|
PostgresTaskSummaryReader: () => PostgresTaskSummaryReader,
|
|
38
39
|
PostgresTaskTimelineReader: () => PostgresTaskTimelineReader,
|
|
40
|
+
SingleProcessTaskSupervisor: () => SingleProcessTaskSupervisor,
|
|
39
41
|
TASK_STATUSES: () => TASK_STATUSES,
|
|
40
42
|
TASK_TIMELINE_EVENT_TYPES: () => TASK_TIMELINE_EVENT_TYPES,
|
|
41
43
|
TaskIdempotencyConflictError: () => TaskIdempotencyConflictError,
|
|
@@ -48,7 +50,8 @@ __export(index_exports, {
|
|
|
48
50
|
canTransitionTask: () => canTransitionTask,
|
|
49
51
|
createTaskFingerprint: () => createTaskFingerprint,
|
|
50
52
|
isTerminalTaskStatus: () => isTerminalTaskStatus,
|
|
51
|
-
requiresTaskLease: () => requiresTaskLease
|
|
53
|
+
requiresTaskLease: () => requiresTaskLease,
|
|
54
|
+
runWithExecutionHeartbeat: () => runWithExecutionHeartbeat
|
|
52
55
|
});
|
|
53
56
|
module.exports = __toCommonJS(index_exports);
|
|
54
57
|
|
|
@@ -1900,6 +1903,159 @@ var TaskOutboxDispatcher = class {
|
|
|
1900
1903
|
return result;
|
|
1901
1904
|
}
|
|
1902
1905
|
};
|
|
1906
|
+
|
|
1907
|
+
// src/single-process-task-supervisor.ts
|
|
1908
|
+
var SingleProcessTaskSupervisor = class {
|
|
1909
|
+
static {
|
|
1910
|
+
__name(this, "SingleProcessTaskSupervisor");
|
|
1911
|
+
}
|
|
1912
|
+
options;
|
|
1913
|
+
queued = /* @__PURE__ */ new Set();
|
|
1914
|
+
running = /* @__PURE__ */ new Map();
|
|
1915
|
+
interrupted = /* @__PURE__ */ new Set();
|
|
1916
|
+
queue = [];
|
|
1917
|
+
scanTimer;
|
|
1918
|
+
stopped = false;
|
|
1919
|
+
started = false;
|
|
1920
|
+
constructor(options) {
|
|
1921
|
+
this.options = options;
|
|
1922
|
+
}
|
|
1923
|
+
async onApplicationBootstrap() {
|
|
1924
|
+
if (this.started || this.stopped) return;
|
|
1925
|
+
this.started = true;
|
|
1926
|
+
const interrupted = await this.options.initialize?.() ?? [];
|
|
1927
|
+
interrupted.forEach((id) => this.interrupted.add(id));
|
|
1928
|
+
await this.scanPending();
|
|
1929
|
+
this.scheduleScan();
|
|
1930
|
+
}
|
|
1931
|
+
submit(id) {
|
|
1932
|
+
if (this.stopped || this.queued.has(id) || this.running.has(id)) return;
|
|
1933
|
+
this.queued.add(id);
|
|
1934
|
+
this.queue.push(id);
|
|
1935
|
+
this.drain();
|
|
1936
|
+
}
|
|
1937
|
+
submitRecovery(id) {
|
|
1938
|
+
if (this.stopped || this.queued.has(id) || this.running.has(id)) return;
|
|
1939
|
+
this.interrupted.add(id);
|
|
1940
|
+
this.submit(id);
|
|
1941
|
+
}
|
|
1942
|
+
async waitForIdle() {
|
|
1943
|
+
while (this.queue.length > 0 || this.running.size > 0) {
|
|
1944
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
async onApplicationShutdown() {
|
|
1948
|
+
this.stopped = true;
|
|
1949
|
+
if (this.scanTimer) clearTimeout(this.scanTimer);
|
|
1950
|
+
this.queue = [];
|
|
1951
|
+
this.queued.clear();
|
|
1952
|
+
this.interrupted.clear();
|
|
1953
|
+
for (const id of this.running.keys()) this.options.abort(id);
|
|
1954
|
+
}
|
|
1955
|
+
drain() {
|
|
1956
|
+
while (!this.stopped && this.running.size < this.options.concurrency && this.queue.length > 0) {
|
|
1957
|
+
const id = this.queue.shift();
|
|
1958
|
+
if (id === void 0) return;
|
|
1959
|
+
this.queued.delete(id);
|
|
1960
|
+
const recovery = this.interrupted.delete(id);
|
|
1961
|
+
const execution = Promise.resolve().then(() => this.options.run(id, recovery)).catch((error) => this.options.onExecutionError?.(id, error)).catch(() => {
|
|
1962
|
+
}).finally(() => {
|
|
1963
|
+
this.running.delete(id);
|
|
1964
|
+
this.drain();
|
|
1965
|
+
});
|
|
1966
|
+
this.running.set(id, execution);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
async scanPending() {
|
|
1970
|
+
if (this.stopped) return;
|
|
1971
|
+
const pending = await this.options.loadPending();
|
|
1972
|
+
pending.forEach((id) => this.submit(id));
|
|
1973
|
+
}
|
|
1974
|
+
scheduleScan() {
|
|
1975
|
+
if (this.stopped) return;
|
|
1976
|
+
this.scanTimer = setTimeout(() => {
|
|
1977
|
+
void this.scanPending().catch((error) => this.options.onScanError?.(error)).catch(() => {
|
|
1978
|
+
}).finally(() => this.scheduleScan());
|
|
1979
|
+
}, this.options.scanIntervalMs);
|
|
1980
|
+
this.scanTimer.unref?.();
|
|
1981
|
+
}
|
|
1982
|
+
};
|
|
1983
|
+
|
|
1984
|
+
// src/execution-heartbeat.ts
|
|
1985
|
+
var ExecutionHeartbeatError = class extends Error {
|
|
1986
|
+
static {
|
|
1987
|
+
__name(this, "ExecutionHeartbeatError");
|
|
1988
|
+
}
|
|
1989
|
+
consecutiveFailures;
|
|
1990
|
+
constructor(consecutiveFailures, options) {
|
|
1991
|
+
super(`Execution heartbeat failed ${consecutiveFailures} consecutive times`, options), this.consecutiveFailures = consecutiveFailures;
|
|
1992
|
+
this.name = "ExecutionHeartbeatError";
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
async function runWithExecutionHeartbeat(options, work) {
|
|
1996
|
+
if (!Number.isFinite(options.intervalMs) || options.intervalMs <= 0 || !Number.isSafeInteger(options.failureThreshold) || options.failureThreshold < 1) {
|
|
1997
|
+
throw new RangeError("Heartbeat interval and failure threshold must be positive");
|
|
1998
|
+
}
|
|
1999
|
+
const controller = new AbortController();
|
|
2000
|
+
let stopped = false;
|
|
2001
|
+
let failures = 0;
|
|
2002
|
+
let heartbeatFailure;
|
|
2003
|
+
let timer;
|
|
2004
|
+
let inFlight;
|
|
2005
|
+
const cancel = /* @__PURE__ */ __name(() => {
|
|
2006
|
+
if (timer) clearTimeout(timer);
|
|
2007
|
+
controller.abort(options.signal?.reason);
|
|
2008
|
+
}, "cancel");
|
|
2009
|
+
options.signal?.addEventListener("abort", cancel, {
|
|
2010
|
+
once: true
|
|
2011
|
+
});
|
|
2012
|
+
if (options.signal?.aborted) cancel();
|
|
2013
|
+
const schedule = /* @__PURE__ */ __name(() => {
|
|
2014
|
+
if (stopped || controller.signal.aborted) return;
|
|
2015
|
+
timer = setTimeout(() => {
|
|
2016
|
+
inFlight = tick();
|
|
2017
|
+
}, options.intervalMs);
|
|
2018
|
+
timer.unref?.();
|
|
2019
|
+
}, "schedule");
|
|
2020
|
+
const tick = /* @__PURE__ */ __name(async () => {
|
|
2021
|
+
if (stopped || controller.signal.aborted) return;
|
|
2022
|
+
try {
|
|
2023
|
+
await options.heartbeat();
|
|
2024
|
+
failures = 0;
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
if (!stopped && !controller.signal.aborted) {
|
|
2027
|
+
failures++;
|
|
2028
|
+
if (failures >= options.failureThreshold) {
|
|
2029
|
+
heartbeatFailure = new ExecutionHeartbeatError(failures, {
|
|
2030
|
+
cause: error
|
|
2031
|
+
});
|
|
2032
|
+
controller.abort(heartbeatFailure);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
} finally {
|
|
2036
|
+
schedule();
|
|
2037
|
+
}
|
|
2038
|
+
}, "tick");
|
|
2039
|
+
try {
|
|
2040
|
+
controller.signal.throwIfAborted();
|
|
2041
|
+
schedule();
|
|
2042
|
+
let result;
|
|
2043
|
+
try {
|
|
2044
|
+
result = await work(controller.signal);
|
|
2045
|
+
} catch (error) {
|
|
2046
|
+
throw heartbeatFailure ?? error;
|
|
2047
|
+
}
|
|
2048
|
+
if (heartbeatFailure) throw heartbeatFailure;
|
|
2049
|
+
controller.signal.throwIfAborted();
|
|
2050
|
+
return result;
|
|
2051
|
+
} finally {
|
|
2052
|
+
stopped = true;
|
|
2053
|
+
if (timer) clearTimeout(timer);
|
|
2054
|
+
options.signal?.removeEventListener("abort", cancel);
|
|
2055
|
+
await inFlight;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
__name(runWithExecutionHeartbeat, "runWithExecutionHeartbeat");
|
|
1903
2059
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1904
2060
|
0 && (module.exports = {
|
|
1905
2061
|
DurableTaskLeaseLostError,
|
|
@@ -1907,6 +2063,7 @@ var TaskOutboxDispatcher = class {
|
|
|
1907
2063
|
DurableTaskNotFoundError,
|
|
1908
2064
|
DurableTaskRetryScheduledError,
|
|
1909
2065
|
DurableWorkerRuntime,
|
|
2066
|
+
ExecutionHeartbeatError,
|
|
1910
2067
|
ExponentialTaskRetryPolicy,
|
|
1911
2068
|
InMemoryTaskOutboxRepository,
|
|
1912
2069
|
InMemoryTaskRepository,
|
|
@@ -1917,6 +2074,7 @@ var TaskOutboxDispatcher = class {
|
|
|
1917
2074
|
PostgresTaskSubmitter,
|
|
1918
2075
|
PostgresTaskSummaryReader,
|
|
1919
2076
|
PostgresTaskTimelineReader,
|
|
2077
|
+
SingleProcessTaskSupervisor,
|
|
1920
2078
|
TASK_STATUSES,
|
|
1921
2079
|
TASK_TIMELINE_EVENT_TYPES,
|
|
1922
2080
|
TaskIdempotencyConflictError,
|
|
@@ -1929,5 +2087,6 @@ var TaskOutboxDispatcher = class {
|
|
|
1929
2087
|
canTransitionTask,
|
|
1930
2088
|
createTaskFingerprint,
|
|
1931
2089
|
isTerminalTaskStatus,
|
|
1932
|
-
requiresTaskLease
|
|
2090
|
+
requiresTaskLease,
|
|
2091
|
+
runWithExecutionHeartbeat
|
|
1933
2092
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1849,12 +1849,166 @@ var TaskOutboxDispatcher = class {
|
|
|
1849
1849
|
return result;
|
|
1850
1850
|
}
|
|
1851
1851
|
};
|
|
1852
|
+
|
|
1853
|
+
// src/single-process-task-supervisor.ts
|
|
1854
|
+
var SingleProcessTaskSupervisor = class {
|
|
1855
|
+
static {
|
|
1856
|
+
__name(this, "SingleProcessTaskSupervisor");
|
|
1857
|
+
}
|
|
1858
|
+
options;
|
|
1859
|
+
queued = /* @__PURE__ */ new Set();
|
|
1860
|
+
running = /* @__PURE__ */ new Map();
|
|
1861
|
+
interrupted = /* @__PURE__ */ new Set();
|
|
1862
|
+
queue = [];
|
|
1863
|
+
scanTimer;
|
|
1864
|
+
stopped = false;
|
|
1865
|
+
started = false;
|
|
1866
|
+
constructor(options) {
|
|
1867
|
+
this.options = options;
|
|
1868
|
+
}
|
|
1869
|
+
async onApplicationBootstrap() {
|
|
1870
|
+
if (this.started || this.stopped) return;
|
|
1871
|
+
this.started = true;
|
|
1872
|
+
const interrupted = await this.options.initialize?.() ?? [];
|
|
1873
|
+
interrupted.forEach((id) => this.interrupted.add(id));
|
|
1874
|
+
await this.scanPending();
|
|
1875
|
+
this.scheduleScan();
|
|
1876
|
+
}
|
|
1877
|
+
submit(id) {
|
|
1878
|
+
if (this.stopped || this.queued.has(id) || this.running.has(id)) return;
|
|
1879
|
+
this.queued.add(id);
|
|
1880
|
+
this.queue.push(id);
|
|
1881
|
+
this.drain();
|
|
1882
|
+
}
|
|
1883
|
+
submitRecovery(id) {
|
|
1884
|
+
if (this.stopped || this.queued.has(id) || this.running.has(id)) return;
|
|
1885
|
+
this.interrupted.add(id);
|
|
1886
|
+
this.submit(id);
|
|
1887
|
+
}
|
|
1888
|
+
async waitForIdle() {
|
|
1889
|
+
while (this.queue.length > 0 || this.running.size > 0) {
|
|
1890
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
async onApplicationShutdown() {
|
|
1894
|
+
this.stopped = true;
|
|
1895
|
+
if (this.scanTimer) clearTimeout(this.scanTimer);
|
|
1896
|
+
this.queue = [];
|
|
1897
|
+
this.queued.clear();
|
|
1898
|
+
this.interrupted.clear();
|
|
1899
|
+
for (const id of this.running.keys()) this.options.abort(id);
|
|
1900
|
+
}
|
|
1901
|
+
drain() {
|
|
1902
|
+
while (!this.stopped && this.running.size < this.options.concurrency && this.queue.length > 0) {
|
|
1903
|
+
const id = this.queue.shift();
|
|
1904
|
+
if (id === void 0) return;
|
|
1905
|
+
this.queued.delete(id);
|
|
1906
|
+
const recovery = this.interrupted.delete(id);
|
|
1907
|
+
const execution = Promise.resolve().then(() => this.options.run(id, recovery)).catch((error) => this.options.onExecutionError?.(id, error)).catch(() => {
|
|
1908
|
+
}).finally(() => {
|
|
1909
|
+
this.running.delete(id);
|
|
1910
|
+
this.drain();
|
|
1911
|
+
});
|
|
1912
|
+
this.running.set(id, execution);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
async scanPending() {
|
|
1916
|
+
if (this.stopped) return;
|
|
1917
|
+
const pending = await this.options.loadPending();
|
|
1918
|
+
pending.forEach((id) => this.submit(id));
|
|
1919
|
+
}
|
|
1920
|
+
scheduleScan() {
|
|
1921
|
+
if (this.stopped) return;
|
|
1922
|
+
this.scanTimer = setTimeout(() => {
|
|
1923
|
+
void this.scanPending().catch((error) => this.options.onScanError?.(error)).catch(() => {
|
|
1924
|
+
}).finally(() => this.scheduleScan());
|
|
1925
|
+
}, this.options.scanIntervalMs);
|
|
1926
|
+
this.scanTimer.unref?.();
|
|
1927
|
+
}
|
|
1928
|
+
};
|
|
1929
|
+
|
|
1930
|
+
// src/execution-heartbeat.ts
|
|
1931
|
+
var ExecutionHeartbeatError = class extends Error {
|
|
1932
|
+
static {
|
|
1933
|
+
__name(this, "ExecutionHeartbeatError");
|
|
1934
|
+
}
|
|
1935
|
+
consecutiveFailures;
|
|
1936
|
+
constructor(consecutiveFailures, options) {
|
|
1937
|
+
super(`Execution heartbeat failed ${consecutiveFailures} consecutive times`, options), this.consecutiveFailures = consecutiveFailures;
|
|
1938
|
+
this.name = "ExecutionHeartbeatError";
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
async function runWithExecutionHeartbeat(options, work) {
|
|
1942
|
+
if (!Number.isFinite(options.intervalMs) || options.intervalMs <= 0 || !Number.isSafeInteger(options.failureThreshold) || options.failureThreshold < 1) {
|
|
1943
|
+
throw new RangeError("Heartbeat interval and failure threshold must be positive");
|
|
1944
|
+
}
|
|
1945
|
+
const controller = new AbortController();
|
|
1946
|
+
let stopped = false;
|
|
1947
|
+
let failures = 0;
|
|
1948
|
+
let heartbeatFailure;
|
|
1949
|
+
let timer;
|
|
1950
|
+
let inFlight;
|
|
1951
|
+
const cancel = /* @__PURE__ */ __name(() => {
|
|
1952
|
+
if (timer) clearTimeout(timer);
|
|
1953
|
+
controller.abort(options.signal?.reason);
|
|
1954
|
+
}, "cancel");
|
|
1955
|
+
options.signal?.addEventListener("abort", cancel, {
|
|
1956
|
+
once: true
|
|
1957
|
+
});
|
|
1958
|
+
if (options.signal?.aborted) cancel();
|
|
1959
|
+
const schedule = /* @__PURE__ */ __name(() => {
|
|
1960
|
+
if (stopped || controller.signal.aborted) return;
|
|
1961
|
+
timer = setTimeout(() => {
|
|
1962
|
+
inFlight = tick();
|
|
1963
|
+
}, options.intervalMs);
|
|
1964
|
+
timer.unref?.();
|
|
1965
|
+
}, "schedule");
|
|
1966
|
+
const tick = /* @__PURE__ */ __name(async () => {
|
|
1967
|
+
if (stopped || controller.signal.aborted) return;
|
|
1968
|
+
try {
|
|
1969
|
+
await options.heartbeat();
|
|
1970
|
+
failures = 0;
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
if (!stopped && !controller.signal.aborted) {
|
|
1973
|
+
failures++;
|
|
1974
|
+
if (failures >= options.failureThreshold) {
|
|
1975
|
+
heartbeatFailure = new ExecutionHeartbeatError(failures, {
|
|
1976
|
+
cause: error
|
|
1977
|
+
});
|
|
1978
|
+
controller.abort(heartbeatFailure);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
} finally {
|
|
1982
|
+
schedule();
|
|
1983
|
+
}
|
|
1984
|
+
}, "tick");
|
|
1985
|
+
try {
|
|
1986
|
+
controller.signal.throwIfAborted();
|
|
1987
|
+
schedule();
|
|
1988
|
+
let result;
|
|
1989
|
+
try {
|
|
1990
|
+
result = await work(controller.signal);
|
|
1991
|
+
} catch (error) {
|
|
1992
|
+
throw heartbeatFailure ?? error;
|
|
1993
|
+
}
|
|
1994
|
+
if (heartbeatFailure) throw heartbeatFailure;
|
|
1995
|
+
controller.signal.throwIfAborted();
|
|
1996
|
+
return result;
|
|
1997
|
+
} finally {
|
|
1998
|
+
stopped = true;
|
|
1999
|
+
if (timer) clearTimeout(timer);
|
|
2000
|
+
options.signal?.removeEventListener("abort", cancel);
|
|
2001
|
+
await inFlight;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
__name(runWithExecutionHeartbeat, "runWithExecutionHeartbeat");
|
|
1852
2005
|
export {
|
|
1853
2006
|
DurableTaskLeaseLostError,
|
|
1854
2007
|
DurableTaskNotClaimableError,
|
|
1855
2008
|
DurableTaskNotFoundError,
|
|
1856
2009
|
DurableTaskRetryScheduledError,
|
|
1857
2010
|
DurableWorkerRuntime,
|
|
2011
|
+
ExecutionHeartbeatError,
|
|
1858
2012
|
ExponentialTaskRetryPolicy,
|
|
1859
2013
|
InMemoryTaskOutboxRepository,
|
|
1860
2014
|
InMemoryTaskRepository,
|
|
@@ -1865,6 +2019,7 @@ export {
|
|
|
1865
2019
|
PostgresTaskSubmitter,
|
|
1866
2020
|
PostgresTaskSummaryReader,
|
|
1867
2021
|
PostgresTaskTimelineReader,
|
|
2022
|
+
SingleProcessTaskSupervisor,
|
|
1868
2023
|
TASK_STATUSES,
|
|
1869
2024
|
TASK_TIMELINE_EVENT_TYPES,
|
|
1870
2025
|
TaskIdempotencyConflictError,
|
|
@@ -1877,5 +2032,6 @@ export {
|
|
|
1877
2032
|
canTransitionTask,
|
|
1878
2033
|
createTaskFingerprint,
|
|
1879
2034
|
isTerminalTaskStatus,
|
|
1880
|
-
requiresTaskLease
|
|
2035
|
+
requiresTaskLease,
|
|
2036
|
+
runWithExecutionHeartbeat
|
|
1881
2037
|
};
|