@nicnocquee/dataqueue 1.22.0 → 1.24.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/dist/index.cjs +486 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +151 -2
- package/dist/index.d.ts +151 -2
- package/dist/index.js +485 -30
- package/dist/index.js.map +1 -1
- package/migrations/1765809419000_add_force_kill_on_timeout_to_job_queue.sql +6 -0
- package/package.json +1 -1
- package/src/handler-validation.test.ts +414 -0
- package/src/handler-validation.ts +168 -0
- package/src/index.test.ts +224 -0
- package/src/index.ts +33 -0
- package/src/processor.test.ts +55 -0
- package/src/processor.ts +261 -17
- package/src/queue.test.ts +522 -0
- package/src/queue.ts +286 -9
- package/src/types.ts +102 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
validateHandlerSerializable,
|
|
4
|
+
testHandlerSerialization,
|
|
5
|
+
} from './handler-validation.js';
|
|
6
|
+
import { JobHandler } from './types.js';
|
|
7
|
+
|
|
8
|
+
// Define test payload map
|
|
9
|
+
interface TestPayloadMap {
|
|
10
|
+
simple: { data: string };
|
|
11
|
+
complex: { id: number; name: string };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('validateHandlerSerializable', () => {
|
|
15
|
+
it('should validate a simple standalone handler as serializable', () => {
|
|
16
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
17
|
+
payload,
|
|
18
|
+
signal,
|
|
19
|
+
) => {
|
|
20
|
+
await Promise.resolve();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const result = validateHandlerSerializable(handler, 'simple');
|
|
24
|
+
expect(result.isSerializable).toBe(true);
|
|
25
|
+
expect(result.error).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should validate a handler with local variables as serializable', () => {
|
|
29
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
30
|
+
payload,
|
|
31
|
+
signal,
|
|
32
|
+
) => {
|
|
33
|
+
const localVar = 'test';
|
|
34
|
+
const anotherVar = 123;
|
|
35
|
+
await Promise.resolve(localVar + anotherVar);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const result = validateHandlerSerializable(handler, 'simple');
|
|
39
|
+
expect(result.isSerializable).toBe(true);
|
|
40
|
+
expect(result.error).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should validate a handler that imports dependencies inside as serializable', () => {
|
|
44
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
45
|
+
payload,
|
|
46
|
+
signal,
|
|
47
|
+
) => {
|
|
48
|
+
const { default: something } = await import('path');
|
|
49
|
+
await Promise.resolve();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const result = validateHandlerSerializable(handler, 'simple');
|
|
53
|
+
expect(result.isSerializable).toBe(true);
|
|
54
|
+
expect(result.error).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should reject a handler that uses "this" context', () => {
|
|
58
|
+
// Create a handler that uses 'this' by mocking toString to show 'this.' in the body
|
|
59
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
60
|
+
payload,
|
|
61
|
+
signal,
|
|
62
|
+
) => {
|
|
63
|
+
await Promise.resolve();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Mock toString to simulate a handler that uses 'this'
|
|
67
|
+
const originalToString = handler.toString.bind(handler);
|
|
68
|
+
(handler as any).toString = () =>
|
|
69
|
+
'async (payload) => { return this.value; }';
|
|
70
|
+
|
|
71
|
+
const result = validateHandlerSerializable(handler, 'simple');
|
|
72
|
+
expect(result.isSerializable).toBe(false);
|
|
73
|
+
expect(result.error).toContain("uses 'this' context");
|
|
74
|
+
expect(result.error).toContain('cannot be serialized');
|
|
75
|
+
|
|
76
|
+
// Restore
|
|
77
|
+
(handler as any).toString = originalToString;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should reject a handler with native code', () => {
|
|
81
|
+
// Create a handler that might contain native code
|
|
82
|
+
// This is tricky to test directly, but we can test the detection logic
|
|
83
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
84
|
+
payload,
|
|
85
|
+
signal,
|
|
86
|
+
) => {
|
|
87
|
+
// Native methods like Array.prototype methods might show as native code
|
|
88
|
+
Array.isArray([]);
|
|
89
|
+
await Promise.resolve();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Mock toString to return native code indicator
|
|
93
|
+
const originalToString = handler.toString.bind(handler);
|
|
94
|
+
(handler as any).toString = () => '[native code]';
|
|
95
|
+
|
|
96
|
+
const result = validateHandlerSerializable(handler, 'simple');
|
|
97
|
+
expect(result.isSerializable).toBe(false);
|
|
98
|
+
expect(result.error).toContain('contains native code');
|
|
99
|
+
expect(result.error).toContain('cannot be serialized');
|
|
100
|
+
|
|
101
|
+
// Restore
|
|
102
|
+
(handler as any).toString = originalToString;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should reject a handler that cannot be parsed', () => {
|
|
106
|
+
// Create a handler with invalid syntax when stringified
|
|
107
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
108
|
+
payload,
|
|
109
|
+
signal,
|
|
110
|
+
) => {
|
|
111
|
+
await Promise.resolve();
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Mock toString to return invalid function code
|
|
115
|
+
const originalToString = handler.toString.bind(handler);
|
|
116
|
+
(handler as any).toString = () =>
|
|
117
|
+
'async (payload) => { invalid syntax here !@#$';
|
|
118
|
+
|
|
119
|
+
const result = validateHandlerSerializable(handler, 'simple');
|
|
120
|
+
expect(result.isSerializable).toBe(false);
|
|
121
|
+
expect(result.error).toContain('cannot be serialized');
|
|
122
|
+
|
|
123
|
+
// Restore
|
|
124
|
+
(handler as any).toString = originalToString;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should warn about potential closures but still mark as serializable', () => {
|
|
128
|
+
// This handler has a pattern that might indicate closures
|
|
129
|
+
// but the validation can't be 100% sure, so it warns
|
|
130
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
131
|
+
payload,
|
|
132
|
+
signal,
|
|
133
|
+
) => {
|
|
134
|
+
const local = 'test';
|
|
135
|
+
await Promise.resolve(local);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const result = validateHandlerSerializable(handler, 'simple');
|
|
139
|
+
// The current implementation might return a warning, but it's still considered serializable
|
|
140
|
+
// This test checks the behavior - if it warns, that's OK
|
|
141
|
+
if (result.error) {
|
|
142
|
+
expect(result.error).toContain('Warning');
|
|
143
|
+
expect(result.error).toContain('may have closures');
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should work without jobType parameter', () => {
|
|
148
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
149
|
+
payload,
|
|
150
|
+
signal,
|
|
151
|
+
) => {
|
|
152
|
+
await Promise.resolve();
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const result = validateHandlerSerializable(handler);
|
|
156
|
+
expect(result.isSerializable).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should provide helpful error messages with jobType', () => {
|
|
160
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
161
|
+
payload,
|
|
162
|
+
signal,
|
|
163
|
+
) => {
|
|
164
|
+
await Promise.resolve();
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Mock toString to simulate a handler that uses 'this'
|
|
168
|
+
const originalToString = handler.toString.bind(handler);
|
|
169
|
+
(handler as any).toString = () =>
|
|
170
|
+
'async (payload) => { return this.value; }';
|
|
171
|
+
|
|
172
|
+
const result = validateHandlerSerializable(handler, 'myJobType');
|
|
173
|
+
expect(result.isSerializable).toBe(false);
|
|
174
|
+
expect(result.error).toContain('myJobType');
|
|
175
|
+
expect(result.error).toContain("uses 'this' context");
|
|
176
|
+
|
|
177
|
+
// Restore
|
|
178
|
+
(handler as any).toString = originalToString;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should handle errors during validation gracefully', () => {
|
|
182
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
183
|
+
payload,
|
|
184
|
+
signal,
|
|
185
|
+
) => {
|
|
186
|
+
await Promise.resolve();
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Mock toString to throw an error
|
|
190
|
+
const originalToString = handler.toString.bind(handler);
|
|
191
|
+
(handler as any).toString = () => {
|
|
192
|
+
throw new Error('toString failed');
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const result = validateHandlerSerializable(handler, 'simple');
|
|
196
|
+
expect(result.isSerializable).toBe(false);
|
|
197
|
+
expect(result.error).toContain('Failed to validate handler serialization');
|
|
198
|
+
|
|
199
|
+
// Restore
|
|
200
|
+
(handler as any).toString = originalToString;
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('testHandlerSerialization', () => {
|
|
205
|
+
it('should validate a simple handler as serializable', async () => {
|
|
206
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
207
|
+
payload,
|
|
208
|
+
signal,
|
|
209
|
+
) => {
|
|
210
|
+
await Promise.resolve();
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const result = await testHandlerSerialization(handler, 'simple');
|
|
214
|
+
expect(result.isSerializable).toBe(true);
|
|
215
|
+
expect(result.error).toBeUndefined();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should reject a handler that fails basic validation', async () => {
|
|
219
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
220
|
+
payload,
|
|
221
|
+
signal,
|
|
222
|
+
) => {
|
|
223
|
+
await Promise.resolve();
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Mock toString to simulate a handler that uses 'this'
|
|
227
|
+
const originalToString = handler.toString.bind(handler);
|
|
228
|
+
(handler as any).toString = () =>
|
|
229
|
+
'async (payload) => { return this.value; }';
|
|
230
|
+
|
|
231
|
+
const result = await testHandlerSerialization(handler, 'simple');
|
|
232
|
+
expect(result.isSerializable).toBe(false);
|
|
233
|
+
expect(result.error).toContain("uses 'this' context");
|
|
234
|
+
|
|
235
|
+
// Restore
|
|
236
|
+
(handler as any).toString = originalToString;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should handle handlers that complete quickly', async () => {
|
|
240
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
241
|
+
payload,
|
|
242
|
+
signal,
|
|
243
|
+
) => {
|
|
244
|
+
return Promise.resolve();
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const result = await testHandlerSerialization(handler, 'simple');
|
|
248
|
+
expect(result.isSerializable).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should handle handlers that take time but still validate as serializable', async () => {
|
|
252
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
253
|
+
payload,
|
|
254
|
+
signal,
|
|
255
|
+
) => {
|
|
256
|
+
// Handler that takes longer than the test timeout (100ms)
|
|
257
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const result = await testHandlerSerialization(handler, 'simple');
|
|
261
|
+
// Should still be considered serializable even if it times out during test
|
|
262
|
+
expect(result.isSerializable).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should handle handlers that throw errors during execution', async () => {
|
|
266
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
267
|
+
payload,
|
|
268
|
+
signal,
|
|
269
|
+
) => {
|
|
270
|
+
throw new Error('Handler error');
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const result = await testHandlerSerialization(handler, 'simple');
|
|
274
|
+
// Execution errors are OK - we just want to know if it can be deserialized
|
|
275
|
+
// The handler is still considered serializable
|
|
276
|
+
expect(result.isSerializable).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should handle serialization errors', async () => {
|
|
280
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
281
|
+
payload,
|
|
282
|
+
signal,
|
|
283
|
+
) => {
|
|
284
|
+
await Promise.resolve();
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Mock toString to return invalid code
|
|
288
|
+
const originalToString = handler.toString.bind(handler);
|
|
289
|
+
(handler as any).toString = () => 'invalid function code !@#$';
|
|
290
|
+
|
|
291
|
+
const result = await testHandlerSerialization(handler, 'simple');
|
|
292
|
+
expect(result.isSerializable).toBe(false);
|
|
293
|
+
expect(result.error).toBeDefined();
|
|
294
|
+
|
|
295
|
+
// Restore
|
|
296
|
+
(handler as any).toString = originalToString;
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should work without jobType parameter', async () => {
|
|
300
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
301
|
+
payload,
|
|
302
|
+
signal,
|
|
303
|
+
) => {
|
|
304
|
+
await Promise.resolve();
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const result = await testHandlerSerialization(handler);
|
|
308
|
+
expect(result.isSerializable).toBe(true);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should handle complex payload types', async () => {
|
|
312
|
+
const handler: JobHandler<TestPayloadMap, 'complex'> = async (
|
|
313
|
+
payload,
|
|
314
|
+
signal,
|
|
315
|
+
) => {
|
|
316
|
+
const { id, name } = payload;
|
|
317
|
+
await Promise.resolve(`${id}: ${name}`);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const result = await testHandlerSerialization(handler, 'complex');
|
|
321
|
+
expect(result.isSerializable).toBe(true);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should handle handlers that use signal parameter', async () => {
|
|
325
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
326
|
+
payload,
|
|
327
|
+
signal,
|
|
328
|
+
) => {
|
|
329
|
+
if (signal.aborted) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
await Promise.resolve();
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const result = await testHandlerSerialization(handler, 'simple');
|
|
336
|
+
expect(result.isSerializable).toBe(true);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe('handler validation edge cases', () => {
|
|
341
|
+
it('should handle arrow functions correctly', () => {
|
|
342
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
343
|
+
payload,
|
|
344
|
+
signal,
|
|
345
|
+
) => {
|
|
346
|
+
await Promise.resolve();
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const result = validateHandlerSerializable(handler, 'simple');
|
|
350
|
+
expect(result.isSerializable).toBe(true);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should handle regular function declarations', () => {
|
|
354
|
+
async function handler(
|
|
355
|
+
payload: TestPayloadMap['simple'],
|
|
356
|
+
signal: AbortSignal,
|
|
357
|
+
): Promise<void> {
|
|
358
|
+
await Promise.resolve();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const result = validateHandlerSerializable(handler, 'simple');
|
|
362
|
+
expect(result.isSerializable).toBe(true);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should handle handlers with multiple statements', () => {
|
|
366
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
367
|
+
payload,
|
|
368
|
+
signal,
|
|
369
|
+
) => {
|
|
370
|
+
const step1 = 'first';
|
|
371
|
+
const step2 = 'second';
|
|
372
|
+
const step3 = step1 + step2;
|
|
373
|
+
await Promise.resolve(step3);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const result = validateHandlerSerializable(handler, 'simple');
|
|
377
|
+
expect(result.isSerializable).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should handle handlers with conditional logic', () => {
|
|
381
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
382
|
+
payload,
|
|
383
|
+
signal,
|
|
384
|
+
) => {
|
|
385
|
+
if (signal.aborted) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (payload.data === 'test') {
|
|
389
|
+
await Promise.resolve('matched');
|
|
390
|
+
} else {
|
|
391
|
+
await Promise.resolve('not matched');
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const result = validateHandlerSerializable(handler, 'simple');
|
|
396
|
+
expect(result.isSerializable).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should handle handlers with try-catch blocks', () => {
|
|
400
|
+
const handler: JobHandler<TestPayloadMap, 'simple'> = async (
|
|
401
|
+
payload,
|
|
402
|
+
signal,
|
|
403
|
+
) => {
|
|
404
|
+
try {
|
|
405
|
+
await Promise.resolve();
|
|
406
|
+
} catch (error) {
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const result = validateHandlerSerializable(handler, 'simple');
|
|
412
|
+
expect(result.isSerializable).toBe(true);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { JobHandler } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validates that a job handler can be serialized for use with forceKillOnTimeout.
|
|
5
|
+
*
|
|
6
|
+
* This function checks if a handler can be safely serialized and executed in a worker thread.
|
|
7
|
+
* Use this function during development to catch serialization issues early.
|
|
8
|
+
*
|
|
9
|
+
* @param handler - The job handler function to validate
|
|
10
|
+
* @param jobType - Optional job type name for better error messages
|
|
11
|
+
* @returns An object with `isSerializable` boolean and optional `error` message
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const handler = async (payload, signal) => {
|
|
16
|
+
* await doSomething(payload);
|
|
17
|
+
* };
|
|
18
|
+
*
|
|
19
|
+
* const result = validateHandlerSerializable(handler, 'myJob');
|
|
20
|
+
* if (!result.isSerializable) {
|
|
21
|
+
* console.error('Handler is not serializable:', result.error);
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function validateHandlerSerializable<
|
|
26
|
+
PayloadMap,
|
|
27
|
+
T extends keyof PayloadMap & string,
|
|
28
|
+
>(
|
|
29
|
+
handler: JobHandler<PayloadMap, T>,
|
|
30
|
+
jobType?: string,
|
|
31
|
+
): { isSerializable: boolean; error?: string } {
|
|
32
|
+
try {
|
|
33
|
+
const handlerString = handler.toString();
|
|
34
|
+
const typeLabel = jobType ? `job type "${jobType}"` : 'handler';
|
|
35
|
+
|
|
36
|
+
// Check for common patterns that indicate non-serializable handlers
|
|
37
|
+
// 1. Arrow functions that capture 'this' (indicated by 'this' in the function body but not in parameters)
|
|
38
|
+
if (
|
|
39
|
+
handlerString.includes('this.') &&
|
|
40
|
+
!handlerString.match(/\([^)]*this[^)]*\)/)
|
|
41
|
+
) {
|
|
42
|
+
return {
|
|
43
|
+
isSerializable: false,
|
|
44
|
+
error:
|
|
45
|
+
`Handler for ${typeLabel} uses 'this' context which cannot be serialized. ` +
|
|
46
|
+
`Use a regular function or avoid 'this' references when forceKillOnTimeout is enabled.`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Check if handler string looks like it might have closures
|
|
51
|
+
// This is a heuristic - we can't perfectly detect closures, but we can warn about common patterns
|
|
52
|
+
if (handlerString.includes('[native code]')) {
|
|
53
|
+
return {
|
|
54
|
+
isSerializable: false,
|
|
55
|
+
error:
|
|
56
|
+
`Handler for ${typeLabel} contains native code which cannot be serialized. ` +
|
|
57
|
+
`Ensure your handler is a plain function when forceKillOnTimeout is enabled.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. Try to create a function from the string to validate it's parseable
|
|
62
|
+
// This will catch syntax errors early
|
|
63
|
+
try {
|
|
64
|
+
new Function('return ' + handlerString);
|
|
65
|
+
} catch (parseError) {
|
|
66
|
+
return {
|
|
67
|
+
isSerializable: false,
|
|
68
|
+
error:
|
|
69
|
+
`Handler for ${typeLabel} cannot be serialized: ${parseError instanceof Error ? parseError.message : String(parseError)}. ` +
|
|
70
|
+
`When using forceKillOnTimeout, handlers must be serializable functions without closures over external variables.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 4. Check for common closure patterns (heuristic)
|
|
75
|
+
// Look for variable references that might be from outer scope
|
|
76
|
+
// This is not perfect but can catch some common issues
|
|
77
|
+
const hasPotentialClosure =
|
|
78
|
+
/const\s+\w+\s*=\s*[^;]+;\s*async\s*\(/.test(handlerString) ||
|
|
79
|
+
/let\s+\w+\s*=\s*[^;]+;\s*async\s*\(/.test(handlerString);
|
|
80
|
+
|
|
81
|
+
if (hasPotentialClosure) {
|
|
82
|
+
// This is just a warning, not a hard error, since we can't be 100% sure
|
|
83
|
+
// The actual serialization will fail at runtime if there's a real issue
|
|
84
|
+
return {
|
|
85
|
+
isSerializable: true, // Still serializable, but might have issues
|
|
86
|
+
error:
|
|
87
|
+
`Warning: Handler for ${typeLabel} may have closures over external variables. ` +
|
|
88
|
+
`Test thoroughly with forceKillOnTimeout enabled. If the handler fails to execute in a worker thread, ` +
|
|
89
|
+
`ensure all dependencies are imported within the handler function.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { isSerializable: true };
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
isSerializable: false,
|
|
97
|
+
error: `Failed to validate handler serialization${jobType ? ` for job type "${jobType}"` : ''}: ${error instanceof Error ? error.message : String(error)}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Test if a handler can be serialized and executed in a worker thread.
|
|
104
|
+
* This is a more thorough check that actually attempts to serialize and deserialize the handler.
|
|
105
|
+
*
|
|
106
|
+
* @param handler - The job handler function to test
|
|
107
|
+
* @param jobType - Optional job type name for better error messages
|
|
108
|
+
* @returns Promise that resolves to validation result
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* const handler = async (payload, signal) => {
|
|
113
|
+
* await doSomething(payload);
|
|
114
|
+
* };
|
|
115
|
+
*
|
|
116
|
+
* const result = await testHandlerSerialization(handler, 'myJob');
|
|
117
|
+
* if (!result.isSerializable) {
|
|
118
|
+
* console.error('Handler failed serialization test:', result.error);
|
|
119
|
+
* }
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export async function testHandlerSerialization<
|
|
123
|
+
PayloadMap,
|
|
124
|
+
T extends keyof PayloadMap & string,
|
|
125
|
+
>(
|
|
126
|
+
handler: JobHandler<PayloadMap, T>,
|
|
127
|
+
jobType?: string,
|
|
128
|
+
): Promise<{ isSerializable: boolean; error?: string }> {
|
|
129
|
+
// First do the basic validation
|
|
130
|
+
const basicValidation = validateHandlerSerializable(handler, jobType);
|
|
131
|
+
if (!basicValidation.isSerializable) {
|
|
132
|
+
return basicValidation;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Then try to actually serialize and deserialize in a worker-like context
|
|
136
|
+
try {
|
|
137
|
+
const handlerString = handler.toString();
|
|
138
|
+
const handlerFn = new Function('return ' + handlerString)();
|
|
139
|
+
|
|
140
|
+
// Try to call it with dummy parameters to see if it executes
|
|
141
|
+
// We use a very short timeout to avoid hanging
|
|
142
|
+
const testPromise = handlerFn({}, new AbortController().signal);
|
|
143
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
144
|
+
setTimeout(() => reject(new Error('Handler test timeout')), 100),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await Promise.race([testPromise, timeoutPromise]);
|
|
149
|
+
} catch (execError) {
|
|
150
|
+
// Execution errors are OK - we just want to know if it can be deserialized
|
|
151
|
+
// The actual job execution will handle real errors
|
|
152
|
+
if (
|
|
153
|
+
execError instanceof Error &&
|
|
154
|
+
execError.message === 'Handler test timeout'
|
|
155
|
+
) {
|
|
156
|
+
// Handler is taking too long, but that's OK for serialization test
|
|
157
|
+
return { isSerializable: true };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { isSerializable: true };
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return {
|
|
164
|
+
isSerializable: false,
|
|
165
|
+
error: `Handler failed serialization test: ${error instanceof Error ? error.message : String(error)}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|