@tstdl/base 0.93.138 → 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.
- package/README.md +166 -0
- package/ai/genkit/multi-region.plugin.js +5 -3
- package/ai/genkit/tests/multi-region.test.d.ts +1 -0
- package/ai/genkit/tests/multi-region.test.js +5 -2
- package/ai/parser/parser.js +2 -2
- package/ai/prompts/build.js +1 -0
- package/ai/prompts/instructions-formatter.d.ts +15 -2
- package/ai/prompts/instructions-formatter.js +36 -31
- package/ai/prompts/prompt-builder.js +5 -5
- package/ai/prompts/steering.d.ts +3 -2
- package/ai/prompts/steering.js +3 -1
- package/ai/tests/instructions-formatter.test.js +1 -0
- package/api/README.md +403 -0
- package/api/client/client.js +7 -13
- package/api/client/tests/api-client.test.js +10 -10
- package/api/default-error-handlers.js +1 -1
- package/api/response.d.ts +2 -2
- package/api/response.js +22 -33
- package/api/server/api-controller.d.ts +1 -1
- package/api/server/api-controller.js +3 -3
- package/api/server/api-request-token.provider.d.ts +1 -0
- package/api/server/api-request-token.provider.js +1 -0
- package/api/server/middlewares/allowed-methods.middleware.js +2 -1
- package/api/server/middlewares/content-type.middleware.js +2 -1
- package/api/types.d.ts +3 -2
- package/application/README.md +240 -0
- package/application/application.js +2 -2
- package/audit/README.md +267 -0
- package/authentication/README.md +288 -0
- package/authentication/client/authentication.service.d.ts +12 -11
- package/authentication/client/authentication.service.js +21 -21
- package/authentication/client/http-client.middleware.js +2 -2
- package/authentication/tests/authentication.client-error-handling.test.js +2 -1
- package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
- package/browser/README.md +401 -0
- package/cancellation/README.md +156 -0
- package/cancellation/tests/coverage.test.d.ts +1 -0
- package/cancellation/tests/coverage.test.js +49 -0
- package/cancellation/tests/leak.test.d.ts +1 -0
- package/cancellation/tests/leak.test.js +35 -0
- package/cancellation/tests/token.test.d.ts +1 -0
- package/cancellation/tests/token.test.js +136 -0
- package/cancellation/token.d.ts +53 -177
- package/cancellation/token.js +132 -201
- package/context/README.md +174 -0
- package/cookie/README.md +161 -0
- package/css/README.md +157 -0
- package/data-structures/README.md +320 -0
- package/decorators/README.md +140 -0
- package/distributed-loop/README.md +231 -0
- package/distributed-loop/distributed-loop.js +1 -1
- package/document-management/README.md +403 -0
- package/document-management/server/services/document-management.service.js +9 -7
- package/document-management/tests/document-management-core.test.js +2 -7
- package/document-management/tests/document-management.api.test.js +6 -7
- package/document-management/tests/document-statistics.service.test.js +11 -12
- package/document-management/tests/document.service.test.js +3 -3
- package/document-management/tests/enum-helpers.test.js +2 -3
- package/dom/README.md +213 -0
- package/enumerable/README.md +259 -0
- package/enumeration/README.md +121 -0
- package/errors/README.md +267 -0
- package/file/README.md +191 -0
- package/formats/README.md +210 -0
- package/function/README.md +144 -0
- package/http/README.md +318 -0
- package/http/client/adapters/undici.adapter.js +1 -1
- package/http/client/http-client-request.d.ts +6 -5
- package/http/client/http-client-request.js +8 -9
- package/http/server/node/node-http-server.js +1 -2
- package/image-service/README.md +137 -0
- package/injector/README.md +491 -0
- package/injector/injector.d.ts +1 -0
- package/injector/injector.js +17 -5
- package/injector/tests/leak.test.d.ts +1 -0
- package/injector/tests/leak.test.js +45 -0
- package/intl/README.md +113 -0
- package/json-path/README.md +182 -0
- package/jsx/README.md +154 -0
- package/key-value-store/README.md +191 -0
- package/lock/README.md +249 -0
- package/lock/web/web-lock.js +119 -47
- package/logger/README.md +287 -0
- package/mail/README.md +256 -0
- package/memory/README.md +144 -0
- package/message-bus/README.md +244 -0
- package/message-bus/message-bus-base.js +1 -1
- package/module/README.md +182 -0
- package/module/module.d.ts +1 -1
- package/module/module.js +77 -17
- package/module/modules/web-server.module.js +1 -1
- package/notification/tests/notification-type.service.test.js +24 -15
- package/object-storage/README.md +300 -0
- package/openid-connect/README.md +274 -0
- package/orm/README.md +423 -0
- package/package.json +8 -6
- package/password/README.md +164 -0
- package/pdf/README.md +246 -0
- package/polyfills.js +1 -0
- package/pool/README.md +198 -0
- package/process/README.md +237 -0
- package/promise/README.md +252 -0
- package/promise/cancelable-promise.js +1 -1
- package/random/README.md +193 -0
- package/reflection/README.md +305 -0
- package/rpc/README.md +386 -0
- package/rxjs-utils/README.md +262 -0
- package/schema/README.md +342 -0
- package/serializer/README.md +342 -0
- package/signals/implementation/README.md +134 -0
- package/sse/README.md +278 -0
- package/task-queue/README.md +300 -0
- package/task-queue/postgres/task-queue.d.ts +2 -1
- package/task-queue/postgres/task-queue.js +32 -2
- package/task-queue/task-context.js +1 -1
- package/task-queue/task-queue.d.ts +17 -0
- package/task-queue/task-queue.js +103 -44
- package/task-queue/tests/complex.test.js +4 -4
- package/task-queue/tests/dependencies.test.js +4 -2
- package/task-queue/tests/queue.test.js +111 -0
- package/task-queue/tests/worker.test.js +21 -13
- package/templates/README.md +287 -0
- package/testing/README.md +157 -0
- package/text/README.md +346 -0
- package/threading/README.md +238 -0
- package/types/README.md +311 -0
- package/utils/README.md +322 -0
- package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
- package/utils/async-iterable-helpers/observable-iterable.js +4 -8
- package/utils/async-iterable-helpers/take-until.js +4 -4
- package/utils/backoff.js +89 -30
- package/utils/retry-with-backoff.js +1 -1
- package/utils/timer.d.ts +1 -1
- package/utils/timer.js +5 -7
- package/utils/timing.d.ts +1 -1
- package/utils/timing.js +2 -4
- package/utils/z-base32.d.ts +1 -0
- 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
|
}
|