@gobing-ai/ts-infra 0.4.59 → 0.4.62
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 +14 -4
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +3 -0
- package/dist/application/types.d.ts +11 -2
- package/dist/application/types.d.ts.map +1 -1
- package/dist/application-node.d.ts.map +1 -1
- package/dist/application-node.js +34 -4
- package/dist/execution-policy.d.ts +76 -0
- package/dist/execution-policy.d.ts.map +1 -0
- package/dist/execution-policy.js +123 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/job-queue/db-job-queue.d.ts +14 -1
- package/dist/job-queue/db-job-queue.d.ts.map +1 -1
- package/dist/job-queue/db-job-queue.js +165 -26
- package/dist/job-queue/types.d.ts +47 -7
- package/dist/job-queue/types.d.ts.map +1 -1
- package/dist/scheduler/cloudflare.d.ts +4 -0
- package/dist/scheduler/cloudflare.d.ts.map +1 -1
- package/dist/scheduler/cloudflare.js +2 -1
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/node.d.ts +10 -1
- package/dist/scheduler/node.d.ts.map +1 -1
- package/dist/scheduler/node.js +28 -6
- package/dist/scheduler/types.d.ts +20 -3
- package/dist/scheduler/types.d.ts.map +1 -1
- package/dist/scheduler/wrap-handler.d.ts.map +1 -1
- package/dist/scheduler/wrap-handler.js +2 -2
- package/package.json +6 -6
- package/src/application/index.ts +3 -0
- package/src/application/types.ts +16 -3
- package/src/application-node.ts +40 -4
- package/src/execution-policy.ts +168 -0
- package/src/index.ts +3 -1
- package/src/job-queue/db-job-queue.ts +184 -25
- package/src/job-queue/types.ts +47 -7
- package/src/scheduler/cloudflare.ts +3 -1
- package/src/scheduler/index.ts +6 -1
- package/src/scheduler/node.ts +39 -6
- package/src/scheduler/types.ts +34 -5
- package/src/scheduler/wrap-handler.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-infra",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.62",
|
|
4
4
|
"description": "@gobing-ai/ts-infra — Infrastructure backbone: event bus, job queue, scheduler, telemetry, API client, and logging.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -78,12 +78,12 @@
|
|
|
78
78
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-infra-v<version> && git push --tags' && exit 1"
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
|
-
"@gobing-ai/ts-utils": "^0.4.
|
|
81
|
+
"@gobing-ai/ts-utils": "^0.4.62",
|
|
82
82
|
"@logtape/logtape": "^2.0.0"
|
|
83
83
|
},
|
|
84
84
|
"peerDependencies": {
|
|
85
|
-
"@gobing-ai/ts-db": "^0.4.
|
|
86
|
-
"@gobing-ai/ts-runtime": "^0.4.
|
|
85
|
+
"@gobing-ai/ts-db": "^0.4.62",
|
|
86
|
+
"@gobing-ai/ts-runtime": "^0.4.62",
|
|
87
87
|
"@opentelemetry/api": "^1.9.0",
|
|
88
88
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
89
89
|
"@opentelemetry/sdk-metrics": "^2.0.0",
|
|
@@ -116,8 +116,8 @@
|
|
|
116
116
|
}
|
|
117
117
|
},
|
|
118
118
|
"devDependencies": {
|
|
119
|
-
"@gobing-ai/ts-db": "^0.4.
|
|
120
|
-
"@gobing-ai/ts-runtime": "^0.4.
|
|
119
|
+
"@gobing-ai/ts-db": "^0.4.62",
|
|
120
|
+
"@gobing-ai/ts-runtime": "^0.4.62",
|
|
121
121
|
"@types/bun": "1.3.14",
|
|
122
122
|
"@opentelemetry/api": "^1.9.0",
|
|
123
123
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
package/src/application/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { EventBus } from '../event-bus/event-bus';
|
|
|
15
15
|
import { attachFileObserver } from '../event-bus/file-observer';
|
|
16
16
|
import type { BusLifecycleEvents, EventMap } from '../event-bus/types';
|
|
17
17
|
import type { InfraEvents } from '../events';
|
|
18
|
+
import { resolveExecutionTimeoutMs } from '../execution-policy';
|
|
18
19
|
import { getLogger, type Logger } from '../logger';
|
|
19
20
|
import { initScheduler } from '../scheduler/factory';
|
|
20
21
|
import type { SchedulerAdapter } from '../scheduler/types';
|
|
@@ -126,6 +127,8 @@ export async function runApplication<TAppConfig = unknown, TEvents extends Event
|
|
|
126
127
|
enabled: schedOpts?.enabled ?? false,
|
|
127
128
|
autoStart: schedOpts?.autoStart ?? true,
|
|
128
129
|
jobs: schedOpts?.jobs ?? [],
|
|
130
|
+
// Bootstrap-level execution policy (A21): omitted resolves to unlimited.
|
|
131
|
+
timeoutMs: resolveExecutionTimeoutMs('bootstrap.scheduler', schedOpts?.timeoutMs),
|
|
129
132
|
};
|
|
130
133
|
const eventsEnabled = options.config?.events?.enabled ?? true;
|
|
131
134
|
const eventsLifecycle = options.config?.events?.lifecycle ?? true;
|
package/src/application/types.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { FileObserverWriter } from '../event-bus/file-observer';
|
|
|
13
13
|
import type { BusLifecycleEvents, EventMap } from '../event-bus/types';
|
|
14
14
|
import type { InfraEvents } from '../events';
|
|
15
15
|
import type { Logger, LogLevel } from '../logger';
|
|
16
|
-
import type { SchedulerAdapter, SchedulerJobConfig } from '../scheduler/types';
|
|
16
|
+
import type { ScheduledAction, SchedulerAdapter, SchedulerJobConfig } from '../scheduler/types';
|
|
17
17
|
import type { PluginHost } from './plugins/host';
|
|
18
18
|
import type { Plugin } from './plugins/types';
|
|
19
19
|
|
|
@@ -81,7 +81,7 @@ export interface SchedulerOptions {
|
|
|
81
81
|
/** Injected adapter (skips noop default when provided). */
|
|
82
82
|
adapter?: SchedulerAdapter;
|
|
83
83
|
/** Cron entries to register: `[cron, action][]`. */
|
|
84
|
-
entries?: Array<[string,
|
|
84
|
+
entries?: Array<[string, ScheduledAction]>;
|
|
85
85
|
/** Start scheduler immediately after registration. Default `true` when enabled. */
|
|
86
86
|
autoStart?: boolean;
|
|
87
87
|
/**
|
|
@@ -91,6 +91,13 @@ export interface SchedulerOptions {
|
|
|
91
91
|
* job `command` itself.
|
|
92
92
|
*/
|
|
93
93
|
jobs?: readonly SchedulerJobConfig[];
|
|
94
|
+
/**
|
|
95
|
+
* Bootstrap-level execution policy (A21): a positive integer ms deadline
|
|
96
|
+
* or explicit `null` = unlimited; omitted resolves to unlimited. Per-job
|
|
97
|
+
* `timeoutMs` overrides this default through the shared first-match-wins
|
|
98
|
+
* resolution.
|
|
99
|
+
*/
|
|
100
|
+
timeoutMs?: number | null;
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
// ── Resolved bootstrap config ─────────────────────────────────────────────
|
|
@@ -113,7 +120,13 @@ export interface ApplicationBootstrapConfig {
|
|
|
113
120
|
filePath?: string;
|
|
114
121
|
};
|
|
115
122
|
readonly telemetry: { enabled: boolean; serviceName: string; environment: string; dbStatementDebug: boolean };
|
|
116
|
-
readonly scheduler: {
|
|
123
|
+
readonly scheduler: {
|
|
124
|
+
enabled: boolean;
|
|
125
|
+
autoStart: boolean;
|
|
126
|
+
jobs: readonly SchedulerJobConfig[];
|
|
127
|
+
/** Resolved bootstrap execution policy; `null` = unlimited. */
|
|
128
|
+
timeoutMs: number | null;
|
|
129
|
+
};
|
|
117
130
|
}
|
|
118
131
|
|
|
119
132
|
// ── Injected services ─────────────────────────────────────────────────────
|
package/src/application-node.ts
CHANGED
|
@@ -39,6 +39,7 @@ import type {
|
|
|
39
39
|
SchedulerOptions,
|
|
40
40
|
TelemetryOptions,
|
|
41
41
|
} from './application/types';
|
|
42
|
+
import { resolveExecutionTimeoutMs } from './execution-policy';
|
|
42
43
|
import { parseCronExpression } from './scheduler/cron';
|
|
43
44
|
import type { SchedulerJobConfig } from './scheduler/types';
|
|
44
45
|
import { NodeSchedulerAdapter } from './scheduler-node';
|
|
@@ -97,7 +98,22 @@ function validateAppConfig<TAppConfig>(
|
|
|
97
98
|
throw new ConfigValidationError(`Unsupported validator shape for section "${section}"`);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
// ── Scheduler job validation (task 0734)
|
|
101
|
+
// ── Scheduler job validation (task 0734) ────────────────────────────
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve and validate the bootstrap-level scheduler execution policy (A21),
|
|
105
|
+
* rethrowing invalid values as {@link ConfigValidationError} with the exact
|
|
106
|
+
* config path so startup aborts before the user `start` callback.
|
|
107
|
+
*/
|
|
108
|
+
function resolveBootstrapSchedulerTimeout(value: number | null | undefined): number | null {
|
|
109
|
+
try {
|
|
110
|
+
return resolveExecutionTimeoutMs('bootstrap.scheduler', value);
|
|
111
|
+
} catch {
|
|
112
|
+
throw new ConfigValidationError(
|
|
113
|
+
`bootstrap.scheduler.timeoutMs must be a positive integer or null; received ${String(value)}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
101
117
|
|
|
102
118
|
/** Max `intervalMinutes` so that `intervalMinutes * 60_000` fits in the platform timer maximum. */
|
|
103
119
|
const MAX_INTERVAL_MINUTES = Math.floor(MAX_TIMEOUT_MS / 60_000);
|
|
@@ -131,6 +147,22 @@ function normalizeSchedulerJobs(raw: unknown): readonly SchedulerJobConfig[] {
|
|
|
131
147
|
const command = typeof entry.command === 'string' ? entry.command.trim() : '';
|
|
132
148
|
const cron = typeof entry.cron === 'string' ? entry.cron.trim() : '';
|
|
133
149
|
const interval = entry.intervalMinutes;
|
|
150
|
+
// Per-job execution policy (A21): undefined inherits, explicit null is
|
|
151
|
+
// unlimited, otherwise a positive integer. Validated here so a bad
|
|
152
|
+
// value aborts startup with the exact job path.
|
|
153
|
+
const rawTimeout = entry.timeoutMs;
|
|
154
|
+
let timeoutMs: number | null | undefined;
|
|
155
|
+
if (rawTimeout !== undefined) {
|
|
156
|
+
if (
|
|
157
|
+
rawTimeout !== null &&
|
|
158
|
+
(typeof rawTimeout !== 'number' || !Number.isInteger(rawTimeout) || rawTimeout <= 0)
|
|
159
|
+
) {
|
|
160
|
+
throw new ConfigValidationError(
|
|
161
|
+
`bootstrap.scheduler.jobs.${index}.timeoutMs must be a positive integer or null`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
timeoutMs = rawTimeout as number | null;
|
|
165
|
+
}
|
|
134
166
|
|
|
135
167
|
if (name === '') {
|
|
136
168
|
throw new ConfigValidationError(`bootstrap.scheduler.jobs.${index}.name must be a non-empty string`);
|
|
@@ -165,7 +197,7 @@ function normalizeSchedulerJobs(raw: unknown): readonly SchedulerJobConfig[] {
|
|
|
165
197
|
`bootstrap.scheduler.jobs.${index}.intervalMinutes must be an integer in 1..${MAX_INTERVAL_MINUTES}`,
|
|
166
198
|
);
|
|
167
199
|
}
|
|
168
|
-
jobs.push({ name, command, intervalMinutes: interval as number });
|
|
200
|
+
jobs.push({ name, command, intervalMinutes: interval as number, timeoutMs });
|
|
169
201
|
} else {
|
|
170
202
|
if (cron === '') {
|
|
171
203
|
throw new ConfigValidationError(`bootstrap.scheduler.jobs.${index}.cron must be a non-empty string`);
|
|
@@ -176,7 +208,7 @@ function normalizeSchedulerJobs(raw: unknown): readonly SchedulerJobConfig[] {
|
|
|
176
208
|
const detail = error instanceof Error ? error.message : String(error);
|
|
177
209
|
throw new ConfigValidationError(`bootstrap.scheduler.jobs.${index}.cron: ${detail}`);
|
|
178
210
|
}
|
|
179
|
-
jobs.push({ name, command, cron });
|
|
211
|
+
jobs.push({ name, command, cron, timeoutMs });
|
|
180
212
|
}
|
|
181
213
|
});
|
|
182
214
|
|
|
@@ -369,9 +401,13 @@ export async function runNodeApplication<TAppConfig = unknown, TEvents extends E
|
|
|
369
401
|
enabled: schedulerOpts.enabled === true,
|
|
370
402
|
autoStart: schedulerOpts.autoStart,
|
|
371
403
|
jobs: normalizeSchedulerJobs((schedulerOpts as Partial<SchedulerOptions>).jobs),
|
|
404
|
+
// Bootstrap-level execution policy (A21): validated with the exact
|
|
405
|
+
// config path so a bad value aborts startup before the user callback.
|
|
406
|
+
timeoutMs: resolveBootstrapSchedulerTimeout(schedulerOpts.timeoutMs),
|
|
372
407
|
};
|
|
373
408
|
if (schedulerConfig.enabled) {
|
|
374
|
-
schedulerConfig.adapter =
|
|
409
|
+
schedulerConfig.adapter =
|
|
410
|
+
schedulerOpts.adapter ?? new NodeSchedulerAdapter({ timeoutMs: schedulerConfig.timeoutMs });
|
|
375
411
|
if (schedulerOpts.entries) {
|
|
376
412
|
schedulerConfig.entries = schedulerOpts.entries;
|
|
377
413
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared execution-deadline policy for background work (feature A21).
|
|
3
|
+
*
|
|
4
|
+
* One nullable policy (`number | null`) and one clock per execution: a finite
|
|
5
|
+
* deadline arms a single timer that requests cancellation through an
|
|
6
|
+
* `AbortController`; the executor always awaits the handler's settlement, even
|
|
7
|
+
* when the handler ignores the abort signal. Both the queue consumer and the
|
|
8
|
+
* Node scheduler run their work through this seam so deadline, cancellation,
|
|
9
|
+
* and timing semantics stay identical across surfaces.
|
|
10
|
+
*
|
|
11
|
+
* Resolution is first-match-wins down the scope chain: an explicit job value
|
|
12
|
+
* (including explicit `null` = unlimited) beats the consumer default; absence
|
|
13
|
+
* (`undefined`) inherits; an absent inherited value is unlimited. A scope
|
|
14
|
+
* never *negates* an inherited finite deadline by being omitted — only an
|
|
15
|
+
* explicit `null` opts out.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Execution deadline policy: positive integer milliseconds, or `null` = unlimited. */
|
|
19
|
+
export type ExecutionDeadlineMs = number | null;
|
|
20
|
+
|
|
21
|
+
/** Cancellation context handed to job and scheduler handlers. */
|
|
22
|
+
export interface ExecutionContext {
|
|
23
|
+
/** Fires when the deadline expires or the caller's signal cancels first. */
|
|
24
|
+
readonly signal: AbortSignal;
|
|
25
|
+
/** The resolved policy for this execution; `null` means unlimited. */
|
|
26
|
+
readonly deadlineMs: ExecutionDeadlineMs;
|
|
27
|
+
/**
|
|
28
|
+
* Why cancellation was requested: `'timeout'` when the execution deadline
|
|
29
|
+
* expired, `'cancelled'` when the caller's signal fired first. `undefined`
|
|
30
|
+
* while the work is not (yet) being cancelled.
|
|
31
|
+
*/
|
|
32
|
+
readonly cancellationReason: 'timeout' | 'cancelled' | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Outcome of an execution run through {@link runWithExecutionDeadline}. */
|
|
36
|
+
export interface ExecutionOutcome {
|
|
37
|
+
/** `'timeout'` and `'cancelled'` still mean the handler settled (or was awaited to settlement). */
|
|
38
|
+
outcome: 'completed' | 'timeout' | 'cancelled' | 'error';
|
|
39
|
+
/** The handler's error when `outcome === 'error'`. */
|
|
40
|
+
error?: unknown;
|
|
41
|
+
elapsedMs: number;
|
|
42
|
+
/** True when the deadline expired, even if the handler ignored the abort and finished late. */
|
|
43
|
+
timedOut: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Options for {@link runWithExecutionDeadline}. */
|
|
47
|
+
export interface ExecutionDeadlineOptions {
|
|
48
|
+
/** Resolved deadline for this run; `undefined` is treated as unlimited. */
|
|
49
|
+
timeoutMs?: number | null;
|
|
50
|
+
/** Caller-controlled cancellation composed into the same controller. */
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the leaf execution policy for one scope.
|
|
56
|
+
*
|
|
57
|
+
* `value === undefined` inherits `inherited` (itself `null` when absent);
|
|
58
|
+
* `value === null` explicitly opts out of this scope's deadline; a positive
|
|
59
|
+
* integer wins outright. Anything else is a configuration error.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveExecutionTimeoutMs(
|
|
62
|
+
scope: string,
|
|
63
|
+
value: number | null | undefined,
|
|
64
|
+
inherited?: number | null,
|
|
65
|
+
): number | null {
|
|
66
|
+
if (value === undefined) {
|
|
67
|
+
return inherited ?? null;
|
|
68
|
+
}
|
|
69
|
+
if (value === null) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
|
|
73
|
+
throw new RangeError(`${scope} timeoutMs must be a positive integer or null; received ${String(value)}`);
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A reusable unlimited context for callers that execute actions without
|
|
80
|
+
* arming a ts-infra deadline — notably the Cloudflare adapter, whose ticks
|
|
81
|
+
* run under the Workers runtime's own limits (A21).
|
|
82
|
+
*/
|
|
83
|
+
export function unlimitedExecutionContext(): ExecutionContext {
|
|
84
|
+
return {
|
|
85
|
+
signal: new AbortController().signal,
|
|
86
|
+
deadlineMs: null,
|
|
87
|
+
cancellationReason: undefined,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run `action` under one shared deadline clock.
|
|
93
|
+
*
|
|
94
|
+
* A finite `timeoutMs` arms exactly one timer that aborts the context's
|
|
95
|
+
* controller (`cancellationReason: 'timeout'`); an aborted caller signal
|
|
96
|
+
* aborts the same controller (`cancellationReason: 'cancelled'`). The handler
|
|
97
|
+
* is always awaited — including after abort, so an uncooperative handler that
|
|
98
|
+
* finishes late is still reported as timed out, never as a successful
|
|
99
|
+
* cancellation — and handler rejections surface as `{ outcome: 'error' }`.
|
|
100
|
+
*/
|
|
101
|
+
export async function runWithExecutionDeadline<T>(
|
|
102
|
+
action: (context: ExecutionContext) => Promise<T>,
|
|
103
|
+
options: ExecutionDeadlineOptions,
|
|
104
|
+
): Promise<ExecutionOutcome & { result?: T }> {
|
|
105
|
+
const deadlineMs = options.timeoutMs ?? null;
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
const cancellation: { reason: 'timeout' | 'cancelled' | undefined } = { reason: undefined };
|
|
108
|
+
|
|
109
|
+
const context: ExecutionContext = {
|
|
110
|
+
signal: controller.signal,
|
|
111
|
+
deadlineMs,
|
|
112
|
+
get cancellationReason() {
|
|
113
|
+
return cancellation.reason;
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (options.signal !== undefined) {
|
|
118
|
+
if (options.signal.aborted) {
|
|
119
|
+
cancellation.reason = 'cancelled';
|
|
120
|
+
controller.abort();
|
|
121
|
+
} else {
|
|
122
|
+
const abortFromCaller = (): void => {
|
|
123
|
+
if (cancellation.reason === undefined) cancellation.reason = 'cancelled';
|
|
124
|
+
controller.abort();
|
|
125
|
+
};
|
|
126
|
+
options.signal.addEventListener('abort', abortFromCaller, { once: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const startMs = performance.now();
|
|
131
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
132
|
+
if (deadlineMs !== null) {
|
|
133
|
+
timer = setTimeout(() => {
|
|
134
|
+
if (cancellation.reason === undefined) cancellation.reason = 'timeout';
|
|
135
|
+
controller.abort();
|
|
136
|
+
}, deadlineMs);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let result: T | undefined;
|
|
140
|
+
let error: unknown;
|
|
141
|
+
let failed = false;
|
|
142
|
+
try {
|
|
143
|
+
result = await action(context);
|
|
144
|
+
} catch (caught) {
|
|
145
|
+
error = caught;
|
|
146
|
+
failed = true;
|
|
147
|
+
} finally {
|
|
148
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const elapsedMs = performance.now() - startMs;
|
|
152
|
+
const timedOut = cancellation.reason === 'timeout';
|
|
153
|
+
if (failed) {
|
|
154
|
+
return { outcome: 'error', error, elapsedMs, timedOut };
|
|
155
|
+
}
|
|
156
|
+
if (timedOut) {
|
|
157
|
+
return {
|
|
158
|
+
outcome: 'timeout',
|
|
159
|
+
error: new Error(`execution deadline (${String(deadlineMs)}ms) expired`),
|
|
160
|
+
elapsedMs,
|
|
161
|
+
timedOut,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (cancellation.reason === 'cancelled') {
|
|
165
|
+
return { outcome: 'cancelled', elapsedMs, timedOut };
|
|
166
|
+
}
|
|
167
|
+
return { outcome: 'completed', result, elapsedMs, timedOut };
|
|
168
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -43,7 +43,9 @@ export type {
|
|
|
43
43
|
SchedulerJobExecutedDetail,
|
|
44
44
|
WithEventSeverity,
|
|
45
45
|
} from './events';
|
|
46
|
-
|
|
46
|
+
// Execution policy (A21) — shared deadline/cancellation seam for background work
|
|
47
|
+
export type { ExecutionContext, ExecutionDeadlineMs, ExecutionOutcome } from './execution-policy';
|
|
48
|
+
export { resolveExecutionTimeoutMs, runWithExecutionDeadline } from './execution-policy';
|
|
47
49
|
export type {
|
|
48
50
|
EnqueueOptions,
|
|
49
51
|
Job,
|