@tstdl/base 0.93.139 → 0.93.140

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 (133) hide show
  1. package/README.md +166 -0
  2. package/ai/genkit/multi-region.plugin.js +5 -3
  3. package/ai/genkit/tests/multi-region.test.d.ts +1 -0
  4. package/ai/genkit/tests/multi-region.test.js +5 -2
  5. package/ai/parser/parser.js +2 -2
  6. package/ai/prompts/build.js +1 -0
  7. package/ai/prompts/instructions-formatter.d.ts +15 -2
  8. package/ai/prompts/instructions-formatter.js +36 -31
  9. package/ai/prompts/prompt-builder.js +5 -5
  10. package/ai/prompts/steering.d.ts +3 -2
  11. package/ai/prompts/steering.js +3 -1
  12. package/ai/tests/instructions-formatter.test.js +1 -0
  13. package/api/README.md +403 -0
  14. package/api/client/client.js +7 -13
  15. package/api/client/tests/api-client.test.js +10 -10
  16. package/api/default-error-handlers.js +1 -1
  17. package/api/response.d.ts +2 -2
  18. package/api/response.js +22 -33
  19. package/api/server/api-controller.d.ts +1 -1
  20. package/api/server/api-controller.js +3 -3
  21. package/api/server/api-request-token.provider.d.ts +1 -0
  22. package/api/server/api-request-token.provider.js +1 -0
  23. package/api/server/middlewares/allowed-methods.middleware.js +2 -1
  24. package/api/server/middlewares/content-type.middleware.js +2 -1
  25. package/api/types.d.ts +3 -2
  26. package/application/README.md +240 -0
  27. package/application/application.js +2 -2
  28. package/audit/README.md +267 -0
  29. package/authentication/README.md +288 -0
  30. package/authentication/client/authentication.service.d.ts +12 -11
  31. package/authentication/client/authentication.service.js +21 -21
  32. package/authentication/client/http-client.middleware.js +2 -2
  33. package/authentication/tests/authentication.client-error-handling.test.js +2 -1
  34. package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
  35. package/browser/README.md +401 -0
  36. package/cancellation/README.md +156 -0
  37. package/cancellation/tests/coverage.test.d.ts +1 -0
  38. package/cancellation/tests/coverage.test.js +49 -0
  39. package/cancellation/tests/leak.test.js +24 -29
  40. package/cancellation/tests/token.test.d.ts +1 -0
  41. package/cancellation/tests/token.test.js +136 -0
  42. package/cancellation/token.d.ts +53 -177
  43. package/cancellation/token.js +132 -208
  44. package/context/README.md +174 -0
  45. package/cookie/README.md +161 -0
  46. package/css/README.md +157 -0
  47. package/data-structures/README.md +320 -0
  48. package/decorators/README.md +140 -0
  49. package/distributed-loop/README.md +231 -0
  50. package/distributed-loop/distributed-loop.js +1 -1
  51. package/document-management/README.md +403 -0
  52. package/document-management/server/services/document-management.service.js +9 -7
  53. package/document-management/tests/document-management-core.test.js +2 -7
  54. package/document-management/tests/document-management.api.test.js +6 -7
  55. package/document-management/tests/document-statistics.service.test.js +11 -12
  56. package/document-management/tests/document.service.test.js +3 -3
  57. package/document-management/tests/enum-helpers.test.js +2 -3
  58. package/dom/README.md +213 -0
  59. package/enumerable/README.md +259 -0
  60. package/enumeration/README.md +121 -0
  61. package/errors/README.md +267 -0
  62. package/file/README.md +191 -0
  63. package/formats/README.md +210 -0
  64. package/function/README.md +144 -0
  65. package/http/README.md +318 -0
  66. package/http/client/adapters/undici.adapter.js +1 -1
  67. package/http/client/http-client-request.d.ts +6 -5
  68. package/http/client/http-client-request.js +8 -9
  69. package/http/server/node/node-http-server.js +1 -2
  70. package/image-service/README.md +137 -0
  71. package/injector/README.md +491 -0
  72. package/intl/README.md +113 -0
  73. package/json-path/README.md +182 -0
  74. package/jsx/README.md +154 -0
  75. package/key-value-store/README.md +191 -0
  76. package/lock/README.md +249 -0
  77. package/lock/web/web-lock.js +119 -47
  78. package/logger/README.md +287 -0
  79. package/mail/README.md +256 -0
  80. package/memory/README.md +144 -0
  81. package/message-bus/README.md +244 -0
  82. package/message-bus/message-bus-base.js +1 -1
  83. package/module/README.md +182 -0
  84. package/module/module.d.ts +1 -1
  85. package/module/module.js +77 -17
  86. package/module/modules/web-server.module.js +1 -1
  87. package/notification/tests/notification-type.service.test.js +24 -15
  88. package/object-storage/README.md +300 -0
  89. package/openid-connect/README.md +274 -0
  90. package/orm/README.md +423 -0
  91. package/package.json +8 -6
  92. package/password/README.md +164 -0
  93. package/pdf/README.md +246 -0
  94. package/polyfills.js +1 -0
  95. package/pool/README.md +198 -0
  96. package/process/README.md +237 -0
  97. package/promise/README.md +252 -0
  98. package/promise/cancelable-promise.js +1 -1
  99. package/random/README.md +193 -0
  100. package/reflection/README.md +305 -0
  101. package/rpc/README.md +386 -0
  102. package/rxjs-utils/README.md +262 -0
  103. package/schema/README.md +342 -0
  104. package/serializer/README.md +342 -0
  105. package/signals/implementation/README.md +134 -0
  106. package/sse/README.md +278 -0
  107. package/task-queue/README.md +300 -0
  108. package/task-queue/postgres/task-queue.d.ts +2 -1
  109. package/task-queue/postgres/task-queue.js +32 -2
  110. package/task-queue/task-context.js +1 -1
  111. package/task-queue/task-queue.d.ts +17 -0
  112. package/task-queue/task-queue.js +103 -45
  113. package/task-queue/tests/complex.test.js +4 -4
  114. package/task-queue/tests/dependencies.test.js +4 -2
  115. package/task-queue/tests/queue.test.js +111 -0
  116. package/task-queue/tests/worker.test.js +21 -13
  117. package/templates/README.md +287 -0
  118. package/testing/README.md +157 -0
  119. package/text/README.md +346 -0
  120. package/threading/README.md +238 -0
  121. package/types/README.md +311 -0
  122. package/utils/README.md +322 -0
  123. package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
  124. package/utils/async-iterable-helpers/observable-iterable.js +4 -8
  125. package/utils/async-iterable-helpers/take-until.js +4 -4
  126. package/utils/backoff.js +89 -30
  127. package/utils/retry-with-backoff.js +1 -1
  128. package/utils/timer.d.ts +1 -1
  129. package/utils/timer.js +5 -7
  130. package/utils/timing.d.ts +1 -1
  131. package/utils/timing.js +2 -4
  132. package/utils/z-base32.d.ts +1 -0
  133. package/utils/z-base32.js +1 -0
package/sse/README.md ADDED
@@ -0,0 +1,278 @@
1
+ # @tstdl/base/sse
2
+
3
+ A comprehensive module for Server-Sent Events (SSE) that provides both a low-level reactive client for discrete events and a high-level, delta-capable data streaming abstraction for synchronizing complex objects efficiently.
4
+
5
+ ## Table of Contents
6
+
7
+ - [✨ Features](#-features)
8
+ - [Core Concepts](#core-concepts)
9
+ - [High-Level: Data Streaming](#high-level-data-streaming)
10
+ - [Low-Level: Event Pushing](#low-level-event-pushing)
11
+ - [🚀 Basic Usage](#-basic-usage)
12
+ - [Server-Side Data Streaming](#server-side-data-streaming)
13
+ - [Client-Side Data Consumption](#client-side-data-consumption)
14
+ - [🔧 Advanced Topics](#-advanced-topics)
15
+ - [Low-Level Event Pushing](#low-level-event-pushing-1)
16
+ - [Handling Errors](#handling-errors)
17
+ - [📚 API](#-api)
18
+
19
+ ## ✨ Features
20
+
21
+ - **Isomorphic Design**: Type-safe utilities for both server (Node.js/Edge) and client (Browser) environments.
22
+ - **Smart Data Synchronization**: `DataStreamSource` automatically calculates JSON diffs (deltas) between updates to minimize bandwidth usage. For optimal array synchronization, ensure your objects have an `id` property.
23
+ - **Automatic State Reconstruction**: The client-side `DataStream` utility automatically patches the local state with incoming deltas, exposing a seamless RxJS Observable of the full object.
24
+ - **Reactive Client**: A robust RxJS wrapper around the native `EventSource` API (`ServerSentEvents`) for handling connection states and events.
25
+ - **Web Streams Support**: Server-side implementation uses standard `ReadableStream`, making it compatible with modern runtimes.
26
+ - **Full SSE Specification**: Supports named events, custom IDs, retry intervals, and comments.
27
+
28
+ ## Core Concepts
29
+
30
+ ### High-Level: Data Streaming
31
+
32
+ The **Data Streaming** abstraction is designed for synchronizing a single, potentially complex state object between the server and client.
33
+
34
+ - **Server (`DataStreamSource`)**: You simply pass the full object to the source whenever it changes. The source uses `jsondiffpatch` to calculate the difference between the current and previous object. It sends the full object initially, and then only the small deltas for subsequent updates.
35
+ - **Client (`DataStream`)**: Consumes the stream, listening for `data` (full snapshot) and `delta` (patch) events. It maintains the local state by applying patches automatically and emits the fully updated object to subscribers.
36
+
37
+ ### Low-Level: Event Pushing
38
+
39
+ The **Event Pushing** layer provides direct control over the SSE protocol, suitable for sending discrete notifications (e.g., "OrderShipped", "NewMessage").
40
+
41
+ - **Server (`ServerSentEventsSource`)**: A wrapper around a `TransformStream` that formats messages according to the SSE spec.
42
+ - **Client (`ServerSentEvents`)**: Wraps the browser's `EventSource` to provide RxJS Observables for messages, connection status, and errors.
43
+
44
+ ## 🚀 Basic Usage
45
+
46
+ ### Server-Side Data Streaming
47
+
48
+ Use `DataStreamSource` to stream state changes. The most convenient way is `DataStreamSource.fromIterable`.
49
+
50
+ ```typescript
51
+ import { apiController, type ApiServerResult } from '@tstdl/base/api/server';
52
+ import { defineApi } from '@tstdl/base/api';
53
+ import { DataStream, DataStreamSource } from '@tstdl/base/sse';
54
+ import { CancellationSignal } from '@tstdl/base/cancellation';
55
+ import { inject } from '@tstdl/base/injector';
56
+ import { timeout } from '@tstdl/base/utils/timing';
57
+ import { object, string, number } from '@tstdl/base/schema';
58
+
59
+ // 1. Define the data shape
60
+ const StockPrice = object({
61
+ symbol: string(),
62
+ price: number(),
63
+ timestamp: number(),
64
+ });
65
+ type StockPrice = typeof StockPrice.T;
66
+
67
+ // 2. Define the API
68
+ const stockApi = defineApi({
69
+ resource: 'stocks',
70
+ endpoints: {
71
+ track: {
72
+ resource: ':symbol/stream',
73
+ method: 'GET',
74
+ parameters: object({ symbol: string() }),
75
+ result: DataStream<StockPrice>, // Type hint for the client
76
+ },
77
+ },
78
+ });
79
+
80
+ // 3. Implement the Controller
81
+ @apiController(stockApi)
82
+ class StockApiController {
83
+ #cancellationSignal = inject(CancellationSignal);
84
+
85
+ track({ parameters }: any): ApiServerResult<typeof stockApi, 'track'> {
86
+ // Create an async generator that yields data updates
87
+ const ticker = this.createTicker(parameters.symbol, this.#cancellationSignal);
88
+
89
+ // Create a source that automatically handles diffing and serialization
90
+ return DataStreamSource.fromIterable(ticker, { delta: true });
91
+ }
92
+
93
+ async *createTicker(symbol: string, signal: CancellationSignal): AsyncIterable<StockPrice> {
94
+ let price = 100;
95
+ while (signal.isUnset) {
96
+ price += (Math.random() - 0.5) * 2;
97
+ yield { symbol, price, timestamp: Date.now() };
98
+ await timeout(1000, signal);
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ ### Client-Side Data Consumption
105
+
106
+ Use `DataStream.parse` to transform the raw SSE connection into a stream of your data objects.
107
+
108
+ ```typescript
109
+ import { DataStream, ServerSentEvents } from '@tstdl/base/sse';
110
+ import { type Observable } from 'rxjs';
111
+
112
+ type StockPrice = {
113
+ symbol: string;
114
+ price: number;
115
+ timestamp: number;
116
+ };
117
+
118
+ // 1. Establish the connection
119
+ const sse = new ServerSentEvents('/api/stocks/AAPL/stream');
120
+
121
+ // 2. Parse the data stream
122
+ // This handles the initial full load and all subsequent delta patches automatically
123
+ const stock$: Observable<StockPrice> = DataStream.parse<StockPrice>(sse);
124
+
125
+ // 3. Subscribe to updates
126
+ const subscription = stock$.subscribe({
127
+ next: (stock) => {
128
+ console.log(`Update for ${stock.symbol}: $${stock.price.toFixed(2)}`);
129
+ },
130
+ error: (err) => console.error('Stream error:', err),
131
+ complete: () => console.log('Stream closed'),
132
+ });
133
+
134
+ // Cleanup when done
135
+ // subscription.unsubscribe();
136
+ ```
137
+
138
+ ## 🔧 Advanced Topics
139
+
140
+ ### Low-Level Event Pushing
141
+
142
+ If you don't need object synchronization and just want to send named events, use `ServerSentEventsSource` directly.
143
+
144
+ **Server:**
145
+
146
+ ```typescript
147
+ import { ServerSentEventsSource } from '@tstdl/base/sse';
148
+ import { timeout } from '@tstdl/base/utils/timing';
149
+
150
+ async function notificationStream(): Promise<ServerSentEventsSource> {
151
+ const source = new ServerSentEventsSource();
152
+
153
+ // Run in background
154
+ (async () => {
155
+ await timeout(1000);
156
+ if (source.closed()) return;
157
+
158
+ // Send a named JSON event
159
+ await source.sendJson({
160
+ name: 'notification',
161
+ id: '1',
162
+ data: { message: 'Hello World' },
163
+ });
164
+
165
+ await timeout(2000);
166
+ if (source.closed()) return;
167
+
168
+ // Send a text event
169
+ await source.sendText({
170
+ name: 'ping',
171
+ data: 'keep-alive',
172
+ });
173
+
174
+ await source.close();
175
+ })();
176
+
177
+ return source;
178
+ }
179
+ ```
180
+
181
+ **Client:**
182
+
183
+ ```typescript
184
+ import { ServerSentEvents } from '@tstdl/base/sse';
185
+ import { filter, map } from 'rxjs';
186
+
187
+ const sse = new ServerSentEvents('/api/notifications');
188
+
189
+ // Listen specifically for 'notification' events
190
+ const notifications$ = sse.message$('notification').pipe(map((event) => JSON.parse(event.data)));
191
+
192
+ notifications$.subscribe((data) => console.log('New notification:', data));
193
+ ```
194
+
195
+ ### Handling Errors
196
+
197
+ The `DataStreamSource` has a built-in mechanism to send errors to the client before closing the stream.
198
+
199
+ ```typescript
200
+ // Server
201
+ const source = new DataStreamSource();
202
+ try {
203
+ // ... logic
204
+ } catch (err) {
205
+ // Sends an 'error' event with the formatted error and closes the connection
206
+ await source.error(err);
207
+ }
208
+ ```
209
+
210
+ On the client side, `DataStream.parse` will listen for these `error` events and error out the Observable stream accordingly.
211
+
212
+ ## 📚 API
213
+
214
+ ### `DataStreamSource<T>`
215
+
216
+ Server-side class for streaming data objects with optional delta compression.
217
+
218
+ | Member | Signature | Description |
219
+ | :------------- | :----------------------------------------------------------- | :---------------------------------------------------------------------------------- |
220
+ | `constructor` | `(options?: DataStreamSourceOptions)` | Creates a new source. Options include `delta` (boolean) and `errorFormatter`. |
221
+ | `fromIterable` | `static fromIterable<T>(iterable: AnyIterable<T>, options?)` | Creates a source that automatically pulls from an async iterable and sends updates. |
222
+ | `send` | `send(data: T): Promise<void>` | Sends a data update. If `delta` is enabled, calculates diff from previous data. |
223
+ | `error` | `error(error: unknown): Promise<void>` | Sends an error event to the client and closes the stream. |
224
+ | `close` | `close(): Promise<void>` | Closes the underlying stream. |
225
+ | `closed` | `ReadonlySignal<boolean>` | Signal indicating if the stream is closed. |
226
+ | `eventSource` | `ServerSentEventsSource` | Access to the underlying SSE source. |
227
+
228
+ ### `DataStream<T>`
229
+
230
+ Client-side utility for reconstructing data streams.
231
+
232
+ | Member | Signature | Description |
233
+ | :------ | :-------------------------------------------------------------- | :----------------------------------------------------------------------------------- |
234
+ | `parse` | `static parse<T>(eventSource: ServerSentEvents): Observable<T>` | Transforms an SSE connection into an Observable of the synchronized data object `T`. |
235
+
236
+ ### `ServerSentEventsSource`
237
+
238
+ Low-level server-side SSE writer.
239
+
240
+ | Member | Signature | Description |
241
+ | :------------ | :---------------------------------------------------- | :------------------------------------------------------------------------ |
242
+ | `readable` | `ReadableStream<string>` | The Web Stream to return in the HTTP response body. |
243
+ | `sendJson` | `sendJson(event: ServerSentJsonEvent): Promise<void>` | Sends a named event with JSON data. |
244
+ | `sendText` | `sendText(event: ServerSentTextEvent): Promise<void>` | Sends a named event with raw text data. |
245
+ | `sendComment` | `sendComment(comment: string): Promise<void>` | Sends an SSE comment (ignored by browser clients, useful for keep-alive). |
246
+ | `close` | `close(): Promise<void>` | Closes the stream. |
247
+ | `closed` | `ReadonlySignal<boolean>` | Signal indicating if the stream is closed. |
248
+ | `error` | `ReadonlySignal<Error \| undefined>` | Signal containing the error if the stream failed. |
249
+
250
+ ### `ServerSentEvents`
251
+
252
+ Client-side reactive wrapper for `EventSource`.
253
+
254
+ | Member | Signature | Description |
255
+ | :-------------- | :--------------------------------------------------- | :------------------------------------------------------------------------------------- |
256
+ | `constructor` | `(url: string, options?: EventSourceInit)` | Initiates the connection. |
257
+ | `message$` | `message$(event?: string): Observable<MessageEvent>` | Returns an Observable of messages. If `event` is provided, filters by that event name. |
258
+ | `state$` | `Observable<ServerSentEventsState>` | Emits connection state changes (`Connecting`, `Open`, `Closed`). |
259
+ | `open$` | `Observable<void>` | Emits when the connection is open. |
260
+ | `close$` | `Observable<void>` | Emits when the connection is closed. |
261
+ | `error$` | `Observable<Event>` | Emits on connection errors. |
262
+ | `isOpen$` | `Observable<boolean>` | Emits `true` when connected. |
263
+ | `isConnecting$` | `Observable<boolean>` | Emits `true` when connecting. |
264
+ | `isClosed$` | `Observable<boolean>` | Emits `true` when closed. |
265
+ | `state` | `ServerSentEventsState` | Gets the current connection state. |
266
+ | `close` | `close(): void` | Manually closes the connection. |
267
+
268
+ ### Types
269
+
270
+ | Type | Description |
271
+ | :-------------------------- | :-------------------------------------------------------------------------------- |
272
+ | `DataStreamSourceOptions` | Options for `DataStreamSource`: `delta` (boolean) and `errorFormatter` (function). |
273
+ | `DataStreamErrorFormatter` | Function type: `(error: unknown) => UndefinableJson`. |
274
+ | `ServerSentEventBase<Data>` | Base interface for events with `name`, `data`, `id`, and `retry`. |
275
+ | `ServerSentJsonEvent` | Event where `data` is any JSON-serializable value. |
276
+ | `ServerSentTextEvent` | Event where `data` is a string. |
277
+ | `ServerSentEvent` | Union of `ServerSentTextEvent` and `ServerSentJsonEvent`. |
278
+ | `ServerSentEventsState` | Enum: `Connecting` (0), `Open` (1), `Closed` (2). |
@@ -0,0 +1,300 @@
1
+ # Task Queue
2
+
3
+ A robust, type-safe, and distributed task orchestration system backed by PostgreSQL. It supports prioritization, hierarchical task dependencies, batch operations, circuit breaking, and automatic retries with exponential backoff.
4
+
5
+ ## Table of Contents
6
+
7
+ - [✨ Features](#-features)
8
+ - [Core Concepts](#core-concepts)
9
+ - [🚀 Basic Usage](#-basic-usage)
10
+ - [Configuration](#configuration)
11
+ - [Defining Tasks](#defining-tasks)
12
+ - [Enqueueing Tasks](#enqueueing-tasks)
13
+ - [Processing Tasks](#processing-tasks)
14
+ - [🔧 Advanced Topics](#-advanced-topics)
15
+ - [Task Hierarchies (Parent/Child)](#task-hierarchies-parentchild)
16
+ - [Batch Operations](#batch-operations)
17
+ - [Deduplication (Idempotency)](#deduplication-idempotency)
18
+ - [Prioritization](#prioritization)
19
+ - [Transactional Enqueueing](#transactional-enqueueing)
20
+ - [Manual Consumption](#manual-consumption)
21
+ - [📚 API](#-api)
22
+
23
+ ## ✨ Features
24
+
25
+ - **PostgreSQL Backend**: Reliable persistence using Drizzle ORM with row-level locking (`SKIP LOCKED`) for safe concurrency.
26
+ - **Type-Safe**: Fully typed task data, state, and result payloads using a `TaskDefinitionMap`.
27
+ - **Task Hierarchies**: Support for parent-child relationships where parents wait for children to complete (Fan-Out/Fan-In).
28
+ - **Prioritization**: Integer-based priority system with automatic aging to prevent starvation.
29
+ - **Deduplication**: Idempotency keys with configurable strategies (`replace: true/false`) and retention windows.
30
+ - **Resilience**: Built-in Circuit Breaker, automatic retries with exponential backoff, and zombie task recovery.
31
+ - **Observability**: Tracks progress, state, and detailed error information.
32
+
33
+ ## Core Concepts
34
+
35
+ ### TaskQueue
36
+
37
+ The `TaskQueue<Definitions>` class is the primary entry point. It manages the lifecycle of tasks, handles persistence, and provides the worker loop abstraction.
38
+
39
+ ### Task
40
+
41
+ A unit of work defined by its `type`. Key properties include:
42
+
43
+ - **Status**: `Pending` → `Running` → `Completed` (or `Failed`/`Dead`/`Cancelled`/`Waiting`).
44
+ - **Lease**: A temporary lock (`visibilityDeadline`) held by a worker during execution.
45
+ - **Tries**: Number of attempts made to process the task.
46
+ - **Data/State/Result**: Managed, type-safe JSON payloads.
47
+
48
+ ### Worker
49
+
50
+ A function that processes tasks. The `TaskQueue` provides a managed loop (`process`) that handles:
51
+
52
+ 1. **Dequeueing**: Locking tasks from the database.
53
+ 2. **Heartbeating**: Automatically extending the lease (`touch`) while the worker runs.
54
+ 3. **Completion**: Resolving the task state based on the handler's return value.
55
+
56
+ ## 🚀 Basic Usage
57
+
58
+ ### Configuration
59
+
60
+ Register the PostgreSQL backend in your application bootstrap.
61
+
62
+ ```typescript
63
+ import { configurePostgresTaskQueue, migratePostgresTaskQueueSchema } from '@tstdl/base/task-queue/postgres';
64
+ import { runInInjectionContext, inject } from '@tstdl/base/injector';
65
+ import { Injector } from '@tstdl/base/injector';
66
+
67
+ export async function bootstrap() {
68
+ const injector = inject(Injector);
69
+
70
+ // Configure the module (uses default DatabaseConfig from context)
71
+ configurePostgresTaskQueue();
72
+
73
+ // Run migrations to create the 'task_queue.task' table
74
+ await runInInjectionContext(injector, migratePostgresTaskQueueSchema);
75
+ }
76
+ ```
77
+
78
+ ### Defining Tasks
79
+
80
+ Define your task types and their payload shapes using a `TaskDefinitionMap`.
81
+
82
+ ```typescript
83
+ import { TaskDefinitionMap } from '@tstdl/base/task-queue';
84
+
85
+ export type MyTasks = TaskDefinitionMap<{
86
+ 'send-email': {
87
+ data: { to: string; subject: string; body: string };
88
+ state: { sentAt?: number };
89
+ result: { messageId: string };
90
+ };
91
+ 'generate-report': {
92
+ data: { reportId: string };
93
+ state: { progress: number };
94
+ result: void;
95
+ };
96
+ }>;
97
+ ```
98
+
99
+ ### Enqueueing Tasks
100
+
101
+ Inject the `TaskQueue` service.
102
+
103
+ ```typescript
104
+ import { inject, Singleton } from '@tstdl/base/injector';
105
+ import { TaskQueue } from '@tstdl/base/task-queue';
106
+
107
+ @Singleton()
108
+ export class MailService {
109
+ // Inject a queue named 'mail-queue' with our definitions
110
+ readonly #queue = inject<TaskQueue<MyTasks>>(TaskQueue, 'mail-queue');
111
+
112
+ async sendLater(to: string, subject: string, body: string): Promise<void> {
113
+ await this.#queue.enqueue('send-email', { to, subject, body });
114
+ }
115
+ }
116
+ ```
117
+
118
+ ### Processing Tasks
119
+
120
+ Start a worker to process tasks. The handler receives a `TaskContext` and must return a `TaskProcessResult`.
121
+
122
+ ```typescript
123
+ import { inject, Singleton } from '@tstdl/base/injector';
124
+ import { Logger } from '@tstdl/base/logger';
125
+ import { CancellationSignal } from '@tstdl/base/cancellation';
126
+ import { TaskQueue, TaskProcessResult } from '@tstdl/base/task-queue';
127
+
128
+ @Singleton()
129
+ export class MailWorker {
130
+ readonly #queue = inject<TaskQueue<MyTasks>>(TaskQueue, 'mail-queue');
131
+ readonly #logger = inject(Logger, 'MailWorker');
132
+
133
+ start(signal: CancellationSignal): void {
134
+ this.#queue.process(
135
+ {
136
+ cancellationSignal: signal,
137
+ concurrency: 5, // Process up to 5 tasks concurrently
138
+ },
139
+ async (context) => {
140
+ this.#logger.info(`Sending mail to ${context.data.to}`);
141
+
142
+ const messageId = await sendEmail(context.data);
143
+
144
+ // Update state periodically if needed
145
+ await context.checkpoint({ state: { sentAt: Date.now() } });
146
+
147
+ return TaskProcessResult.Complete({ messageId });
148
+ },
149
+ );
150
+ }
151
+ }
152
+ ```
153
+
154
+ ## 🔧 Advanced Topics
155
+
156
+ ### Task Hierarchies (Parent/Child)
157
+
158
+ You can spawn child tasks from within a worker. The parent task will enter a `Waiting` state and will only transition back to `Pending` (or `Completed`) once all its children have reached a terminal state.
159
+
160
+ ```typescript
161
+ this.#queue.process({ cancellationSignal }, async (context) => {
162
+ // 1. Spawn child tasks. These are automatically linked to the current task.
163
+ await context.spawn('send-email', { to: 'admin@example.com', ... });
164
+ await context.spawn('send-email', { to: 'user@example.com', ... });
165
+
166
+ // 2. Complete the current execution.
167
+ // The task transitions to 'Waiting' until children finish.
168
+ return TaskProcessResult.Complete();
169
+ });
170
+ ```
171
+
172
+ ### Batch Operations
173
+
174
+ For high throughput, use batch operations to reduce database round-trips.
175
+
176
+ **Enqueueing:**
177
+
178
+ ```typescript
179
+ const batch = this.#queue.batch();
180
+ batch.add('send-email', { to: 'user1@example.com', ... });
181
+ batch.add('send-email', { to: 'user2@example.com', ... });
182
+
183
+ await batch.enqueue();
184
+ ```
185
+
186
+ ### Deduplication (Idempotency)
187
+
188
+ Prevent duplicate tasks using `idempotencyKey`.
189
+
190
+ ```typescript
191
+ // If a task with this key exists, this call does nothing (it returns the existing task).
192
+ await queue.enqueue('generate-report', data, {
193
+ idempotencyKey: 'report-2023-10',
194
+ });
195
+
196
+ // Use 'replace: true' to update the existing task and reset it to 'Pending'.
197
+ await queue.enqueue('generate-report', data, {
198
+ idempotencyKey: 'report-2023-10',
199
+ replace: true,
200
+ });
201
+ ```
202
+
203
+ ### Prioritization
204
+
205
+ Tasks with lower priority numbers are processed first. The default is `1000`.
206
+
207
+ ```typescript
208
+ await queue.enqueue('type', data, { priority: 1 }); // High priority
209
+ await queue.enqueue('type', data, { priority: 9000 }); // Low priority
210
+ ```
211
+
212
+ ### Transactional Enqueueing
213
+
214
+ Enqueue tasks atomically within an existing database transaction. The task will only be visible to workers if the transaction commits.
215
+
216
+ ```typescript
217
+ import { inject } from '@tstdl/base/injector';
218
+ import { Database } from '@tstdl/base/orm/server';
219
+
220
+ const database = inject(Database);
221
+
222
+ await database.transaction(async (tx) => {
223
+ await userService.create(user, tx);
224
+
225
+ // Pass the transaction to the enqueue options
226
+ await queue.enqueue('welcome-email', { userId: user.id }, { transaction: tx });
227
+ });
228
+ ```
229
+
230
+ ### Manual Consumption
231
+
232
+ If you need full control over the consumption loop (e.g., for custom logic), use the async iterator.
233
+
234
+ ```typescript
235
+ async function runCustomWorker(queue: TaskQueue<any>, signal: CancellationSignal) {
236
+ for await (const task of queue.getConsumer(signal)) {
237
+ try {
238
+ // Manually touch to keep lease alive
239
+ await queue.touch(task);
240
+
241
+ // Do work...
242
+
243
+ // Manually complete
244
+ await queue.complete(task, { result: 'done' });
245
+ } catch (error) {
246
+ await queue.fail(task, error);
247
+ }
248
+ }
249
+ }
250
+ ```
251
+
252
+ ## 📚 API
253
+
254
+ ### `TaskQueue<Definitions>`
255
+
256
+ | Method | Description |
257
+ | :------------------------------- | :------------------------------------------------------------------------------------- |
258
+ | `enqueue(type, data, options?)` | Adds a single task to the queue. |
259
+ | `enqueueMany(items, options?)` | Adds multiple tasks efficiently. |
260
+ | `batch()` | Returns a `TaskQueueEnqueueBatch` builder. |
261
+ | `waitForTasks(ids, options?)` | Reactive waiting for tasks to reach a finalized state (Completed, Dead, or Cancelled). |
262
+ | `process(options, handler)` | Starts a managed worker loop for single tasks. |
263
+ | `dequeue(options?)` | Locks and retrieves the next available task(s). |
264
+ | `complete(task, options?)` | Marks a task as completed. |
265
+ | `fail(task, error, options?)` | Marks a task as failed. Retries if `fatal` is false and tries remain. |
266
+ | `touch(task, options?)` | Extends the worker lease and optionally updates progress/state. |
267
+ | `cancel(id, options?)` | Cancels a task and optionally its descendants. |
268
+ | `getTree(id, options?)` | Retrieves a task and all its descendants. |
269
+ | `reschedule(id, time, options?)` | Sets a new schedule timestamp for a task. |
270
+
271
+ ### `TaskContext<Definitions, Type>`
272
+
273
+ Passed to the worker handler.
274
+
275
+ | Property/Method | Description |
276
+ | :---------------------------- | :------------------------------------------------------------------- |
277
+ | `data` | The input data for the task. |
278
+ | `state` | The current state of the task. |
279
+ | `attempt` | Current attempt number (starts at 1). |
280
+ | `spawn(type, data, options?)` | Creates a child task linked to the current task. |
281
+ | `spawnMany(items)` | Creates multiple child tasks. |
282
+ | `checkpoint(options?)` | Updates progress/state and extends the lease. |
283
+ | `reschedule(time)` | Stops execution and reschedules for a future time. |
284
+ | `complete(result?)` | Manually completes the task (alternative to returning from handler). |
285
+ | `fail(error, fatal?)` | Manually fails the task. |
286
+
287
+ ### `QueueConfig`
288
+
289
+ | Property | Default | Description |
290
+ | :------------------------ | :------ | :------------------------------------------------- |
291
+ | `visibilityTimeout` | `5m` | Duration before a worker token is considered lost. |
292
+ | `maxExecutionTime` | `60m` | Hard limit for `Running` state. |
293
+ | `maxTries` | `3` | Maximum dequeue attempts allowed. |
294
+ | `retention` | `30d` | Duration to retain terminal tasks before archival. |
295
+ | `globalConcurrency` | `null` | Max simultaneous running tasks across all workers. |
296
+ | `circuitBreakerThreshold` | `5` | Failures before tripping the circuit breaker. |
297
+ | `retryDelayMinimum` | `5s` | Floor for exponential backoff delay. |
298
+ | `retryDelayMaximum` | `5m` | Ceiling for exponential backoff delay. |
299
+ | `idempotencyWindow` | `1h` | Time to retain non-replaced idempotency keys. |
300
+ | `priorityAgingInterval` | `1m` | Interval for automatic priority promotion. |
@@ -3,7 +3,7 @@ import { afterResolve } from '../../injector/index.js';
3
3
  import type { Query } from '../../orm/index.js';
4
4
  import { type Transaction } from '../../orm/server/index.js';
5
5
  import type { OneOrMany } from '../../schema/index.js';
6
- import { TaskQueue, type EnqueueManyItem, type EnqueueManyOptions, type EnqueueOneOptions, type Task } from '../task-queue.js';
6
+ import { TaskQueue, type EnqueueManyItem, type EnqueueManyOptions, type EnqueueOneOptions, type Task, type TaskQueueWaitOptions, type TaskQueueWaitResult } from '../task-queue.js';
7
7
  import type { TaskData, TaskDefinitionMap, TaskOfType, TaskResult, TasksResults, TasksStates, TaskState, TaskTypes } from '../types.js';
8
8
  export declare class PostgresTaskQueue<Definitions extends TaskDefinitionMap = TaskDefinitionMap> extends TaskQueue<Definitions> {
9
9
  #private;
@@ -49,6 +49,7 @@ export declare class PostgresTaskQueue<Definitions extends TaskDefinitionMap = T
49
49
  getTreeByQuery(query: Query<Task>, options?: {
50
50
  transaction?: Transaction;
51
51
  }): Promise<Task[]>;
52
+ waitForTasks(ids: string[], options?: TaskQueueWaitOptions): Promise<TaskQueueWaitResult>;
52
53
  cancel(id: string, options?: {
53
54
  transaction?: Transaction;
54
55
  }): Promise<void>;
@@ -57,10 +57,10 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
57
57
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
58
58
  });
59
59
  import { and, asc, count, eq, gt, gte, inArray, lt, lte, notInArray, or, sql, isNull as sqlIsNull } from 'drizzle-orm';
60
- import { merge } from 'rxjs';
60
+ import { merge, throttleTime } from 'rxjs';
61
61
  import { CancellationSignal } from '../../cancellation/index.js';
62
62
  import { CircuitBreaker, CircuitBreakerState } from '../../circuit-breaker/index.js';
63
- import { serializeError } from '../../errors/index.js';
63
+ import { serializeError, TimeoutError } from '../../errors/index.js';
64
64
  import { afterResolve, inject, provide, Singleton } from '../../injector/index.js';
65
65
  import { Logger } from '../../logger/index.js';
66
66
  import { MessageBus } from '../../message-bus/index.js';
@@ -70,6 +70,7 @@ import { RateLimiter } from '../../rate-limit/index.js';
70
70
  import { createArray, distinct, toArray } from '../../utils/array/array.js';
71
71
  import { digest } from '../../utils/cryptography.js';
72
72
  import { currentTimestamp } from '../../utils/date-time.js';
73
+ import { Timer } from '../../utils/timer.js';
73
74
  import { cancelableTimeout } from '../../utils/timing.js';
74
75
  import { isDefined, isNotNull, isNull, isString, isUndefined } from '../../utils/type-guards.js';
75
76
  import { millisecondsPerSecond } from '../../utils/units.js';
@@ -304,6 +305,35 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
304
305
  return await repositoryWithTransaction.mapManyToEntity(rows);
305
306
  });
306
307
  }
308
+ async waitForTasks(ids, options) {
309
+ if (ids.length == 0) {
310
+ return { cancelled: false };
311
+ }
312
+ const timeout = options?.timeout ?? Infinity;
313
+ const interval = options?.interval ?? 1000;
314
+ const cancellationSignal = this.#cancellationSignal.optionallyInherit(options?.cancellationSignal);
315
+ const continue$ = merge(this.#messageBus.allMessages$.pipe(throttleTime(50, undefined, { leading: true, trailing: true })), cancellationSignal);
316
+ const timer = Timer.startNew();
317
+ const finalizedStatuses = [TaskStatus.Completed, TaskStatus.Cancelled, TaskStatus.Dead];
318
+ while (true) {
319
+ if (cancellationSignal.isSet) {
320
+ return { cancelled: true };
321
+ }
322
+ if (timer.milliseconds > timeout) {
323
+ throw new TimeoutError('Timeout while waiting for tasks to complete');
324
+ }
325
+ const hasNonFinalized = await this.#repository.hasByQuery({
326
+ id: { $in: ids },
327
+ status: { $nin: finalizedStatuses },
328
+ });
329
+ if (!hasNonFinalized) {
330
+ return { cancelled: false };
331
+ }
332
+ const remainingTimeout = timeout - timer.milliseconds;
333
+ const waitTime = Math.min(interval, remainingTimeout);
334
+ await cancelableTimeout(waitTime, continue$);
335
+ }
336
+ }
307
337
  async cancel(id, options) {
308
338
  await this.cancelMany([id], options);
309
339
  }
@@ -46,7 +46,7 @@ export class TaskContext {
46
46
  return this.triesLeft <= 0;
47
47
  }
48
48
  get signal() {
49
- return this.#signal.signal;
49
+ return this.#signal;
50
50
  }
51
51
  get logger() {
52
52
  return this.#logger;