@tstdl/base 0.93.140 → 0.93.141

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.
Files changed (98) hide show
  1. package/application/application.d.ts +1 -1
  2. package/application/application.js +1 -1
  3. package/application/providers.d.ts +20 -2
  4. package/application/providers.js +34 -7
  5. package/audit/module.d.ts +5 -0
  6. package/audit/module.js +9 -1
  7. package/authentication/server/module.d.ts +5 -0
  8. package/authentication/server/module.js +9 -1
  9. package/authentication/tests/authentication.api-controller.test.js +1 -1
  10. package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
  11. package/authentication/tests/authentication.client-service.test.js +1 -1
  12. package/circuit-breaker/postgres/module.d.ts +1 -0
  13. package/circuit-breaker/postgres/module.js +5 -1
  14. package/document-management/server/configure.js +5 -1
  15. package/document-management/server/module.d.ts +1 -1
  16. package/document-management/server/module.js +1 -1
  17. package/document-management/server/services/document-management-ancillary.service.js +1 -1
  18. package/document-management/tests/ai-config-hierarchy.test.js +0 -5
  19. package/document-management/tests/document-management-ai-overrides.test.js +0 -1
  20. package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
  21. package/examples/document-management/main.d.ts +1 -0
  22. package/examples/document-management/main.js +14 -11
  23. package/key-value-store/postgres/module.d.ts +1 -0
  24. package/key-value-store/postgres/module.js +5 -1
  25. package/lock/postgres/module.d.ts +1 -0
  26. package/lock/postgres/module.js +5 -1
  27. package/mail/module.d.ts +5 -1
  28. package/mail/module.js +11 -6
  29. package/module/modules/web-server.module.js +2 -3
  30. package/notification/server/module.d.ts +1 -0
  31. package/notification/server/module.js +5 -1
  32. package/notification/tests/notification-flow.test.js +2 -2
  33. package/orm/decorators.d.ts +5 -1
  34. package/orm/decorators.js +1 -1
  35. package/orm/server/drizzle/schema-converter.js +17 -30
  36. package/orm/server/encryption.d.ts +0 -1
  37. package/orm/server/encryption.js +1 -4
  38. package/orm/server/index.d.ts +1 -6
  39. package/orm/server/index.js +1 -6
  40. package/orm/server/migration.d.ts +19 -0
  41. package/orm/server/migration.js +72 -0
  42. package/orm/server/repository.d.ts +1 -1
  43. package/orm/server/transaction.d.ts +5 -10
  44. package/orm/server/transaction.js +22 -26
  45. package/orm/server/transactional.js +3 -3
  46. package/orm/tests/database-migration.test.d.ts +1 -0
  47. package/orm/tests/database-migration.test.js +82 -0
  48. package/orm/tests/encryption.test.js +3 -4
  49. package/orm/utils.d.ts +17 -2
  50. package/orm/utils.js +49 -1
  51. package/package.json +4 -3
  52. package/rate-limit/postgres/module.d.ts +1 -0
  53. package/rate-limit/postgres/module.js +5 -1
  54. package/reflection/decorator-data.js +11 -12
  55. package/task-queue/README.md +2 -9
  56. package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
  57. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
  58. package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
  59. package/task-queue/postgres/module.d.ts +1 -0
  60. package/task-queue/postgres/module.js +5 -1
  61. package/task-queue/postgres/schemas.d.ts +9 -6
  62. package/task-queue/postgres/schemas.js +4 -3
  63. package/task-queue/postgres/task-queue.d.ts +2 -12
  64. package/task-queue/postgres/task-queue.js +431 -354
  65. package/task-queue/postgres/task.model.d.ts +12 -5
  66. package/task-queue/postgres/task.model.js +51 -25
  67. package/task-queue/task-context.d.ts +2 -2
  68. package/task-queue/task-context.js +7 -7
  69. package/task-queue/task-queue.d.ts +36 -19
  70. package/task-queue/task-queue.js +18 -10
  71. package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
  72. package/task-queue/tests/cascading-cancellations.test.js +38 -0
  73. package/task-queue/tests/complex.test.js +44 -228
  74. package/task-queue/tests/coverage-branch.test.d.ts +1 -0
  75. package/task-queue/tests/coverage-branch.test.js +407 -0
  76. package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
  77. package/task-queue/tests/coverage-enhancement.test.js +144 -0
  78. package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
  79. package/task-queue/tests/dag-dependencies.test.js +41 -0
  80. package/task-queue/tests/dependencies.test.js +26 -26
  81. package/task-queue/tests/extensive-dependencies.test.js +64 -139
  82. package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
  83. package/task-queue/tests/fan-out-spawning.test.js +53 -0
  84. package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
  85. package/task-queue/tests/idempotent-replacement.test.js +61 -0
  86. package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
  87. package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
  88. package/task-queue/tests/queue.test.js +33 -24
  89. package/task-queue/tests/worker.test.js +20 -5
  90. package/task-queue/tests/zombie-parent.test.d.ts +1 -0
  91. package/task-queue/tests/zombie-parent.test.js +45 -0
  92. package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
  93. package/task-queue/tests/zombie-recovery.test.js +51 -0
  94. package/test5.js +5 -5
  95. package/testing/integration-setup.d.ts +4 -4
  96. package/testing/integration-setup.js +54 -29
  97. package/text/localization.service.js +2 -2
  98. package/utils/file-reader.js +1 -2
@@ -1,7 +1,6 @@
1
1
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
2
- import { DependencyJoinMode, TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
2
+ import { TaskQueueProvider, TaskStatus } from '../../task-queue/index.js';
3
3
  import { setupIntegrationTest } from '../../testing/index.js';
4
- import { currentTimestamp } from '../../utils/date-time.js';
5
4
  import { timeout } from '../../utils/timing.js';
6
5
  describe('Complex Queue Scenarios', () => {
7
6
  let injector;
@@ -37,19 +36,15 @@ describe('Complex Queue Scenarios', () => {
37
36
  const task = await queue.getTask(id);
38
37
  if (task?.status === status)
39
38
  return;
40
- await queue.processPendingFanIn();
41
39
  await timeout(10);
42
40
  }
43
41
  }
44
42
  describe('Complex Dependencies', () => {
45
43
  it('should handle Diamond Dependency (A -> B, A -> C, (B&C) -> D)', async () => {
46
- // D waits for B and C
47
- const taskD = await queue.enqueue('D', {}, { scheduleAfterTags: ['tag-b', 'tag-c'] });
48
- // B and C wait for A
49
- const taskB = await queue.enqueue('B', {}, { tags: ['tag-b'], scheduleAfterTags: ['tag-a'] });
50
- const taskC = await queue.enqueue('C', {}, { tags: ['tag-c'], scheduleAfterTags: ['tag-a'] });
51
- // A runs first
52
- const taskA = await queue.enqueue('A', {}, { tags: ['tag-a'] });
44
+ const taskA = await queue.enqueue('A', {});
45
+ const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id] });
46
+ const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskA.id] });
47
+ const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskB.id, taskC.id] });
53
48
  expect(taskD.status).toBe(TaskStatus.Waiting);
54
49
  expect(taskB.status).toBe(TaskStatus.Waiting);
55
50
  expect(taskC.status).toBe(TaskStatus.Waiting);
@@ -62,8 +57,6 @@ describe('Complex Queue Scenarios', () => {
62
57
  // Process B
63
58
  const dB = await queue.dequeue({ types: ['B'] });
64
59
  await queue.complete(dB);
65
- await timeout(20);
66
- await queue.processPendingFanIn();
67
60
  // D still waiting (needs C)
68
61
  const uD2 = await queue.getTask(taskD.id);
69
62
  expect(uD2?.status).toBe(TaskStatus.Waiting);
@@ -76,231 +69,54 @@ describe('Complex Queue Scenarios', () => {
76
69
  expect(uD3?.status).toBe(TaskStatus.Pending);
77
70
  });
78
71
  it('should handle Deep Chain (A -> B -> C -> D)', async () => {
79
- const D = await queue.enqueue('D', {}, { scheduleAfterTags: ['C'] });
80
- const C = await queue.enqueue('C', {}, { tags: ['C'], scheduleAfterTags: ['B'] });
81
- const B = await queue.enqueue('B', {}, { tags: ['B'], scheduleAfterTags: ['A'] });
82
- const A = await queue.enqueue('A', {}, { tags: ['A'] });
83
- // Run A
72
+ const taskA = await queue.enqueue('A', {});
73
+ const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id] });
74
+ const taskC = await queue.enqueue('C', {}, { scheduleAfter: [taskB.id] });
75
+ const taskD = await queue.enqueue('D', {}, { scheduleAfter: [taskC.id] });
84
76
  await queue.complete((await queue.dequeue({ types: ['A'] })));
85
- await waitForStatus(B.id, TaskStatus.Pending);
86
- // Run B
77
+ await waitForStatus(taskB.id, TaskStatus.Pending);
87
78
  await queue.complete((await queue.dequeue({ types: ['B'] })));
88
- await waitForStatus(C.id, TaskStatus.Pending);
89
- // Run C
79
+ await waitForStatus(taskC.id, TaskStatus.Pending);
90
80
  await queue.complete((await queue.dequeue({ types: ['C'] })));
91
- await waitForStatus(D.id, TaskStatus.Pending);
92
- expect((await queue.getTask(D.id))?.status).toBe(TaskStatus.Pending);
81
+ await waitForStatus(taskD.id, TaskStatus.Pending);
93
82
  });
94
- it('should propagate cancellation down the dependency tree', async () => {
95
- // Use parentId for explicit tree structure which `cancel` supports
96
- const root = await queue.enqueue('root', {});
97
- const child = await queue.enqueue('child', {}, { parentId: root.id });
98
- const grandChild = await queue.enqueue('grand', {}, { parentId: child.id });
99
- await queue.cancel(root.id);
100
- expect((await queue.getTask(root.id))?.status).toBe(TaskStatus.Cancelled);
101
- expect((await queue.getTask(child.id))?.status).toBe(TaskStatus.Cancelled);
102
- expect((await queue.getTask(grandChild.id))?.status).toBe(TaskStatus.Cancelled);
83
+ it('should respect failFast = false (continue other branches)', async () => {
84
+ const taskA = await queue.enqueue('A', {});
85
+ const taskB = await queue.enqueue('B', {}, { scheduleAfter: [taskA.id], failFast: false });
86
+ const taskC = await queue.enqueue('C', {}); // Independent
87
+ const dA = await queue.dequeue({ types: ['A'] });
88
+ await queue.fail(dA, new Error('fatal'), { fatal: true });
89
+ // taskB should stay Waiting (default requiredStatus is Completed)
90
+ // If failFast is false, it should transition to Pending once the dependency is terminal, even if it failed.
91
+ await timeout(100);
92
+ const uB = await queue.getTask(taskB.id);
93
+ expect(uB?.status).toBe(TaskStatus.Pending);
94
+ const dC = await queue.dequeue({ types: ['C'] });
95
+ expect(dC?.id).toBe(taskC.id);
103
96
  });
104
97
  });
105
- describe('Scheduling & Priorities', () => {
106
- it('should promote priority of old pending tasks (Aging)', async () => {
98
+ describe('Other Scenarios', () => {
99
+ it('should handle priority aging', async () => {
107
100
  const t1 = await queue.enqueue('low', {}, { priority: 2000 });
108
- // Wait for aging interval (50ms)
109
- await timeout(60);
110
- await queue.maintenance();
111
- const updated = await queue.getTask(t1.id);
112
- // Default step is 10. 2000 - 10 = 1990
113
- expect(updated?.priority).toBe(1990);
114
- });
115
- it('should respect exponential backoff on failure', async () => {
116
- const task = await queue.enqueue('fail-test', {});
117
- // Try 1
118
- const d1 = await queue.dequeue();
119
- await queue.fail(d1, new Error('fail 1'));
120
- const u1 = await queue.getTask(task.id);
121
- expect(u1?.tries).toBe(1);
122
- const delay1 = u1.scheduleTimestamp - currentTimestamp();
123
- expect(delay1).toBeGreaterThan(20); // Approx check
124
- // Force reschedule to now
125
- await queue.reschedule(task.id, currentTimestamp());
126
- // Try 2
127
- const d2 = await queue.dequeue();
128
- await queue.fail(d2, new Error('fail 2'));
129
- const u2 = await queue.getTask(task.id);
130
- expect(u2?.tries).toBe(2);
131
- const now = currentTimestamp();
132
- expect(u2.scheduleTimestamp > now + 50).toBe(true);
133
- });
134
- });
135
- describe('Rate Limiting & Concurrency', () => {
136
- it('should limit burst dequeue rate', async () => {
137
- // Rate limit 5, interval 100ms
138
- await queue.enqueueMany(Array.from({ length: 10 }, (_, i) => ({ type: 'burst', data: { i } })));
139
- // Request burstCapacity (5)
140
- const batch1 = await queue.dequeueMany(5);
141
- expect(batch1.length).toBe(5);
142
- // Try immediately again
143
- const batch2 = await queue.dequeueMany(1);
144
- expect(batch2.length).toBe(0); // Rate limited
145
- // Wait for refill
146
- await timeout(300);
147
- const batch3 = await queue.dequeueMany(5);
148
- expect(batch3.length).toBe(5); // Refilled
149
- });
150
- it('should respect global concurrency limit', async () => {
151
- const queueProvider = injector.resolve(TaskQueueProvider);
152
- const limitQueue = queueProvider.get(`limit-${Date.now()}`, { globalConcurrency: 2 });
153
- await limitQueue.enqueueMany([
154
- { type: 'c', data: {} },
155
- { type: 'c', data: {} },
156
- { type: 'c', data: {} },
157
- ]);
158
- const t1 = await limitQueue.dequeue();
159
- const t2 = await limitQueue.dequeue();
160
- const t3 = await limitQueue.dequeue();
161
- expect(t1).toBeDefined();
162
- expect(t2).toBeDefined();
163
- expect(t3).toBeUndefined(); // Max concurrency reached
164
- await limitQueue.complete(t1);
165
- const t3_retry = await limitQueue.dequeue();
166
- expect(t3_retry).toBeDefined();
167
- await limitQueue.clear();
168
- });
169
- });
170
- describe('Archival & Maintenance', () => {
171
- it('should archive completed tasks after retention period', async () => {
172
- const qProvider = injector.resolve(TaskQueueProvider);
173
- const archiveQueue = qProvider.get(`archive-${Date.now()}-${Math.random()}`, { retention: 100 });
174
- const task = await archiveQueue.enqueue('archive-me', {});
175
- const d = await archiveQueue.dequeue();
176
- await archiveQueue.complete(d);
177
- // Verify it is in main table
178
- const before = await archiveQueue.getTask(task.id);
179
- expect(before).toBeDefined();
180
- expect(before?.status).toBe(TaskStatus.Completed);
181
- expect(before.completeTimestamp > 0).toBe(true);
182
- // Wait for retention (50ms).
183
- await timeout(60);
184
- await archiveQueue.maintenance();
185
- // Should move from main table to archive
186
- const loaded = await archiveQueue.getTask(task.id);
187
- expect(loaded).toBeDefined();
188
- expect(loaded?.status).toBe(TaskStatus.Completed);
189
- await archiveQueue.clear();
190
- });
191
- it('should prune expired pending tasks', async () => {
192
- // Time to live: 50ms
193
- const task = await queue.enqueue('expire-me', {}, { timeToLive: currentTimestamp() + 50 });
194
- await timeout(60);
195
- await queue.maintenance();
196
- const updated = await queue.getTask(task.id);
197
- expect(updated?.status).toBe(TaskStatus.Dead);
198
- expect((updated?.error)['code']).toBe('Expired');
199
- });
200
- it('should retrieve task from archive', async () => {
201
- // Manually insert into archive? We can't access archiveRepository directly.
202
- // But we can use maintenance to force it.
203
- // Wait, if the previous test failed, this might also fail.
204
- // But let's try with a VERY clear scenario.
205
- const task = await queue.enqueue('archive-retrieval-test', {});
206
- const d = await queue.dequeue();
207
- await queue.complete(d);
208
- // Force move
209
- await timeout(60);
101
+ const t2 = await queue.enqueue('high', {}, { priority: 1000 });
102
+ // Wait for aging to trigger
103
+ await timeout(100);
210
104
  await queue.maintenance();
211
- // Verify retrieval
212
- const fromArchive = await queue.getTask(task.id);
213
- expect(fromArchive).toBeDefined();
214
- expect(fromArchive?.status).toBe(TaskStatus.Completed);
215
- });
216
- it('should defer archival of parent tasks until children are archived', async () => {
217
- const qProvider = injector.resolve(TaskQueueProvider);
218
- const treeQueue = qProvider.get(`archive-tree-${Date.now()}`, { retention: 50 });
219
- const parent = await treeQueue.enqueue('parent', {});
220
- const child = await treeQueue.enqueue('child', {}, { parentId: parent.id });
221
- const d1 = await treeQueue.dequeue();
222
- const d2 = await treeQueue.dequeue();
223
- await treeQueue.complete(d1);
224
- await treeQueue.complete(d2);
225
- // Wait for retention
226
- await timeout(60);
227
- // First maintenance: should archive child, but parent stays because child is still in main table (until it's deleted in the same tx maybe? No, loadMany happens before delete)
228
- await treeQueue.maintenance();
229
- const parentStillActive = await treeQueue.getTask(parent.id);
230
- const childInArchive = await treeQueue.getTask(child.id);
231
- // Verify child moved
232
- expect(childInArchive).toBeDefined();
233
- // Parent MUST still be in main table because child was present when archival query ran
234
- expect(parentStillActive).toBeDefined();
235
- // Second maintenance: should now archive parent because child is gone from main table
236
- await treeQueue.maintenance();
237
- const finalParent = await treeQueue.getTask(parent.id);
238
- expect(finalParent).toBeDefined(); // Still defined (getTask checks both main and archive)
239
- // We need to check if it's REALLY archived.
240
- // We can use the #repository directly if we were in the class, but here we can check if it moved by checking against the main table?
241
- // Actually getTask is implemented as:
242
- /*
243
- const active = await this.#repository.tryLoadByQuery({ namespace: this.#namespace, id });
244
- if (isDefined(active)) return active;
245
- return await this.#archiveRepository.tryLoadByQuery({ namespace: this.#namespace, id });
246
- */
247
- await treeQueue.clear();
248
- });
249
- });
250
- describe('Batch & Bulk Operations', () => {
251
- it('should enqueue and dequeue large batches', async () => {
252
- const items = Array.from({ length: 50 }, (_, i) => ({ type: 'bulk', data: { i } }));
253
- const queueProvider = injector.resolve(TaskQueueProvider);
254
- const bulkQueue = queueProvider.get(`bulk-${Date.now()}`, { rateLimit: 100 });
255
- await bulkQueue.enqueueMany(items);
256
- const count = await bulkQueue.countByTags([]); // All -> 0 because empty intersection
257
- expect(count).toBe(0);
258
- const batch = await bulkQueue.dequeueMany(50);
259
- expect(batch.length).toBe(50);
260
- await bulkQueue.completeMany(batch);
261
- await bulkQueue.clear();
262
- });
263
- it('should handle idempotency window', async () => {
264
- const key = 'idem-test';
265
- const t1 = await queue.enqueue('t', { v: 1 }, { idempotencyKey: key });
266
- const t2 = await queue.enqueue('t', { v: 2 }, { idempotencyKey: key });
267
- expect(t1.id).toBe(t2.id); // Deduplicated
268
- const t3 = await queue.enqueue('t', { v: 3 }, { idempotencyKey: key, replace: true });
269
- expect(t3.id).not.toBe(t1.id); // Replaced
270
- expect(t3.data['v']).toBe(3);
271
- });
272
- });
273
- describe('Edge Cases', () => {
274
- it('should not fan-in if dependency fails and failFast=false', async () => {
275
- const dependent = await queue.enqueue('dep', {}, { scheduleAfterTags: ['fail-tag'], failFast: false });
276
- const prereq = await queue.enqueue('pre', {}, { tags: ['fail-tag'] });
277
- const d = await queue.dequeue({ types: ['pre'] });
278
- await queue.fail(d, new Error('fail'));
279
- await queue.processPendingFanIn();
280
- // Retry check because fan-in might be processed by background worker asynchronously
281
- let u = await queue.getTask(dependent.id);
282
- for (let i = 0; i < 5; i++) {
283
- if (u?.status == TaskStatus.Waiting)
284
- break;
285
- await timeout(10);
286
- u = await queue.getTask(dependent.id);
105
+ const u1 = await queue.getTask(t1.id);
106
+ const u2 = await queue.getTask(t2.id);
107
+ expect(u1.priority).toBeLessThan(2000);
108
+ expect(u2.priority).toBeLessThan(1000);
109
+ });
110
+ it('should handle rate limiting', async () => {
111
+ // Limit is 5 per 200ms
112
+ for (let i = 0; i < 10; i++) {
113
+ await queue.enqueue('task', { i });
287
114
  }
288
- expect(u?.status).toBe(TaskStatus.Waiting); // Should still be waiting because dependency didn't Complete
289
- });
290
- it('should handle mixed AND/OR dependencies', async () => {
291
- const dep = await queue.enqueue('dep', {}, {
292
- scheduleAfterTags: ['A', 'B'],
293
- dependencyJoinMode: DependencyJoinMode.Or,
294
- });
295
- const A = await queue.enqueue('A', {}, { tags: ['A'] });
296
- await queue.complete((await queue.dequeue({ types: ['A'] })));
297
- await waitForStatus(dep.id, TaskStatus.Pending);
298
- expect((await queue.getTask(dep.id))?.status).toBe(TaskStatus.Pending);
299
- });
300
- it('should not reschedule if task is not running', async () => {
301
- const task = await queue.enqueue('t', {});
302
- const res = await queue.touch(task);
303
- expect(res).toBeUndefined();
115
+ const batch1 = await queue.dequeueMany(10);
116
+ expect(batch1.length).toBe(5);
117
+ await timeout(500); // Increased from 250
118
+ const batch2 = await queue.dequeueMany(10);
119
+ expect(batch2.length).toBe(5);
304
120
  });
305
121
  });
306
122
  });
@@ -0,0 +1 @@
1
+ export {};