@zintrust/core 2.4.3 → 2.4.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/package.json +9 -1
- package/src/index.js +3 -3
- package/src/runtime/BackgroundTaskScheduler.d.ts +39 -0
- package/src/runtime/BackgroundTaskScheduler.d.ts.map +1 -0
- package/src/runtime/BackgroundTaskScheduler.js +105 -0
- package/src/tasks.d.ts +2 -0
- package/src/tasks.d.ts.map +1 -0
- package/src/tasks.js +1 -0
- package/src/worker-runtime/drain.d.ts +18 -0
- package/src/worker-runtime/drain.d.ts.map +1 -0
- package/src/worker-runtime/drain.js +152 -0
- package/src/worker-runtime/install-enqueue-hook.d.ts +17 -0
- package/src/worker-runtime/install-enqueue-hook.d.ts.map +1 -0
- package/src/worker-runtime/install-enqueue-hook.js +8 -0
- package/src/worker-runtime/ping.d.ts +40 -0
- package/src/worker-runtime/ping.d.ts.map +1 -0
- package/src/worker-runtime/ping.js +82 -0
- package/src/worker-runtime/processor-registry.d.ts +22 -0
- package/src/worker-runtime/processor-registry.d.ts.map +1 -0
- package/src/worker-runtime/processor-registry.js +24 -0
- package/src/worker-runtime/rpc-client.d.ts +23 -0
- package/src/worker-runtime/rpc-client.d.ts.map +1 -0
- package/src/worker-runtime/rpc-client.js +59 -0
- package/src/worker-runtime.d.ts +5 -0
- package/src/worker-runtime.d.ts.map +1 -0
- package/src/worker-runtime.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/core",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.5",
|
|
4
4
|
"description": "Production-grade TypeScript backend framework for JavaScript",
|
|
5
5
|
"homepage": "https://zintrust.com",
|
|
6
6
|
"repository": {
|
|
@@ -202,6 +202,14 @@
|
|
|
202
202
|
"types": "./src/socket-index.d.ts",
|
|
203
203
|
"import": "./src/socket-index.js"
|
|
204
204
|
},
|
|
205
|
+
"./tasks": {
|
|
206
|
+
"types": "./src/tasks.d.ts",
|
|
207
|
+
"import": "./src/tasks.js"
|
|
208
|
+
},
|
|
209
|
+
"./worker-runtime": {
|
|
210
|
+
"types": "./src/worker-runtime.d.ts",
|
|
211
|
+
"import": "./src/worker-runtime.js"
|
|
212
|
+
},
|
|
205
213
|
"./package.json": "./package.json"
|
|
206
214
|
},
|
|
207
215
|
"dependencies": {
|
package/src/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @zintrust/core v2.4.
|
|
2
|
+
* @zintrust/core v2.4.5
|
|
3
3
|
*
|
|
4
4
|
* ZinTrust Framework - Production-Grade TypeScript Backend
|
|
5
5
|
* Built for performance, type safety, and exceptional developer experience
|
|
6
6
|
*
|
|
7
7
|
* Build Information:
|
|
8
|
-
* Built: 2026-06-
|
|
8
|
+
* Built: 2026-06-01T13:37:05.393Z
|
|
9
9
|
* Node: >=20.0.0
|
|
10
10
|
* License: MIT
|
|
11
11
|
*
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* Available at runtime for debugging and health checks
|
|
22
22
|
*/
|
|
23
23
|
export const ZINTRUST_VERSION = '0.1.41';
|
|
24
|
-
export const ZINTRUST_BUILD_DATE = '2026-06-
|
|
24
|
+
export const ZINTRUST_BUILD_DATE = '2026-06-01T13:37:05.359Z'; // Replaced during build
|
|
25
25
|
export { Application } from './boot/Application.js';
|
|
26
26
|
export { AwsSigV4 } from './common/index.js';
|
|
27
27
|
export { SignedRequest } from './security/SignedRequest.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BackgroundTaskScheduler — runtime-agnostic background task scheduling.
|
|
3
|
+
* Maps to ctx.waitUntil() in Cloudflare Workers, normal promises in Node.
|
|
4
|
+
*/
|
|
5
|
+
type ExecutionContext = {
|
|
6
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
7
|
+
};
|
|
8
|
+
type BackgroundTaskScheduler = {
|
|
9
|
+
schedule(task: Promise<void>): void;
|
|
10
|
+
isAvailable(): boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare const BackgroundTaskScheduler: Readonly<{
|
|
13
|
+
/**
|
|
14
|
+
* Set the Worker execution context for waitUntil support.
|
|
15
|
+
* Call this from your Cloudflare Worker fetch handler:
|
|
16
|
+
* BackgroundTaskScheduler.setExecutionContext(ctx);
|
|
17
|
+
*/
|
|
18
|
+
setExecutionContext(ctx: ExecutionContext): void;
|
|
19
|
+
/**
|
|
20
|
+
* Get the current scheduler instance.
|
|
21
|
+
*/
|
|
22
|
+
getScheduler(): BackgroundTaskScheduler;
|
|
23
|
+
/**
|
|
24
|
+
* Schedule a background task.
|
|
25
|
+
* In Workers: uses ctx.waitUntil() if context is set
|
|
26
|
+
* In Node: fire-and-forget with error suppression
|
|
27
|
+
*/
|
|
28
|
+
schedule(task: Promise<void>): void;
|
|
29
|
+
/**
|
|
30
|
+
* Check if a proper scheduler is available (i.e., Workers context is set).
|
|
31
|
+
*/
|
|
32
|
+
isAvailable(): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Reset the scheduler (useful for testing).
|
|
35
|
+
*/
|
|
36
|
+
reset(): void;
|
|
37
|
+
}>;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=BackgroundTaskScheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BackgroundTaskScheduler.d.ts","sourceRoot":"","sources":["../../../src/runtime/BackgroundTaskScheduler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,KAAK,gBAAgB,GAAG;IACtB,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;CAC5C,CAAC;AAEF,KAAK,uBAAuB,GAAG;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpC,WAAW,IAAI,OAAO,CAAC;CACxB,CAAC;AA4DF,eAAO,MAAM,uBAAuB;IAClC;;;;OAIG;6BACsB,gBAAgB,GAAG,IAAI;IAKhD;;OAEG;oBACa,uBAAuB;IAmBvC;;;;OAIG;mBACY,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAInC;;OAEG;mBACY,OAAO;IAItB;;OAEG;aACM,IAAI;EAIb,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BackgroundTaskScheduler — runtime-agnostic background task scheduling.
|
|
3
|
+
* Maps to ctx.waitUntil() in Cloudflare Workers, normal promises in Node.
|
|
4
|
+
*/
|
|
5
|
+
let _executionContext;
|
|
6
|
+
let _scheduler;
|
|
7
|
+
const isCloudflareRuntime = () => {
|
|
8
|
+
const globalRef = typeof globalThis === 'undefined' ? undefined : globalThis;
|
|
9
|
+
if (!globalRef)
|
|
10
|
+
return false;
|
|
11
|
+
// @ts-ignore - navigator is available in workers
|
|
12
|
+
if (typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers') {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return (globalRef.caches !== undefined ||
|
|
16
|
+
typeof globalRef.WebSocketPair === 'function' ||
|
|
17
|
+
globalRef.CF !== undefined);
|
|
18
|
+
};
|
|
19
|
+
const createWorkerScheduler = (ctx) => {
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
schedule(task) {
|
|
22
|
+
ctx.waitUntil(task);
|
|
23
|
+
},
|
|
24
|
+
isAvailable() {
|
|
25
|
+
return true;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
const createNodeScheduler = () => {
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
schedule(task) {
|
|
32
|
+
// In Node, fire-and-forget is acceptable since the process doesn't terminate
|
|
33
|
+
// like Workers do. We still catch errors to prevent unhandled rejections.
|
|
34
|
+
task.catch(() => {
|
|
35
|
+
// Silently ignore background task errors to avoid noise
|
|
36
|
+
// The individual watchers handle their own error logging
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
isAvailable() {
|
|
40
|
+
return true;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
const createFallbackScheduler = () => {
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
schedule(task) {
|
|
47
|
+
// Fallback: fire-and-forget with error suppression
|
|
48
|
+
task.catch(() => undefined);
|
|
49
|
+
},
|
|
50
|
+
isAvailable() {
|
|
51
|
+
return false;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
export const BackgroundTaskScheduler = Object.freeze({
|
|
56
|
+
/**
|
|
57
|
+
* Set the Worker execution context for waitUntil support.
|
|
58
|
+
* Call this from your Cloudflare Worker fetch handler:
|
|
59
|
+
* BackgroundTaskScheduler.setExecutionContext(ctx);
|
|
60
|
+
*/
|
|
61
|
+
setExecutionContext(ctx) {
|
|
62
|
+
_executionContext = ctx;
|
|
63
|
+
_scheduler = createWorkerScheduler(ctx);
|
|
64
|
+
},
|
|
65
|
+
/**
|
|
66
|
+
* Get the current scheduler instance.
|
|
67
|
+
*/
|
|
68
|
+
getScheduler() {
|
|
69
|
+
if (_scheduler)
|
|
70
|
+
return _scheduler;
|
|
71
|
+
if (_executionContext) {
|
|
72
|
+
_scheduler = createWorkerScheduler(_executionContext);
|
|
73
|
+
return _scheduler;
|
|
74
|
+
}
|
|
75
|
+
if (isCloudflareRuntime()) {
|
|
76
|
+
// We're in Workers but no context was set - use fallback
|
|
77
|
+
_scheduler = createFallbackScheduler();
|
|
78
|
+
return _scheduler;
|
|
79
|
+
}
|
|
80
|
+
// Node.js or other runtime
|
|
81
|
+
_scheduler = createNodeScheduler();
|
|
82
|
+
return _scheduler;
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* Schedule a background task.
|
|
86
|
+
* In Workers: uses ctx.waitUntil() if context is set
|
|
87
|
+
* In Node: fire-and-forget with error suppression
|
|
88
|
+
*/
|
|
89
|
+
schedule(task) {
|
|
90
|
+
this.getScheduler().schedule(task);
|
|
91
|
+
},
|
|
92
|
+
/**
|
|
93
|
+
* Check if a proper scheduler is available (i.e., Workers context is set).
|
|
94
|
+
*/
|
|
95
|
+
isAvailable() {
|
|
96
|
+
return this.getScheduler().isAvailable();
|
|
97
|
+
},
|
|
98
|
+
/**
|
|
99
|
+
* Reset the scheduler (useful for testing).
|
|
100
|
+
*/
|
|
101
|
+
reset() {
|
|
102
|
+
_executionContext = undefined;
|
|
103
|
+
_scheduler = undefined;
|
|
104
|
+
},
|
|
105
|
+
});
|
package/src/tasks.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/tasks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC"}
|
package/src/tasks.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BackgroundTaskScheduler } from './runtime/BackgroundTaskScheduler.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WorkerModule } from './processor-registry';
|
|
2
|
+
export type AppWorkerDefinition = Readonly<{
|
|
3
|
+
name: string;
|
|
4
|
+
queueName: string;
|
|
5
|
+
version: string;
|
|
6
|
+
autoStart: boolean;
|
|
7
|
+
activeStatus: boolean;
|
|
8
|
+
concurrency: number;
|
|
9
|
+
processorSpec: string;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Ensure a drain loop is running. Returns immediately. If a loop is already active this is
|
|
13
|
+
* a no-op (concurrent-PING guard). The returned promise resolves when the active loop ends
|
|
14
|
+
* and is suitable for `ctx.waitUntil`.
|
|
15
|
+
*/
|
|
16
|
+
export declare const ensureDraining: (appWorkerDefinitions: AppWorkerDefinition[], workerModules: ReadonlyArray<WorkerModule>) => Promise<void>;
|
|
17
|
+
export declare const isDraining: () => boolean;
|
|
18
|
+
//# sourceMappingURL=drain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drain.d.ts","sourceRoot":"","sources":["../../../src/worker-runtime/drain.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAgCvE,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAAC;AAsKH;;;;GAIG;AACH,eAAO,MAAM,cAAc,GACzB,sBAAsB,mBAAmB,EAAE,EAC3C,eAAe,aAAa,CAAC,YAAY,CAAC,KACzC,OAAO,CAAC,IAAI,CAUd,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,OAA+B,CAAC"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker drain loop.
|
|
3
|
+
*
|
|
4
|
+
* The Cloudflare Worker is not an always-on daemon, so it does not hold persistent BullMQ
|
|
5
|
+
* consumers. Instead it is woken by a PING (on enqueue, or by cron) and then drains
|
|
6
|
+
* available jobs: pull an atomically-claimed job over HTTP RPC, run the bundled processor,
|
|
7
|
+
* and ack/fail it back over RPC. The whole drain runs inside the Worker so it executes the
|
|
8
|
+
* app's real business logic with all bindings/code already bundled.
|
|
9
|
+
*
|
|
10
|
+
* A single drain loop runs at a time (guard). A PING that arrives while a loop is active is
|
|
11
|
+
* a no-op for the loop (the caller still gets an immediate ack).
|
|
12
|
+
*/
|
|
13
|
+
import { Env } from '../config/env.js';
|
|
14
|
+
import { Logger } from '../config/logger.js';
|
|
15
|
+
import { isNullish } from '../helper/index.js';
|
|
16
|
+
import { resolveProcessor } from './processor-registry.js';
|
|
17
|
+
import { ackJob, failJob, isRedisRpcConfigured, listWorkers, pullJob, } from './rpc-client.js';
|
|
18
|
+
let activeDrain = null;
|
|
19
|
+
const sleep = async (ms) => new Promise((resolve) => globalThis.setTimeout(resolve, ms));
|
|
20
|
+
const intEnv = (key, fallback) => {
|
|
21
|
+
const parsed = Number.parseInt(Env.get(key, '').trim(), 10);
|
|
22
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Resolve which queues this Worker should drain: app manifest workers that have a bundled
|
|
26
|
+
* processor and are not disabled. The backend lifecycle registry is authoritative for
|
|
27
|
+
* start/stop — a worker explicitly `stopped` there is skipped; otherwise the manifest's
|
|
28
|
+
* `autoStart` decides.
|
|
29
|
+
*/
|
|
30
|
+
const resolveDrainTargets = async (appWorkerDefinitions, workerModules) => {
|
|
31
|
+
let statusByQueue = new Map();
|
|
32
|
+
try {
|
|
33
|
+
const registered = await listWorkers();
|
|
34
|
+
statusByQueue = new Map(registered
|
|
35
|
+
.filter((entry) => typeof entry.queueName === 'string' && entry.source === 'redis-rpc-registry')
|
|
36
|
+
.map((entry) => [entry.queueName, entry.status ?? 'stopped']));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
Logger.debug('[worker-runtime] worker registry unavailable; falling back to manifest autoStart', error);
|
|
40
|
+
}
|
|
41
|
+
const targets = [];
|
|
42
|
+
for (const definition of appWorkerDefinitions) {
|
|
43
|
+
if (definition.activeStatus === false)
|
|
44
|
+
continue;
|
|
45
|
+
if (!resolveProcessor(definition.processorSpec, workerModules))
|
|
46
|
+
continue;
|
|
47
|
+
const status = statusByQueue.get(definition.queueName);
|
|
48
|
+
const enabled = status === undefined ? definition.autoStart === true : status === 'running';
|
|
49
|
+
if (enabled) {
|
|
50
|
+
targets.push({ queueName: definition.queueName, processorSpec: definition.processorSpec });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return targets;
|
|
54
|
+
};
|
|
55
|
+
const toJob = (queueName, pulled) => {
|
|
56
|
+
return {
|
|
57
|
+
id: pulled.id,
|
|
58
|
+
name: pulled.name ?? 'default',
|
|
59
|
+
data: pulled.payload,
|
|
60
|
+
queueName,
|
|
61
|
+
attemptsMade: pulled.attempts,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
const getDrainLoopConfig = () => ({
|
|
65
|
+
deadline: Date.now() + intEnv('WORKER_DRAIN_MAX_MS', 300_000),
|
|
66
|
+
idleSleepMs: intEnv('WORKER_DRAIN_IDLE_SLEEP_MS', 1_000),
|
|
67
|
+
maxIdleCycles: intEnv('WORKER_DRAIN_MAX_IDLE_CYCLES', 5),
|
|
68
|
+
visibilityTimeoutMs: intEnv('WORKER_DRAIN_VISIBILITY_MS', 30_000),
|
|
69
|
+
});
|
|
70
|
+
/** Pull and process a single job for a queue. Returns true if a job was handled. */
|
|
71
|
+
const drainOneJob = async (target, visibilityTimeoutMs, workerModules) => {
|
|
72
|
+
const pulled = await pullJob(target.queueName, visibilityTimeoutMs);
|
|
73
|
+
if (!pulled)
|
|
74
|
+
return false;
|
|
75
|
+
const processor = resolveProcessor(target.processorSpec, workerModules);
|
|
76
|
+
if (!processor) {
|
|
77
|
+
await failJob(target.queueName, pulled.id, `No bundled processor for ${target.processorSpec}`);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const returnValue = await processor(toJob(target.queueName, pulled));
|
|
82
|
+
await ackJob(target.queueName, pulled.id, returnValue);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
86
|
+
Logger.error('[worker-runtime] job processing failed', {
|
|
87
|
+
queue: target.queueName,
|
|
88
|
+
jobId: pulled.id,
|
|
89
|
+
reason,
|
|
90
|
+
});
|
|
91
|
+
await failJob(target.queueName, pulled.id, reason);
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
};
|
|
95
|
+
const drainTargetQueue = async (target, visibilityTimeoutMs, workerModules, deadline) => {
|
|
96
|
+
let handledCount = 0;
|
|
97
|
+
while (Date.now() < deadline) {
|
|
98
|
+
// eslint-disable-next-line no-await-in-loop
|
|
99
|
+
const handled = await drainOneJob(target, visibilityTimeoutMs, workerModules).catch((error) => {
|
|
100
|
+
Logger.warn('[worker-runtime] drainOneJob error', { queue: target.queueName, error });
|
|
101
|
+
return false;
|
|
102
|
+
});
|
|
103
|
+
if (!handled)
|
|
104
|
+
break;
|
|
105
|
+
handledCount += 1;
|
|
106
|
+
}
|
|
107
|
+
return handledCount;
|
|
108
|
+
};
|
|
109
|
+
const drainTargets = async (targets, visibilityTimeoutMs, workerModules, deadline) => {
|
|
110
|
+
const handledCounts = await Promise.all(targets.map(async (target) => drainTargetQueue(target, visibilityTimeoutMs, workerModules, deadline)));
|
|
111
|
+
return handledCounts.reduce((sum, handledCount) => sum + handledCount, 0);
|
|
112
|
+
};
|
|
113
|
+
const runDrainLoop = async (appWorkerDefinitions, workerModules) => {
|
|
114
|
+
if (!Env.getBool('WORKER_ENABLED', false) || !isRedisRpcConfigured()) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const { deadline, idleSleepMs, maxIdleCycles, visibilityTimeoutMs } = getDrainLoopConfig();
|
|
118
|
+
let idleCycles = 0;
|
|
119
|
+
while (Date.now() < deadline && idleCycles < maxIdleCycles) {
|
|
120
|
+
// eslint-disable-next-line no-await-in-loop
|
|
121
|
+
const targets = await resolveDrainTargets(appWorkerDefinitions, workerModules);
|
|
122
|
+
if (targets.length === 0)
|
|
123
|
+
break;
|
|
124
|
+
// eslint-disable-next-line no-await-in-loop
|
|
125
|
+
const handledThisCycle = await drainTargets(targets, visibilityTimeoutMs, workerModules, deadline);
|
|
126
|
+
if (handledThisCycle === 0) {
|
|
127
|
+
idleCycles += 1;
|
|
128
|
+
// eslint-disable-next-line no-await-in-loop
|
|
129
|
+
await sleep(idleSleepMs);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
idleCycles = 0;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Ensure a drain loop is running. Returns immediately. If a loop is already active this is
|
|
137
|
+
* a no-op (concurrent-PING guard). The returned promise resolves when the active loop ends
|
|
138
|
+
* and is suitable for `ctx.waitUntil`.
|
|
139
|
+
*/
|
|
140
|
+
export const ensureDraining = async (appWorkerDefinitions, workerModules) => {
|
|
141
|
+
if (!isNullish(activeDrain))
|
|
142
|
+
return activeDrain;
|
|
143
|
+
activeDrain = runDrainLoop(appWorkerDefinitions, workerModules)
|
|
144
|
+
.catch((error) => {
|
|
145
|
+
Logger.error('[worker-runtime] drain loop crashed', error);
|
|
146
|
+
})
|
|
147
|
+
.finally(() => {
|
|
148
|
+
activeDrain = null;
|
|
149
|
+
});
|
|
150
|
+
return activeDrain;
|
|
151
|
+
};
|
|
152
|
+
export const isDraining = () => activeDrain !== null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers the global post-enqueue hook that core `Queue.enqueue` invokes after a
|
|
3
|
+
* successful enqueue. Every enqueue then wakes the worker runtime (PING), so jobs are
|
|
4
|
+
* drained promptly instead of waiting for the next cron tick. `triggerWorkerPing` is itself
|
|
5
|
+
* gated (WORKER_ENABLED + Redis RPC configured) and honours `WAIT_FOR_PING`.
|
|
6
|
+
*/
|
|
7
|
+
import type { AppWorkerDefinition } from './drain';
|
|
8
|
+
import type { WorkerModule } from './processor-registry';
|
|
9
|
+
declare global {
|
|
10
|
+
var __zintrustQueueEnqueueHook: ((queue: string, jobId: unknown, driver: string) => unknown) | undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Wire the app's bundled worker manifest and processor modules into the post-enqueue PING so
|
|
14
|
+
* the same-isolate drain loop can resolve which queues to drain and which processor to run.
|
|
15
|
+
*/
|
|
16
|
+
export declare const installQueueEnqueuePingHook: (appWorkerDefinitions: AppWorkerDefinition[], workerModules: ReadonlyArray<WorkerModule>) => void;
|
|
17
|
+
//# sourceMappingURL=install-enqueue-hook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-enqueue-hook.d.ts","sourceRoot":"","sources":["../../../src/worker-runtime/install-enqueue-hook.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAEjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAEvE,OAAO,CAAC,MAAM,CAAC;IACb,IAAI,0BAA0B,EAC1B,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,GAC5D,SAAS,CAAC;CACf;AAED;;;GAGG;AACH,eAAO,MAAM,2BAA2B,GACtC,sBAAsB,mBAAmB,EAAE,EAC3C,eAAe,aAAa,CAAC,YAAY,CAAC,KACzC,IAGF,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { triggerWorkerPing } from './ping.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wire the app's bundled worker manifest and processor modules into the post-enqueue PING so
|
|
4
|
+
* the same-isolate drain loop can resolve which queues to drain and which processor to run.
|
|
5
|
+
*/
|
|
6
|
+
export const installQueueEnqueuePingHook = (appWorkerDefinitions, workerModules) => {
|
|
7
|
+
globalThis.__zintrustQueueEnqueueHook = async () => triggerWorkerPing(appWorkerDefinitions, workerModules);
|
|
8
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker PING — wake + drain trigger.
|
|
3
|
+
*
|
|
4
|
+
* Producer side (`triggerWorkerPing`): called after a job is enqueued (and by cron). It
|
|
5
|
+
* wakes the worker runtime so jobs are drained promptly instead of waiting for the next
|
|
6
|
+
* cron tick.
|
|
7
|
+
*
|
|
8
|
+
* - Monolith / same isolate (no `WORKER_PING_URL`): start the local drain loop directly
|
|
9
|
+
* via the background scheduler (`ctx.waitUntil`).
|
|
10
|
+
* - Split service (`WORKER_PING_URL` set): POST to the worker service's PING endpoint.
|
|
11
|
+
*
|
|
12
|
+
* `WAIT_FOR_PING` gates the producer: when `true` the producer awaits the PING *ack only*
|
|
13
|
+
* (worker-awake confirmation), never the job's completion; when false it is fire-and-forget.
|
|
14
|
+
*
|
|
15
|
+
* Endpoint side (`handlePing`): authenticates, schedules the drain loop on the background
|
|
16
|
+
* scheduler, and responds immediately with an awake ack. The concurrent-PING guard lives in
|
|
17
|
+
* `ensureDraining`, so overlapping pings never start duplicate loops.
|
|
18
|
+
*/
|
|
19
|
+
import { type AppWorkerDefinition } from './drain';
|
|
20
|
+
import type { WorkerModule } from './processor-registry';
|
|
21
|
+
type PingRequest = {
|
|
22
|
+
getHeader?: (name: string) => string | string[] | undefined;
|
|
23
|
+
};
|
|
24
|
+
type PingResponse = {
|
|
25
|
+
json: (body: unknown) => unknown;
|
|
26
|
+
setStatus: (status: number) => PingResponse;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Producer-side wake. Safe no-op when the worker runtime is disabled/unconfigured.
|
|
30
|
+
*
|
|
31
|
+
* `appWorkerDefinitions` / `workerModules` are the app's bundled worker manifest and
|
|
32
|
+
* statically-imported processor modules; they are only used in the same-isolate branch to
|
|
33
|
+
* start the local drain loop. In the split-service branch the wake is a remote PING and the
|
|
34
|
+
* worker service supplies its own definitions, so they are unused there.
|
|
35
|
+
*/
|
|
36
|
+
export declare const triggerWorkerPing: (appWorkerDefinitions: AppWorkerDefinition[], workerModules: ReadonlyArray<WorkerModule>) => Promise<void>;
|
|
37
|
+
/** Endpoint-side handler: authenticate, start draining in the background, ack immediately. */
|
|
38
|
+
export declare const handlePing: (req: PingRequest, res: PingResponse, appWorkerDefinitions: AppWorkerDefinition[], workerModules: ReadonlyArray<WorkerModule>) => void;
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=ping.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ping.d.ts","sourceRoot":"","sources":["../../../src/worker-runtime/ping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,EAA8B,KAAK,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC7F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAGvE,KAAK,WAAW,GAAG;IACjB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;CAC7D,CAAC;AAEF,KAAK,YAAY,GAAG;IAClB,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC;IACjC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,YAAY,CAAC;CAC7C,CAAC;AAcF;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAC5B,sBAAsB,mBAAmB,EAAE,EAC3C,eAAe,aAAa,CAAC,YAAY,CAAC,KACzC,OAAO,CAAC,IAAI,CA+Bd,CAAC;AAEF,8FAA8F;AAC9F,eAAO,MAAM,UAAU,GACrB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,sBAAsB,mBAAmB,EAAE,EAC3C,eAAe,aAAa,CAAC,YAAY,CAAC,KACzC,IAaF,CAAC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker PING — wake + drain trigger.
|
|
3
|
+
*
|
|
4
|
+
* Producer side (`triggerWorkerPing`): called after a job is enqueued (and by cron). It
|
|
5
|
+
* wakes the worker runtime so jobs are drained promptly instead of waiting for the next
|
|
6
|
+
* cron tick.
|
|
7
|
+
*
|
|
8
|
+
* - Monolith / same isolate (no `WORKER_PING_URL`): start the local drain loop directly
|
|
9
|
+
* via the background scheduler (`ctx.waitUntil`).
|
|
10
|
+
* - Split service (`WORKER_PING_URL` set): POST to the worker service's PING endpoint.
|
|
11
|
+
*
|
|
12
|
+
* `WAIT_FOR_PING` gates the producer: when `true` the producer awaits the PING *ack only*
|
|
13
|
+
* (worker-awake confirmation), never the job's completion; when false it is fire-and-forget.
|
|
14
|
+
*
|
|
15
|
+
* Endpoint side (`handlePing`): authenticates, schedules the drain loop on the background
|
|
16
|
+
* scheduler, and responds immediately with an awake ack. The concurrent-PING guard lives in
|
|
17
|
+
* `ensureDraining`, so overlapping pings never start duplicate loops.
|
|
18
|
+
*/
|
|
19
|
+
import { Env } from '../config/env.js';
|
|
20
|
+
import { Logger } from '../config/logger.js';
|
|
21
|
+
import { BackgroundTaskScheduler } from '../runtime/BackgroundTaskScheduler.js';
|
|
22
|
+
import { ensureDraining, isDraining } from './drain.js';
|
|
23
|
+
import { isRedisRpcConfigured } from './rpc-client.js';
|
|
24
|
+
const getPingSecret = () => Env.get('WORKER_PING_SECRET', Env.get('REDIS_RPC_SECRET', Env.get('REDIS_PROXY_SECRET', Env.get('APP_KEY', '')))).trim();
|
|
25
|
+
const headerValue = (req, name) => {
|
|
26
|
+
const raw = req.getHeader?.(name);
|
|
27
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
28
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Producer-side wake. Safe no-op when the worker runtime is disabled/unconfigured.
|
|
32
|
+
*
|
|
33
|
+
* `appWorkerDefinitions` / `workerModules` are the app's bundled worker manifest and
|
|
34
|
+
* statically-imported processor modules; they are only used in the same-isolate branch to
|
|
35
|
+
* start the local drain loop. In the split-service branch the wake is a remote PING and the
|
|
36
|
+
* worker service supplies its own definitions, so they are unused there.
|
|
37
|
+
*/
|
|
38
|
+
export const triggerWorkerPing = async (appWorkerDefinitions, workerModules) => {
|
|
39
|
+
if (!Env.getBool('WORKER_ENABLED', false) || !isRedisRpcConfigured()) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const url = Env.get('WORKER_PING_URL', '').trim();
|
|
43
|
+
if (url.length === 0) {
|
|
44
|
+
// Same isolate: wake the local drain loop in the background.
|
|
45
|
+
BackgroundTaskScheduler.schedule(ensureDraining(appWorkerDefinitions, workerModules));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const secret = getPingSecret();
|
|
49
|
+
const ack = fetch(url, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'content-type': 'application/json',
|
|
53
|
+
...(secret ? { 'x-worker-ping-secret': secret } : {}),
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify({ at: new Date().toISOString() }),
|
|
56
|
+
})
|
|
57
|
+
.then(() => undefined)
|
|
58
|
+
.catch((error) => {
|
|
59
|
+
Logger.warn('[worker-runtime] worker ping failed', error);
|
|
60
|
+
});
|
|
61
|
+
if (Env.getBool('WAIT_FOR_PING', false)) {
|
|
62
|
+
await ack; // wait for the awake ack only — never the job's completion
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
BackgroundTaskScheduler.schedule(ack);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/** Endpoint-side handler: authenticate, start draining in the background, ack immediately. */
|
|
69
|
+
export const handlePing = (req, res, appWorkerDefinitions, workerModules) => {
|
|
70
|
+
const expected = getPingSecret();
|
|
71
|
+
if (expected.length > 0 && headerValue(req, 'x-worker-ping-secret') !== expected) {
|
|
72
|
+
res.setStatus(401).json({ ok: false, error: 'Invalid worker ping secret' });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!Env.getBool('WORKER_ENABLED', false) || !isRedisRpcConfigured()) {
|
|
76
|
+
res.json({ ok: true, status: 'disabled' });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const alreadyDraining = isDraining();
|
|
80
|
+
BackgroundTaskScheduler.schedule(ensureDraining(appWorkerDefinitions, workerModules));
|
|
81
|
+
res.json({ ok: true, status: 'awake', alreadyDraining });
|
|
82
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled processor registry.
|
|
3
|
+
*
|
|
4
|
+
* Cloudflare Workers cannot dynamically `import()` a file path at runtime — the module
|
|
5
|
+
* graph is frozen at build time. So the worker processor functions are statically
|
|
6
|
+
* imported here and exposed as a `processorSpec -> ZinTrustProcessor` map. The Redis RPC
|
|
7
|
+
* backend only stores the `processorSpec` string as metadata; this runtime resolves that
|
|
8
|
+
* string to the already-bundled function when it pulls a job.
|
|
9
|
+
*/
|
|
10
|
+
import type { Job } from 'bullmq';
|
|
11
|
+
export type WorkerProcessor = (job: Job) => Promise<unknown>;
|
|
12
|
+
export type WorkerModule = {
|
|
13
|
+
workerDefinition: {
|
|
14
|
+
processorSpec: string;
|
|
15
|
+
};
|
|
16
|
+
ZinTrustProcessor?: WorkerProcessor;
|
|
17
|
+
default?: WorkerProcessor;
|
|
18
|
+
};
|
|
19
|
+
export declare const resolveProcessor: (processorSpec: string, workerModules: ReadonlyArray<WorkerModule>) => WorkerProcessor | undefined;
|
|
20
|
+
export declare const hasProcessor: (processorSpec: string, workerModules: ReadonlyArray<WorkerModule>) => boolean;
|
|
21
|
+
export declare const listProcessorSpecs: (workerModules: ReadonlyArray<WorkerModule>) => string[];
|
|
22
|
+
//# sourceMappingURL=processor-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processor-registry.d.ts","sourceRoot":"","sources":["../../../src/worker-runtime/processor-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAElC,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAE7D,MAAM,MAAM,YAAY,GAAG;IACzB,gBAAgB,EAAE;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,iBAAiB,CAAC,EAAE,eAAe,CAAC;IACpC,OAAO,CAAC,EAAE,eAAe,CAAC;CAC3B,CAAC;AAoBF,eAAO,MAAM,gBAAgB,GAC3B,eAAe,MAAM,EACrB,eAAe,aAAa,CAAC,YAAY,CAAC,KACzC,eAAe,GAAG,SAAgE,CAAC;AAEtF,eAAO,MAAM,YAAY,GACvB,eAAe,MAAM,EACrB,eAAe,aAAa,CAAC,YAAY,CAAC,KACzC,OAA8D,CAAC;AAElE,eAAO,MAAM,kBAAkB,GAAI,eAAe,aAAa,CAAC,YAAY,CAAC,KAAG,MAAM,EACjC,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled processor registry.
|
|
3
|
+
*
|
|
4
|
+
* Cloudflare Workers cannot dynamically `import()` a file path at runtime — the module
|
|
5
|
+
* graph is frozen at build time. So the worker processor functions are statically
|
|
6
|
+
* imported here and exposed as a `processorSpec -> ZinTrustProcessor` map. The Redis RPC
|
|
7
|
+
* backend only stores the `processorSpec` string as metadata; this runtime resolves that
|
|
8
|
+
* string to the already-bundled function when it pulls a job.
|
|
9
|
+
*/
|
|
10
|
+
const buildRegistry = (workerModules) => {
|
|
11
|
+
const registry = new Map();
|
|
12
|
+
for (const mod of workerModules) {
|
|
13
|
+
const spec = mod.workerDefinition?.processorSpec;
|
|
14
|
+
const processor = mod.ZinTrustProcessor ?? mod.default;
|
|
15
|
+
if (typeof spec === 'string' && spec.trim().length > 0 && typeof processor === 'function') {
|
|
16
|
+
registry.set(spec, processor);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return registry;
|
|
20
|
+
};
|
|
21
|
+
const processorRegistry = (workerModules) => buildRegistry(workerModules);
|
|
22
|
+
export const resolveProcessor = (processorSpec, workerModules) => processorRegistry(workerModules).get(processorSpec);
|
|
23
|
+
export const hasProcessor = (processorSpec, workerModules) => processorRegistry(workerModules).has(processorSpec);
|
|
24
|
+
export const listProcessorSpecs = (workerModules) => Array.from(processorRegistry(workerModules).keys());
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type PulledJob = {
|
|
2
|
+
id: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
payload: unknown;
|
|
5
|
+
attempts: number;
|
|
6
|
+
};
|
|
7
|
+
export declare const isRedisRpcConfigured: () => boolean;
|
|
8
|
+
/** Atomically claim and pull the next waiting job for a queue, or undefined if none. */
|
|
9
|
+
export declare const pullJob: (queueName: string, visibilityTimeoutMs: number) => Promise<PulledJob | undefined>;
|
|
10
|
+
/** Mark a pulled job completed. */
|
|
11
|
+
export declare const ackJob: (queueName: string, jobId: string, returnValue?: unknown) => Promise<void>;
|
|
12
|
+
/** Mark a pulled job failed. */
|
|
13
|
+
export declare const failJob: (queueName: string, jobId: string, reason: string) => Promise<void>;
|
|
14
|
+
export type RegisteredWorker = {
|
|
15
|
+
workerName: string;
|
|
16
|
+
queueName: string;
|
|
17
|
+
status?: string;
|
|
18
|
+
processorSpec?: string | null;
|
|
19
|
+
source?: string;
|
|
20
|
+
};
|
|
21
|
+
/** List worker lifecycle records the backend holds (status running/stopped). */
|
|
22
|
+
export declare const listWorkers: () => Promise<RegisteredWorker[]>;
|
|
23
|
+
//# sourceMappingURL=rpc-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../../src/worker-runtime/rpc-client.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAOF,eAAO,MAAM,oBAAoB,QAAO,OAAkC,CAAC;AA6B3E,wFAAwF;AACxF,eAAO,MAAM,OAAO,GAClB,WAAW,MAAM,EACjB,qBAAqB,MAAM,KAC1B,OAAO,CAAC,SAAS,GAAG,SAAS,CAM/B,CAAC;AAEF,mCAAmC;AACnC,eAAO,MAAM,MAAM,GACjB,WAAW,MAAM,EACjB,OAAO,MAAM,EACb,cAAc,OAAO,KACpB,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF,gCAAgC;AAChC,eAAO,MAAM,OAAO,GAAU,WAAW,MAAM,EAAE,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAM5F,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,gBAAgB,EAAE,CAG9D,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-native Redis RPC client.
|
|
3
|
+
*
|
|
4
|
+
* The Cloudflare Worker cannot open a raw TCP/ioredis connection reliably, so all Redis
|
|
5
|
+
* access goes over HTTP to the Redis RPC backend (`REDIS_RPC_URL`). This is the same
|
|
6
|
+
* `/rpc` envelope used by the worker dashboard.
|
|
7
|
+
*/
|
|
8
|
+
import { Env } from '../config/env.js';
|
|
9
|
+
import { Logger } from '../config/logger.js';
|
|
10
|
+
import { ErrorFactory } from '../exceptions/ZintrustError.js';
|
|
11
|
+
const getBaseUrl = () => Env.get('REDIS_RPC_URL', '').trim();
|
|
12
|
+
const getSecret = () => Env.get('REDIS_RPC_SECRET', Env.get('REDIS_PROXY_SECRET', Env.get('APP_KEY', ''))).trim();
|
|
13
|
+
export const isRedisRpcConfigured = () => getBaseUrl().length > 0;
|
|
14
|
+
const call = async (service, method, payload = {}) => {
|
|
15
|
+
const baseUrl = getBaseUrl();
|
|
16
|
+
if (baseUrl.length === 0) {
|
|
17
|
+
throw ErrorFactory.createConfigError('REDIS_RPC_URL is not configured');
|
|
18
|
+
}
|
|
19
|
+
const secret = getSecret();
|
|
20
|
+
const response = await fetch(new URL('/rpc', baseUrl), {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: {
|
|
23
|
+
'content-type': 'application/json',
|
|
24
|
+
...(secret ? { 'x-redis-rpc-secret': secret } : {}),
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify({ requestId: crypto.randomUUID(), service, method, payload }),
|
|
27
|
+
});
|
|
28
|
+
const parsed = (await response.json());
|
|
29
|
+
if (!response.ok || parsed.ok !== true) {
|
|
30
|
+
throw ErrorFactory.createConfigError(parsed.error?.message ?? `Redis RPC ${service}.${method} failed (${response.status})`);
|
|
31
|
+
}
|
|
32
|
+
return parsed.result;
|
|
33
|
+
};
|
|
34
|
+
/** Atomically claim and pull the next waiting job for a queue, or undefined if none. */
|
|
35
|
+
export const pullJob = async (queueName, visibilityTimeoutMs) => {
|
|
36
|
+
const result = await call('queue', 'dequeue', {
|
|
37
|
+
queueName,
|
|
38
|
+
visibilityTimeoutMs,
|
|
39
|
+
});
|
|
40
|
+
return result ?? undefined;
|
|
41
|
+
};
|
|
42
|
+
/** Mark a pulled job completed. */
|
|
43
|
+
export const ackJob = async (queueName, jobId, returnValue) => {
|
|
44
|
+
await call('queue', 'ack', { queueName, jobId, returnValue });
|
|
45
|
+
};
|
|
46
|
+
/** Mark a pulled job failed. */
|
|
47
|
+
export const failJob = async (queueName, jobId, reason) => {
|
|
48
|
+
try {
|
|
49
|
+
await call('queue', 'fail', { queueName, jobId, reason });
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
Logger.warn('[worker-runtime] failJob RPC failed', { queueName, jobId, error });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/** List worker lifecycle records the backend holds (status running/stopped). */
|
|
56
|
+
export const listWorkers = async () => {
|
|
57
|
+
const result = await call('worker', 'list', {});
|
|
58
|
+
return Array.isArray(result) ? result : [];
|
|
59
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { AppWorkerDefinition } from './worker-runtime/drain';
|
|
2
|
+
export { installQueueEnqueuePingHook } from './worker-runtime/install-enqueue-hook';
|
|
3
|
+
export { handlePing, triggerWorkerPing } from './worker-runtime/ping';
|
|
4
|
+
export { hasProcessor, listProcessorSpecs, type WorkerModule, type WorkerProcessor, } from './worker-runtime/processor-registry';
|
|
5
|
+
//# sourceMappingURL=worker-runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-runtime.d.ts","sourceRoot":"","sources":["../../src/worker-runtime.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,eAAe,GACrB,MAAM,oCAAoC,CAAC"}
|