@prosody-events/prosody 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +657 -353
- package/bindings.d.ts +780 -48
- package/bindings.js +226 -83
- package/index.d.ts +706 -22
- package/index.js +1446 -105
- package/package.json +23 -20
package/README.md
CHANGED
|
@@ -21,56 +21,87 @@ strategies, and integrated OpenTelemetry support for distributed tracing.
|
|
|
21
21
|
npm install @prosody-events/prosody
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
The package ships TypeScript declarations for the public API.
|
|
25
|
+
`EventHandler<P, R>` carries application payload and response types through each
|
|
26
|
+
handler method. Keyed-state definitions carry their item types through
|
|
27
|
+
`context.state()`. Unparameterized API types default to `JsonValue`. See the
|
|
28
|
+
[strict TypeScript examples](examples/) for IDE-ready projects compiled by the
|
|
29
|
+
repository typecheck.
|
|
30
|
+
|
|
24
31
|
## Quick Start
|
|
25
32
|
|
|
33
|
+
Run each example in an asynchronous function unless the example defines one.
|
|
34
|
+
|
|
26
35
|
```javascript
|
|
27
|
-
const {ProsodyClient} = require(
|
|
36
|
+
const { ProsodyClient } = require("@prosody-events/prosody");
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
async function main() {
|
|
39
|
+
// Initialize the client with Kafka bootstrap servers, consumer group, and topics
|
|
40
|
+
const client = await ProsodyClient.create({
|
|
31
41
|
// Bootstrap servers should normally be set using the PROSODY_BOOTSTRAP_SERVERS environment variable
|
|
32
42
|
bootstrapServers: "localhost:9092",
|
|
33
43
|
|
|
34
|
-
// To allow loopbacks,
|
|
35
|
-
// Normally,
|
|
44
|
+
// To allow loopbacks, sourceSystem must be different from groupId.
|
|
45
|
+
// Normally, sourceSystem is omitted and defaults to groupId.
|
|
36
46
|
sourceSystem: "my-application-source",
|
|
37
47
|
|
|
38
|
-
//
|
|
48
|
+
// groupId should be set to the name of your application
|
|
39
49
|
groupId: "my-consumer-group",
|
|
40
50
|
|
|
41
51
|
// Topics the client should subscribe to
|
|
42
|
-
subscribedTopics: "my-topic"
|
|
43
|
-
});
|
|
52
|
+
subscribedTopics: "my-topic",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Define a message handler
|
|
56
|
+
const messageHandler = {
|
|
57
|
+
onExcise: async (context, message, signal) => {
|
|
58
|
+
console.log(`Excise key: ${message.key}`);
|
|
59
|
+
await context.clearScheduled();
|
|
60
|
+
return null;
|
|
61
|
+
},
|
|
44
62
|
|
|
45
|
-
// Define a message handler
|
|
46
|
-
const messageHandler = {
|
|
47
63
|
onMessage: async (context, message, signal) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
// Process the received message
|
|
65
|
+
console.log(`Received message: ${JSON.stringify(message)}`);
|
|
66
|
+
|
|
67
|
+
// Schedule a timer for delayed processing
|
|
68
|
+
if (message.payload.scheduleFollowup) {
|
|
69
|
+
const followupTime = new Date(Date.now() + 30000); // 30 seconds from now
|
|
70
|
+
await context.schedule(followupTime);
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
56
73
|
},
|
|
57
|
-
|
|
74
|
+
|
|
58
75
|
onTimer: async (context, timer, signal) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
};
|
|
76
|
+
// Handle timer firing
|
|
77
|
+
console.log(`Timer fired for key: ${timer.key} at ${timer.time}`);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
63
80
|
|
|
64
|
-
// Subscribe to messages using the message handler
|
|
65
|
-
client.subscribe(messageHandler);
|
|
81
|
+
// Subscribe to messages using the message handler
|
|
82
|
+
await client.subscribe(messageHandler);
|
|
83
|
+
|
|
84
|
+
// Send a message to a topic
|
|
85
|
+
await client.send("my-topic", "message-key", { content: "Hello, Kafka!" });
|
|
86
|
+
await client.excise("my-topic", "obsolete-key");
|
|
66
87
|
|
|
67
|
-
//
|
|
68
|
-
await client.
|
|
88
|
+
// Shut down all client services when done
|
|
89
|
+
await client.shutdown();
|
|
90
|
+
}
|
|
69
91
|
|
|
70
|
-
|
|
71
|
-
await client.unsubscribe();
|
|
92
|
+
main().catch(console.error);
|
|
72
93
|
```
|
|
73
94
|
|
|
95
|
+
## Excise records
|
|
96
|
+
|
|
97
|
+
Applications can copy event data into keyed state and external stores. A regulatory or contractual deletion must remove every copy for one key.
|
|
98
|
+
|
|
99
|
+
An excise record carries this deletion command. Kafka encodes the command as a key with no payload. During topic compaction, Kafka deletes earlier values for the key. Call `excise(topic, key)` to send the record. Prosody routes the record to `onExcise`. The handler must delete all consumer-owned data for the key.
|
|
100
|
+
|
|
101
|
+
Each handler must implement `onMessage`, `onExcise`, and `onTimer`. Subscription fails before consumption if a method is missing.
|
|
102
|
+
|
|
103
|
+
If an excise record is a request, return a response from `onExcise`. Prosody uses this response as the subsystem result.
|
|
104
|
+
|
|
74
105
|
## Architecture
|
|
75
106
|
|
|
76
107
|
Prosody enables efficient, parallel processing of Kafka messages while maintaining order for messages with the same key:
|
|
@@ -109,13 +140,13 @@ When the timer fires, reload the message from Kafka and retry.
|
|
|
109
140
|
|
|
110
141
|
```javascript
|
|
111
142
|
// Configure defer behavior
|
|
112
|
-
const client =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
143
|
+
const client = await ProsodyClient.create({
|
|
144
|
+
groupId: "my-consumer-group",
|
|
145
|
+
subscribedTopics: "my-topic",
|
|
146
|
+
deferEnabled: true, // Enable deferral (default: true)
|
|
147
|
+
deferBaseMs: 1000, // Wait 1s before first retry
|
|
148
|
+
deferMaxDelayMs: 86400000, // Cap at 24 hours
|
|
149
|
+
deferFailureThreshold: 0.9, // Disable when >90% failing
|
|
119
150
|
});
|
|
120
151
|
```
|
|
121
152
|
|
|
@@ -131,12 +162,12 @@ with a transient error, routing them through defer.
|
|
|
131
162
|
|
|
132
163
|
```javascript
|
|
133
164
|
// Configure monopolization detection
|
|
134
|
-
const client =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
165
|
+
const client = await ProsodyClient.create({
|
|
166
|
+
groupId: "my-consumer-group",
|
|
167
|
+
subscribedTopics: "my-topic",
|
|
168
|
+
monopolizationEnabled: true, // Enable detection (default: true)
|
|
169
|
+
monopolizationThreshold: 0.9, // Reject keys using >90% of window
|
|
170
|
+
monopolizationWindowMs: 300000, // 5-minute window
|
|
140
171
|
});
|
|
141
172
|
```
|
|
142
173
|
|
|
@@ -145,11 +176,11 @@ const client = new ProsodyClient({
|
|
|
145
176
|
Handlers are automatically cancelled if they exceed a deadline:
|
|
146
177
|
|
|
147
178
|
```javascript
|
|
148
|
-
const client =
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
179
|
+
const client = await ProsodyClient.create({
|
|
180
|
+
groupId: "my-consumer-group",
|
|
181
|
+
subscribedTopics: "my-topic",
|
|
182
|
+
timeoutMs: 30000, // Cancel after 30 seconds
|
|
183
|
+
stallThresholdMs: 60000, // Report unhealthy after 60 seconds
|
|
153
184
|
});
|
|
154
185
|
```
|
|
155
186
|
|
|
@@ -158,109 +189,11 @@ exit promptly. If not specified, timeout defaults to 80% of `stallThresholdMs`.
|
|
|
158
189
|
|
|
159
190
|
## Configuration
|
|
160
191
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
### Core
|
|
164
|
-
|
|
165
|
-
| Option / Environment Variable | Description | Default |
|
|
166
|
-
|-----------------------------------------|---------------------------------------------------|--------------|
|
|
167
|
-
| `bootstrapServers` / `PROSODY_BOOTSTRAP_SERVERS` | Kafka servers to connect to | - |
|
|
168
|
-
| `groupId` / `PROSODY_GROUP_ID` | Consumer group name | - |
|
|
169
|
-
| `subscribedTopics` / `PROSODY_SUBSCRIBED_TOPICS` | Topics to read from | - |
|
|
170
|
-
| `allowedEvents` / `PROSODY_ALLOWED_EVENTS` | Only process events matching these prefixes | (all) |
|
|
171
|
-
| `sourceSystem` / `PROSODY_SOURCE_SYSTEM` | Tag for outgoing messages (prevents reprocessing)| `<groupId>` |
|
|
172
|
-
| `mock` / `PROSODY_MOCK` | Use in-memory Kafka for testing | false |
|
|
173
|
-
|
|
174
|
-
### Consumer
|
|
175
|
-
|
|
176
|
-
| Option / Environment Variable | Description | Default |
|
|
177
|
-
|-----------------------------------------|------------------------------------------------------|------------------------|
|
|
178
|
-
| `maxConcurrency` / `PROSODY_MAX_CONCURRENCY` | Max messages being processed simultaneously | 32 |
|
|
179
|
-
| `maxUncommitted` / `PROSODY_MAX_UNCOMMITTED` | Max queued messages before pausing consumption | 64 |
|
|
180
|
-
| `timeoutMs` / `PROSODY_TIMEOUT` | Cancel handler if it runs longer than this | 80% of stall threshold |
|
|
181
|
-
| `commitIntervalMs` / `PROSODY_COMMIT_INTERVAL` | How often to save progress to Kafka | 1s |
|
|
182
|
-
| `pollIntervalMs` / `PROSODY_POLL_INTERVAL` | How often to fetch new messages from Kafka | 100ms |
|
|
183
|
-
| `shutdownTimeoutMs` / `PROSODY_SHUTDOWN_TIMEOUT` | Wait this long for in-flight work before force-quit | 30s |
|
|
184
|
-
| `stallThresholdMs` / `PROSODY_STALL_THRESHOLD` | Report unhealthy if no progress for this long | 5m |
|
|
185
|
-
| `probePort` / `PROSODY_PROBE_PORT` | HTTP port for health checks (null to disable) | 8000 |
|
|
186
|
-
| `failureTopic` / `PROSODY_FAILURE_TOPIC` | Send unprocessable messages here (dead letter queue) | - |
|
|
187
|
-
| `idempotenceCacheSize` / `PROSODY_IDEMPOTENCE_CACHE_SIZE` | Global shared cache capacity across all partitions for deduplicating messages. Set to 0 to disable the entire deduplication middleware (both in-memory cache and Cassandra persistent store) | 8192 |
|
|
188
|
-
| `idempotenceVersion` / `PROSODY_IDEMPOTENCE_VERSION` | Version string for cache-busting dedup hashes | `"1"` |
|
|
189
|
-
| `idempotenceTtlS` / `PROSODY_IDEMPOTENCE_TTL` | TTL for dedup records in Cassandra in seconds | 604800 |
|
|
190
|
-
| `slabSizeMs` / `PROSODY_SLAB_SIZE` | Timer storage granularity (rarely needs changing) | 1h |
|
|
191
|
-
| `messageSpans` / `PROSODY_MESSAGE_SPANS` | Span linking for message execution: `child` (child-of) or `follows_from` | `child` |
|
|
192
|
-
| `timerSpans` / `PROSODY_TIMER_SPANS` | Span linking for timer execution: `child` (child-of) or `follows_from` | `follows_from` |
|
|
193
|
-
|
|
194
|
-
### Producer
|
|
195
|
-
|
|
196
|
-
| Option / Environment Variable | Description | Default |
|
|
197
|
-
|-----------------------------------------|---------------------------------|---------|
|
|
198
|
-
| `sendTimeoutMs` / `PROSODY_SEND_TIMEOUT` | Give up sending after this long | 1s |
|
|
199
|
-
|
|
200
|
-
### Retry
|
|
201
|
-
|
|
202
|
-
When a handler fails, retry with exponential backoff:
|
|
203
|
-
|
|
204
|
-
| Option / Environment Variable | Description | Default |
|
|
205
|
-
|-----------------------------------------|-----------------------------------|---------|
|
|
206
|
-
| `maxRetries` / `PROSODY_MAX_RETRIES` | Give up after this many attempts | 3 |
|
|
207
|
-
| `retryBaseMs` / `PROSODY_RETRY_BASE` | Wait this long before first retry | 20ms |
|
|
208
|
-
| `maxRetryDelayMs` / `PROSODY_RETRY_MAX_DELAY` | Never wait longer than this | 5m |
|
|
209
|
-
|
|
210
|
-
### Deferral (Pipeline Mode)
|
|
211
|
-
|
|
212
|
-
| Option / Environment Variable | Description | Default |
|
|
213
|
-
|-----------------------------------------|---------------------------------------------------|---------|
|
|
214
|
-
| `deferEnabled` / `PROSODY_DEFER_ENABLED` | Enable deferral for new messages | true |
|
|
215
|
-
| `deferBaseMs` / `PROSODY_DEFER_BASE` | Wait this long before first deferred retry | 1s |
|
|
216
|
-
| `deferMaxDelayMs` / `PROSODY_DEFER_MAX_DELAY` | Never wait longer than this | 24h |
|
|
217
|
-
| `deferFailureThreshold` / `PROSODY_DEFER_FAILURE_THRESHOLD` | Disable deferral when failure rate exceeds this | 0.9 |
|
|
218
|
-
| `deferFailureWindowMs` / `PROSODY_DEFER_FAILURE_WINDOW` | Measure failure rate over this time window | 5m |
|
|
219
|
-
| `deferCacheSize` / `PROSODY_DEFER_CACHE_SIZE` | Track this many deferred keys in memory | 1024 |
|
|
220
|
-
| `deferStoreCacheSize` / `PROSODY_DEFER_STORE_CACHE_SIZE` | Maximum deferred store cache entries per Cassandra defer store | 8192 |
|
|
221
|
-
| `deferSeekTimeoutMs` / `PROSODY_DEFER_SEEK_TIMEOUT` | Timeout when loading deferred messages | 30s |
|
|
222
|
-
| `deferDiscardThreshold` / `PROSODY_DEFER_DISCARD_THRESHOLD` | Read optimization (rarely needs changing) | 100 |
|
|
192
|
+
For the complete configuration reference, see [CONFIGURATION.md](CONFIGURATION.md).
|
|
223
193
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
| Option / Environment Variable | Description | Default |
|
|
227
|
-
|-----------------------------------------|-----------------------------------------|---------|
|
|
228
|
-
| `monopolizationEnabled` / `PROSODY_MONOPOLIZATION_ENABLED` | Enable hot key protection | true |
|
|
229
|
-
| `monopolizationThreshold` / `PROSODY_MONOPOLIZATION_THRESHOLD` | Max handler time as fraction of window | 0.9 |
|
|
230
|
-
| `monopolizationWindowMs` / `PROSODY_MONOPOLIZATION_WINDOW` | Measurement window | 5m |
|
|
231
|
-
| `monopolizationCacheSize` / `PROSODY_MONOPOLIZATION_CACHE_SIZE` | Max distinct keys to track | 8192 |
|
|
232
|
-
|
|
233
|
-
### Fair Scheduling (All Modes)
|
|
234
|
-
|
|
235
|
-
| Option / Environment Variable | Description | Default |
|
|
236
|
-
|-----------------------------------------|------------------------------------------------------------------|---------|
|
|
237
|
-
| `schedulerFailureWeight` / `PROSODY_SCHEDULER_FAILURE_WEIGHT` | Fraction of processing time reserved for retries | 0.3 |
|
|
238
|
-
| `schedulerMaxWaitMs` / `PROSODY_SCHEDULER_MAX_WAIT` | Messages waiting this long get maximum priority | 2m |
|
|
239
|
-
| `schedulerWaitWeight` / `PROSODY_SCHEDULER_WAIT_WEIGHT` | Priority boost for waiting messages (higher = more aggressive) | 200.0 |
|
|
240
|
-
| `schedulerCacheSize` / `PROSODY_SCHEDULER_CACHE_SIZE` | Max distinct keys to track | 8192 |
|
|
241
|
-
|
|
242
|
-
### Telemetry Emitter
|
|
243
|
-
|
|
244
|
-
Produces message and timer lifecycle events to a Kafka topic for observability:
|
|
194
|
+
Constructor options take precedence. Unset options use environment variables, then library defaults.
|
|
245
195
|
|
|
246
|
-
|
|
247
|
-
|-----------------------------------------|---------------------------------------------------|----------------------------|
|
|
248
|
-
| `telemetryEnabled` / `PROSODY_TELEMETRY_ENABLED` | Produce lifecycle events to Kafka | true |
|
|
249
|
-
| `telemetryTopic` / `PROSODY_TELEMETRY_TOPIC` | Kafka topic for telemetry events | prosody.telemetry-events |
|
|
250
|
-
|
|
251
|
-
### Cassandra
|
|
252
|
-
|
|
253
|
-
Persistent storage for timers and deferred retries (not needed if `mock: true`):
|
|
254
|
-
|
|
255
|
-
| Option / Environment Variable | Description | Default |
|
|
256
|
-
|-----------------------------------------|------------------------------------|---------|
|
|
257
|
-
| `cassandraNodes` / `PROSODY_CASSANDRA_NODES` | Servers to connect to (host:port) | - |
|
|
258
|
-
| `cassandraKeyspace` / `PROSODY_CASSANDRA_KEYSPACE` | Keyspace name | prosody |
|
|
259
|
-
| `cassandraUser` / `PROSODY_CASSANDRA_USER` | Username | - |
|
|
260
|
-
| `cassandraPassword` / `PROSODY_CASSANDRA_PASSWORD` | Password | - |
|
|
261
|
-
| `cassandraDatacenter` / `PROSODY_CASSANDRA_DATACENTER` | Prefer this datacenter for queries | - |
|
|
262
|
-
| `cassandraRack` / `PROSODY_CASSANDRA_RACK` | Prefer this rack for queries | - |
|
|
263
|
-
| `cassandraRetentionSeconds` / `PROSODY_CASSANDRA_RETENTION` | Delete data older than this | 1y |
|
|
196
|
+
Client construction is asynchronous. Replace `new ProsodyClient(config)` with `await ProsodyClient.create(config)`.
|
|
264
197
|
|
|
265
198
|
## Liveness and Readiness Probes
|
|
266
199
|
|
|
@@ -276,11 +209,11 @@ server is tied to the consumer's lifecycle and offers two main endpoints:
|
|
|
276
209
|
Configure the probe server using either the client constructor:
|
|
277
210
|
|
|
278
211
|
```javascript
|
|
279
|
-
const client =
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
212
|
+
const client = await ProsodyClient.create({
|
|
213
|
+
groupId: "my-consumer-group",
|
|
214
|
+
subscribedTopics: "my-topic",
|
|
215
|
+
probePort: 8000, // Set to null to disable
|
|
216
|
+
stallThresholdMs: 15000, // 15 seconds before considering a partition stalled
|
|
284
217
|
});
|
|
285
218
|
```
|
|
286
219
|
|
|
@@ -309,10 +242,64 @@ const partitionCount = client.assignedPartitionCount;
|
|
|
309
242
|
|
|
310
243
|
// You can use these in your own health checks or monitoring
|
|
311
244
|
if (client.isStalled) {
|
|
312
|
-
|
|
245
|
+
console.warn("Consumer has stalled partitions");
|
|
313
246
|
}
|
|
314
247
|
```
|
|
315
248
|
|
|
249
|
+
## Subsystems
|
|
250
|
+
|
|
251
|
+
A consumer group ID identifies a set of processes that share records and the keyed state that the group owns. A subsystem can include one or more services and consumer groups. If callers use these IDs, a refactor can require changes to each caller.
|
|
252
|
+
|
|
253
|
+
A subsystem gives requests and published state one stable public name. Callers use this name instead of consumer group IDs. You can change its services and consumer groups without changing callers. Prosody uses the first response to a subsystem request. For each published-state read, it uses one consumer group that publishes the collection.
|
|
254
|
+
|
|
255
|
+
## Requests
|
|
256
|
+
|
|
257
|
+
Kafka decouples producers from consumers, so a send does not return consumer results. This asynchronous model lets each service process records independently. Some operations must wait for consumer results before they continue. A request recovers synchrony for the caller while consumers continue asynchronous processing.
|
|
258
|
+
|
|
259
|
+
Send a request from a handler or other application code. The Prosody client does not need an active subscription. The result map uses canonical subsystem names as keys. Each value is a `Success` or `Failure` outcome. Use `requestExcise` to send an excise record and collect the same outcome type.
|
|
260
|
+
|
|
261
|
+
Do not rely on map order. The map contains one entry for each selected subsystem. A missing response becomes a timeout `Failure`; Prosody does not omit the subsystem. The request rejects for request-level failures, such as invalid input, a Kafka send failure, or shutdown. Do not await a request if the current consumer group must process it for the same key. That group cannot process it until the handler returns.
|
|
262
|
+
|
|
263
|
+
Message and excise handler return values become successful outcomes. Each return value must have a JSON representation.
|
|
264
|
+
|
|
265
|
+
Set `subsystem` to `inventory` on the client that subscribes this handler.
|
|
266
|
+
|
|
267
|
+
```javascript
|
|
268
|
+
await client.subscribe({
|
|
269
|
+
onMessage: async (_context, message) => ({ accepted: message.key }),
|
|
270
|
+
onExcise: async (_context, message) => ({ accepted: message.key }),
|
|
271
|
+
onTimer: async () => {},
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Send the request:
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
const subsystems = ["inventory", "billing"];
|
|
279
|
+
const results = await client.request(
|
|
280
|
+
"orders",
|
|
281
|
+
"order-1",
|
|
282
|
+
{ type: "order.created" },
|
|
283
|
+
{ subsystems, timeoutMs: 2_000 },
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
for (const [subsystem, outcome] of results) {
|
|
287
|
+
if (outcome.ok) console.log(`${subsystem}:`, outcome.value);
|
|
288
|
+
else console.error(`${subsystem}: ${outcome.error.message}`);
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
The example can print these results:
|
|
293
|
+
|
|
294
|
+
```text
|
|
295
|
+
inventory: { accepted: 'order-1' }
|
|
296
|
+
billing: no response arrived before the deadline
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Each failure contains one typed response error.
|
|
300
|
+
|
|
301
|
+
Each response error has one message.
|
|
302
|
+
|
|
316
303
|
## Advanced Usage
|
|
317
304
|
|
|
318
305
|
### Pipeline Mode
|
|
@@ -320,26 +307,27 @@ if (client.isStalled) {
|
|
|
320
307
|
All messages must be processed. Retries indefinitely. Uses defer and monopolization detection.
|
|
321
308
|
|
|
322
309
|
**Middleware stack:**
|
|
310
|
+
|
|
323
311
|
```
|
|
324
312
|
Kafka → Deduplication → Retry → Defer → Monopolization → Shutdown → Scheduler → Timeout → Telemetry → Handler
|
|
325
313
|
```
|
|
326
314
|
|
|
327
|
-
| Layer | Purpose
|
|
328
|
-
|
|
329
|
-
| Deduplication | Skips messages whose ID was already processed
|
|
330
|
-
| Retry | Retries transient errors indefinitely
|
|
331
|
-
| Defer | Stores failing messages for timer-based retry
|
|
332
|
-
| Monopolization | Rejects keys exceeding execution time threshold
|
|
333
|
-
| Shutdown | Drains in-flight work on partition revocation
|
|
334
|
-
| Scheduler | Enforces concurrency limits and VT-based priority
|
|
335
|
-
| Timeout | Cancels handlers exceeding deadline
|
|
336
|
-
| Telemetry | Emits handler lifecycle events
|
|
315
|
+
| Layer | Purpose |
|
|
316
|
+
| -------------- | ------------------------------------------------- |
|
|
317
|
+
| Deduplication | Skips messages whose ID was already processed |
|
|
318
|
+
| Retry | Retries transient errors indefinitely |
|
|
319
|
+
| Defer | Stores failing messages for timer-based retry |
|
|
320
|
+
| Monopolization | Rejects keys exceeding execution time threshold |
|
|
321
|
+
| Shutdown | Drains in-flight work on partition revocation |
|
|
322
|
+
| Scheduler | Enforces concurrency limits and VT-based priority |
|
|
323
|
+
| Timeout | Cancels handlers exceeding deadline |
|
|
324
|
+
| Telemetry | Emits handler lifecycle events |
|
|
337
325
|
|
|
338
326
|
```javascript
|
|
339
|
-
const client =
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
327
|
+
const client = await ProsodyClient.create({
|
|
328
|
+
mode: Mode.Pipeline, // Default mode
|
|
329
|
+
groupId: "my-consumer-group",
|
|
330
|
+
subscribedTopics: "my-topic",
|
|
343
331
|
});
|
|
344
332
|
```
|
|
345
333
|
|
|
@@ -352,12 +340,12 @@ Tries a few times, then routes failures to a dead letter topic.
|
|
|
352
340
|
- Use when you need to keep moving and can reprocess failures later
|
|
353
341
|
|
|
354
342
|
```javascript
|
|
355
|
-
const client =
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
343
|
+
const client = await ProsodyClient.create({
|
|
344
|
+
mode: Mode.LowLatency,
|
|
345
|
+
groupId: "my-consumer-group",
|
|
346
|
+
subscribedTopics: "my-topic",
|
|
347
|
+
failureTopic: "failed-messages", // Required for low-latency mode
|
|
348
|
+
maxRetries: 3, // Retry up to 3 times after the initial attempt
|
|
361
349
|
});
|
|
362
350
|
```
|
|
363
351
|
|
|
@@ -370,10 +358,10 @@ Logs failures and moves on.
|
|
|
370
358
|
- Use for development or when message loss is acceptable
|
|
371
359
|
|
|
372
360
|
```javascript
|
|
373
|
-
const client =
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
361
|
+
const client = await ProsodyClient.create({
|
|
362
|
+
mode: Mode.BestEffort,
|
|
363
|
+
groupId: "my-consumer-group",
|
|
364
|
+
subscribedTopics: "my-topic",
|
|
377
365
|
});
|
|
378
366
|
```
|
|
379
367
|
|
|
@@ -384,10 +372,10 @@ of events:
|
|
|
384
372
|
|
|
385
373
|
```javascript
|
|
386
374
|
// Process only events with types starting with "user." or "account."
|
|
387
|
-
const client =
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
375
|
+
const client = await ProsodyClient.create({
|
|
376
|
+
groupId: "my-consumer-group",
|
|
377
|
+
subscribedTopics: "my-topic",
|
|
378
|
+
allowedEvents: ["user.", "account."],
|
|
391
379
|
});
|
|
392
380
|
```
|
|
393
381
|
|
|
@@ -420,10 +408,10 @@ Prosody prevents processing loops in distributed systems by tracking the source
|
|
|
420
408
|
|
|
421
409
|
```javascript
|
|
422
410
|
// Consumer and producer in one application
|
|
423
|
-
const client =
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
411
|
+
const client = await ProsodyClient.create({
|
|
412
|
+
groupId: "my-service",
|
|
413
|
+
sourceSystem: "my-service-producer", // Must differ from groupId to allow loopbacks; defaults to groupId
|
|
414
|
+
subscribedTopics: "my-topic",
|
|
427
415
|
});
|
|
428
416
|
```
|
|
429
417
|
|
|
@@ -453,50 +441,34 @@ Deduplication uses a two-tier approach:
|
|
|
453
441
|
- **Cassandra-backed persistent store**: Survives restarts and rebalances across instances. TTL controlled by
|
|
454
442
|
`idempotenceTtlS` (default 7 days, i.e. 604800s).
|
|
455
443
|
|
|
456
|
-
|
|
457
|
-
|
|
444
|
+
Deduplication is always active. `idempotenceCacheSize` must be greater than `0`; a value of `0` (via either the option
|
|
445
|
+
or `PROSODY_IDEMPOTENCE_CACHE_SIZE=0`) is rejected when the client is constructed.
|
|
458
446
|
|
|
459
447
|
```javascript
|
|
460
448
|
// Messages with IDs are deduplicated per key
|
|
461
449
|
await client.send("my-topic", "key1", {
|
|
462
|
-
|
|
463
|
-
|
|
450
|
+
id: "msg-123", // Message will be processed
|
|
451
|
+
content: "Hello!",
|
|
464
452
|
});
|
|
465
453
|
|
|
466
454
|
await client.send("my-topic", "key1", {
|
|
467
|
-
|
|
468
|
-
|
|
455
|
+
id: "msg-123", // Message will be skipped (duplicate)
|
|
456
|
+
content: "Hello again!",
|
|
469
457
|
});
|
|
470
458
|
|
|
471
459
|
await client.send("my-topic", "key2", {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
});
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
The entire deduplication middleware (both in-memory cache and Cassandra persistent store) can be disabled by setting `idempotenceCacheSize: 0`:
|
|
478
|
-
|
|
479
|
-
```javascript
|
|
480
|
-
const client = new ProsodyClient({
|
|
481
|
-
groupId: "my-consumer-group",
|
|
482
|
-
subscribedTopics: "my-topic",
|
|
483
|
-
idempotenceCacheSize: 0 // Disable deduplication entirely
|
|
460
|
+
id: "msg-123", // Message will be processed (different key)
|
|
461
|
+
content: "Hello!",
|
|
484
462
|
});
|
|
485
463
|
```
|
|
486
464
|
|
|
487
|
-
Or via environment variable:
|
|
488
|
-
|
|
489
|
-
```bash
|
|
490
|
-
PROSODY_IDEMPOTENCE_CACHE_SIZE=0
|
|
491
|
-
```
|
|
492
|
-
|
|
493
465
|
To invalidate all previously recorded dedup entries (forcing reprocessing of messages), change the version:
|
|
494
466
|
|
|
495
467
|
```javascript
|
|
496
|
-
const client =
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
468
|
+
const client = await ProsodyClient.create({
|
|
469
|
+
groupId: "my-consumer-group",
|
|
470
|
+
subscribedTopics: "my-topic",
|
|
471
|
+
idempotenceVersion: "2", // Changing this invalidates all previously recorded entries
|
|
500
472
|
});
|
|
501
473
|
```
|
|
502
474
|
|
|
@@ -512,27 +484,33 @@ Prosody supports timer-based delayed execution within message handlers. When a t
|
|
|
512
484
|
|
|
513
485
|
```javascript
|
|
514
486
|
const messageHandler = {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
487
|
+
onMessage: async (context, message, signal) => {
|
|
488
|
+
// Schedule a timer to fire in 30 seconds
|
|
489
|
+
const futureTime = new Date(Date.now() + 30000);
|
|
490
|
+
await context.schedule(futureTime);
|
|
491
|
+
|
|
492
|
+
// Schedule multiple timers
|
|
493
|
+
const oneMinute = new Date(Date.now() + 60000);
|
|
494
|
+
const twoMinutes = new Date(Date.now() + 120000);
|
|
495
|
+
await context.schedule(oneMinute);
|
|
496
|
+
await context.schedule(twoMinutes);
|
|
497
|
+
|
|
498
|
+
// Check what's scheduled
|
|
499
|
+
const scheduled = await context.scheduled();
|
|
500
|
+
console.log(`Scheduled timers: ${scheduled.length}`);
|
|
501
|
+
return null;
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
onTimer: async (context, timer, signal) => {
|
|
505
|
+
console.log("Timer fired!");
|
|
506
|
+
console.log(`Key: ${timer.key}`);
|
|
507
|
+
console.log(`Scheduled time: ${timer.time}`);
|
|
508
|
+
},
|
|
509
|
+
|
|
510
|
+
onExcise: async (context) => {
|
|
511
|
+
await context.clearScheduled();
|
|
512
|
+
return null;
|
|
513
|
+
},
|
|
536
514
|
};
|
|
537
515
|
```
|
|
538
516
|
|
|
@@ -566,11 +544,11 @@ PROSODY_CASSANDRA_NODES=localhost:9042 # Required for timer persistence
|
|
|
566
544
|
Or programmatically when creating the client:
|
|
567
545
|
|
|
568
546
|
```javascript
|
|
569
|
-
const client =
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
547
|
+
const client = await ProsodyClient.create({
|
|
548
|
+
bootstrapServers: "localhost:9092",
|
|
549
|
+
groupId: "my-application",
|
|
550
|
+
subscribedTopics: "my-topic",
|
|
551
|
+
cassandraNodes: "localhost:9042", // Required unless mock: true
|
|
574
552
|
});
|
|
575
553
|
```
|
|
576
554
|
|
|
@@ -578,11 +556,11 @@ For testing, you can use mock mode to avoid Cassandra dependency:
|
|
|
578
556
|
|
|
579
557
|
```javascript
|
|
580
558
|
// Mock mode for testing (timers work but aren't persisted)
|
|
581
|
-
const client =
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
559
|
+
const client = await ProsodyClient.create({
|
|
560
|
+
bootstrapServers: "localhost:9092",
|
|
561
|
+
groupId: "my-application",
|
|
562
|
+
subscribedTopics: "my-topic",
|
|
563
|
+
mock: true, // No Cassandra required in mock mode
|
|
586
564
|
});
|
|
587
565
|
```
|
|
588
566
|
|
|
@@ -591,24 +569,33 @@ const client = new ProsodyClient({
|
|
|
591
569
|
Prosody classifies errors as transient (temporary, can be retried) or permanent (won't be resolved by retrying). By
|
|
592
570
|
default, all errors are considered transient.
|
|
593
571
|
|
|
572
|
+
The error classes and decorators apply to `onMessage`, `onExcise`, and `onTimer`.
|
|
573
|
+
|
|
594
574
|
#### Using Decorators
|
|
595
575
|
|
|
596
576
|
If you're using TypeScript or a JavaScript environment that supports decorators, you can use the `@permanent` decorator
|
|
597
577
|
to classify exceptions that should not be retried:
|
|
598
578
|
|
|
599
579
|
```javascript
|
|
600
|
-
import {permanent, ProsodyClient} from
|
|
580
|
+
import { permanent, ProsodyClient } from "@prosody-events/prosody";
|
|
601
581
|
|
|
602
582
|
class MyHandler {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
583
|
+
@permanent(TypeError, AttributeError)
|
|
584
|
+
async onMessage(context, message, signal) {
|
|
585
|
+
// Your message handling logic here
|
|
586
|
+
// TypeError and AttributeError will be treated as permanent
|
|
587
|
+
// All other exceptions will be treated as transient (default behavior)
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async onExcise() {
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async onTimer() {}
|
|
609
596
|
}
|
|
610
597
|
|
|
611
|
-
const client =
|
|
598
|
+
const client = await ProsodyClient.create(config);
|
|
612
599
|
client.subscribe(new MyHandler());
|
|
613
600
|
```
|
|
614
601
|
|
|
@@ -617,23 +604,26 @@ client.subscribe(new MyHandler());
|
|
|
617
604
|
If you're not using decorators, you can still classify errors as permanent by throwing a `PermanentError`:
|
|
618
605
|
|
|
619
606
|
```javascript
|
|
620
|
-
import {PermanentError, ProsodyClient} from
|
|
607
|
+
import { PermanentError, ProsodyClient } from "@prosody-events/prosody";
|
|
621
608
|
|
|
622
609
|
const messageHandler = {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
}
|
|
610
|
+
onMessage: async (context, message, signal) => {
|
|
611
|
+
try {
|
|
612
|
+
// Your message handling logic here
|
|
613
|
+
} catch (error) {
|
|
614
|
+
if (error instanceof TypeError || error instanceof AttributeError) {
|
|
615
|
+
throw new PermanentError(error.message);
|
|
616
|
+
}
|
|
617
|
+
// All other exceptions will be treated as transient (default behavior)
|
|
618
|
+
throw error;
|
|
633
619
|
}
|
|
620
|
+
return null;
|
|
621
|
+
},
|
|
622
|
+
onExcise: async () => null,
|
|
623
|
+
onTimer: async () => {},
|
|
634
624
|
};
|
|
635
625
|
|
|
636
|
-
const client =
|
|
626
|
+
const client = await ProsodyClient.create(config);
|
|
637
627
|
client.subscribe(messageHandler);
|
|
638
628
|
```
|
|
639
629
|
|
|
@@ -644,6 +634,166 @@ client.subscribe(messageHandler);
|
|
|
644
634
|
- Be cautious with permanent errors as they prevent retries and can result in data loss.
|
|
645
635
|
- Consider system reliability and data consistency when classifying errors.
|
|
646
636
|
|
|
637
|
+
## Keyed State
|
|
638
|
+
|
|
639
|
+
Many stream transformations must reason across multiple events or timer firings. Windows, state machines, aggregates, and complex event processing all require state.
|
|
640
|
+
|
|
641
|
+
A Kafka key identifies an entity, such as a customer or order. Keyed state gives each key independent working state for these transformations. With Cassandra, the state survives restarts and partition reassignment.
|
|
642
|
+
|
|
643
|
+
Prosody selects the current message or timer key. It processes one event at a time for that key but can process other keys concurrently. By default, Prosody commits pending keyed-state changes only when the handler succeeds. If the handler returns an error, Prosody discards those changes.
|
|
644
|
+
|
|
645
|
+
Give most collections a time to live (TTL). Set the TTL beyond the longest timer or workflow that uses the collection. Omit it when state must remain for inactive keys.
|
|
646
|
+
|
|
647
|
+
### A counter for each key
|
|
648
|
+
|
|
649
|
+
Declare each collection once. Register it on the client. In a handler, get the current key's state from the event context:
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
const COUNT = value<number>("count", { ttlSeconds: 30 * 24 * 60 * 60 });
|
|
653
|
+
|
|
654
|
+
const client = await ProsodyClient.create({
|
|
655
|
+
...config,
|
|
656
|
+
stateCollections: [COUNT],
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
client.subscribe({
|
|
660
|
+
async onMessage(context) {
|
|
661
|
+
const count = context.state(COUNT);
|
|
662
|
+
await count.set(((await count.get()) ?? 0) + 1);
|
|
663
|
+
return null;
|
|
664
|
+
},
|
|
665
|
+
async onExcise(context) {
|
|
666
|
+
await context.state(COUNT).clear();
|
|
667
|
+
return null;
|
|
668
|
+
},
|
|
669
|
+
async onTimer() {},
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
Each Kafka key now has an independent counter. A counter expires when that key has no update for 30 days.
|
|
674
|
+
|
|
675
|
+
### Window activity into one notification
|
|
676
|
+
|
|
677
|
+
This example sends the first event for a user immediately. It collects later events for five minutes and then sends one summary.
|
|
678
|
+
|
|
679
|
+
The user ID is the Kafka key. Each user has an independent window.
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
const WINDOW = value<boolean>("window", { ttlSeconds: 24 * 60 * 60 });
|
|
683
|
+
const PENDING = messageDeque<Activity>("pending", {
|
|
684
|
+
capacity: 100,
|
|
685
|
+
ttlSeconds: 24 * 60 * 60,
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const handler = {
|
|
689
|
+
async onMessage(context, message) {
|
|
690
|
+
const window = context.state(WINDOW);
|
|
691
|
+
const pending = context.state(PENDING);
|
|
692
|
+
|
|
693
|
+
if (await window.get()) {
|
|
694
|
+
await pending.push(message);
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
await notify(message.key, [message]);
|
|
699
|
+
await window.set(true);
|
|
700
|
+
await context.clearAndSchedule(new Date(Date.now() + 5 * 60_000));
|
|
701
|
+
return null;
|
|
702
|
+
},
|
|
703
|
+
|
|
704
|
+
async onTimer(context, timer) {
|
|
705
|
+
const pending = context.state(PENDING);
|
|
706
|
+
const batch: Message<Activity>[] = [];
|
|
707
|
+
for await (const message of pending.values()) batch.push(message);
|
|
708
|
+
|
|
709
|
+
if (batch.length > 0) await notify(timer.key, batch);
|
|
710
|
+
await pending.clear();
|
|
711
|
+
await context.state(WINDOW).clear();
|
|
712
|
+
},
|
|
713
|
+
async onExcise(context) {
|
|
714
|
+
await context.state(PENDING).clear();
|
|
715
|
+
await context.state(WINDOW).clear();
|
|
716
|
+
await context.clearScheduled();
|
|
717
|
+
return null;
|
|
718
|
+
},
|
|
719
|
+
} satisfies EventHandler<Activity>;
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
See the complete, type-checked example for imports, types, client setup, and `notify`: [`examples/windowing.ts`](examples/windowing.ts).
|
|
723
|
+
|
|
724
|
+
Why this works:
|
|
725
|
+
|
|
726
|
+
- Register both definitions in `stateCollections` before you subscribe. Keyed state uses Cassandra unless `mock: true`.
|
|
727
|
+
- Use `clearAndSchedule`, not `schedule`, so a retried event does not add another timer for the same key.
|
|
728
|
+
- `capacity: 100` and the one-day TTL bound the saved backlog. Overflow drops the oldest message because this example only pushes.
|
|
729
|
+
- A `messageDeque` requires the original Kafka messages during the window. Use `deque` when topic retention or compaction cannot provide them.
|
|
730
|
+
- Prosody runs one handler at a time for each key, so a user's message and timer handlers cannot overlap.
|
|
731
|
+
- A notification is outside the state transaction. A retry can send it again. Use a stable operation ID to reject duplicate notifications.
|
|
732
|
+
|
|
733
|
+
### Collections and handles
|
|
734
|
+
|
|
735
|
+
A definition sets a collection's durable name, kind, and options. Register it once. Pass it to `context.state` in a handler.
|
|
736
|
+
|
|
737
|
+
Do not reuse a durable name for a different collection kind or payload type. Create handles inside the handler. Do not retain handles or iterators.
|
|
738
|
+
|
|
739
|
+
| Collection | JSON payload | Kafka message | Main operations |
|
|
740
|
+
| ------------------ | ------------ | ----------------- | -------------------------------------------------------------------- |
|
|
741
|
+
| Value | `value<T>` | `messageValue<P>` | `get`, `set`, `clear` |
|
|
742
|
+
| Ordered string map | `map<V>` | `messageMap<P>` | `get`, `getMany`, `has`, `set`, `delete`, `entries`, `keys`, `clear` |
|
|
743
|
+
| Deque | `deque<T>` | `messageDeque<P>` | `push`, `unshift`, `pop`, `shift`, `at`, `length`, `values`, `clear` |
|
|
744
|
+
|
|
745
|
+
All operations are asynchronous. Map and deque scans are asynchronous iterables. A `for await` loop can stop early safely.
|
|
746
|
+
|
|
747
|
+
Map keys are strings. `null` and `undefined` mean absence. Do not store these values. Use `clear()` or `delete()`.
|
|
748
|
+
|
|
749
|
+
### When keyed-state changes become visible
|
|
750
|
+
|
|
751
|
+
By default, retries do not see pending state from a failed attempt. Reads in a handler see its earlier keyed-state writes. Prosody commits pending changes when the event succeeds and discards them when the handler throws.
|
|
752
|
+
|
|
753
|
+
This transaction applies only to keyed state. Some workflows need state changes before the handler ends, so each collection also provides explicit controls:
|
|
754
|
+
|
|
755
|
+
- `readUncommitted: true` persists keyed-state changes before Prosody records the event as complete. If the process stops between these steps, Prosody can process the same event again. The retry sees state changes from the earlier attempt. You must make these keyed-state changes idempotent. Each retry must produce the same state.
|
|
756
|
+
- `commit()` commits the collection's pending changes before the handler ends. A later handler failure does not remove them.
|
|
757
|
+
- `rollback()` discards pending changes since the last `commit()`. It cannot undo committed changes.
|
|
758
|
+
|
|
759
|
+
### Published state
|
|
760
|
+
|
|
761
|
+
Some callers need only the current value for a key. They can accept a stale value or a race with a concurrent update.
|
|
762
|
+
|
|
763
|
+
Use topics and event sourcing when a consumer must process each state change in order. Use published state for direct, read-only lookup of persisted keyed state. The caller does not need to consume the owner's topics or maintain a separate lookup store.
|
|
764
|
+
|
|
765
|
+
Configure the subsystem name on each publisher. Enable publication on the collection definition. Register the definition on the Prosody client:
|
|
766
|
+
|
|
767
|
+
```js
|
|
768
|
+
const CURRENT_ORDER = value("current-order", { published: true });
|
|
769
|
+
const owner = await ProsodyClient.create({
|
|
770
|
+
...config,
|
|
771
|
+
subsystem: "checkout",
|
|
772
|
+
stateCollections: [CURRENT_ORDER],
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// The handler uses the key from its current event.
|
|
776
|
+
const currentOrder = context.state(CURRENT_ORDER);
|
|
777
|
+
await currentOrder.set({ sku: "book" });
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
Read published state from a handler or other application code. The Prosody client does not need an active subscription.
|
|
781
|
+
|
|
782
|
+
Use the subsystem and the same definition to open a reader:
|
|
783
|
+
|
|
784
|
+
```js
|
|
785
|
+
const orderReader = await client.state("checkout", CURRENT_ORDER);
|
|
786
|
+
const currentOrder = await orderReader.get("customer-123");
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
The reader cannot see pending changes that exist only in a handler. It cannot change the collection. Each read takes an explicit key because no handler supplies one.
|
|
790
|
+
|
|
791
|
+
Map and deque readers fetch data in chunks. They do not load the complete collection before iteration starts.
|
|
792
|
+
|
|
793
|
+
The default cache window is five seconds. Set `readCache: { ttlMs }` to select a different window. Set `readCache: false` to bypass the cache.
|
|
794
|
+
|
|
795
|
+
To stop publication, deploy the definition with `published: false`. Keep the definition registered during that deployment. Keep the subsystem configured during that deployment.
|
|
796
|
+
|
|
647
797
|
## OpenTelemetry Tracing
|
|
648
798
|
|
|
649
799
|
Prosody supports OpenTelemetry tracing, allowing you to monitor and analyze the performance of your Kafka-based
|
|
@@ -666,19 +816,21 @@ npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/exporter-t
|
|
|
666
816
|
To initialize tracing in your application:
|
|
667
817
|
|
|
668
818
|
```javascript
|
|
669
|
-
const opentelemetry = require(
|
|
670
|
-
const {NodeSDK} = require(
|
|
671
|
-
const {
|
|
819
|
+
const opentelemetry = require("@opentelemetry/api");
|
|
820
|
+
const { NodeSDK } = require("@opentelemetry/sdk-node");
|
|
821
|
+
const {
|
|
822
|
+
OTLPTraceExporter,
|
|
823
|
+
} = require("@opentelemetry/exporter-trace-otlp-http");
|
|
672
824
|
|
|
673
825
|
const sdk = new NodeSDK({
|
|
674
|
-
|
|
675
|
-
|
|
826
|
+
traceExporter: new OTLPTraceExporter(),
|
|
827
|
+
serviceName: "my-service-name",
|
|
676
828
|
});
|
|
677
829
|
|
|
678
830
|
sdk.start();
|
|
679
831
|
|
|
680
832
|
// Creates a tracer from the global tracer provider
|
|
681
|
-
const tracer = opentelemetry.trace.getTracer(
|
|
833
|
+
const tracer = opentelemetry.trace.getTracer("my-service-name");
|
|
682
834
|
```
|
|
683
835
|
|
|
684
836
|
### Setting OpenTelemetry Environment Variables
|
|
@@ -694,34 +846,39 @@ OTEL_SERVICE_NAME=my-service-name
|
|
|
694
846
|
For more information on these and other OpenTelemetry environment variables, refer to
|
|
695
847
|
the [OpenTelemetry specification](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration).
|
|
696
848
|
|
|
849
|
+
Call `flushTelemetry()` to export pending telemetry. Call `shutdownTelemetry()` before the process exits.
|
|
850
|
+
|
|
697
851
|
### Using Tracing in Your Application
|
|
698
852
|
|
|
699
853
|
After initializing tracing, you can define spans in your application, and they will be properly propagated through
|
|
700
854
|
Kafka:
|
|
701
855
|
|
|
702
856
|
```javascript
|
|
703
|
-
const {ProsodyClient} = require(
|
|
704
|
-
const opentelemetry = require(
|
|
857
|
+
const { ProsodyClient } = require("@prosody-events/prosody");
|
|
858
|
+
const opentelemetry = require("@opentelemetry/api");
|
|
705
859
|
|
|
706
|
-
const tracer = opentelemetry.trace.getTracer(
|
|
860
|
+
const tracer = opentelemetry.trace.getTracer("my-service-name");
|
|
707
861
|
|
|
708
|
-
const client =
|
|
709
|
-
|
|
710
|
-
|
|
862
|
+
const client = await ProsodyClient.create({
|
|
863
|
+
groupId: "my-consumer-group",
|
|
864
|
+
subscribedTopics: "my-topic",
|
|
711
865
|
});
|
|
712
866
|
|
|
713
867
|
const messageHandler = {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
868
|
+
onMessage: async (context, message, signal) => {
|
|
869
|
+
const span = tracer.startSpan("process-message");
|
|
870
|
+
try {
|
|
871
|
+
// Process the received message
|
|
872
|
+
span.addEvent("message.received", {
|
|
873
|
+
"message.payload": JSON.stringify(message),
|
|
874
|
+
});
|
|
875
|
+
} finally {
|
|
876
|
+
span.end();
|
|
724
877
|
}
|
|
878
|
+
return null;
|
|
879
|
+
},
|
|
880
|
+
onExcise: async () => null,
|
|
881
|
+
onTimer: async () => {},
|
|
725
882
|
};
|
|
726
883
|
|
|
727
884
|
client.subscribe(messageHandler);
|
|
@@ -776,55 +933,53 @@ Strategies for achieving idempotence:
|
|
|
776
933
|
- Each message advances the state machine, allowing for idempotent processing and easy failure recovery.
|
|
777
934
|
- Particularly useful for complex, distributed transactions across multiple services.
|
|
778
935
|
|
|
779
|
-
###
|
|
936
|
+
### Application shutdown
|
|
937
|
+
|
|
938
|
+
A Prosody client runs a subscription, timers, and other services in the background. Before an application terminates, it must stop all client services. `unsubscribe()` stops only the active subscription.
|
|
780
939
|
|
|
781
|
-
|
|
940
|
+
Call `shutdown()` when the application terminates. It stops all client services and rejects new operations. Call `unsubscribe()` only when the application will use the client again. You do not need to call `unsubscribe()` before `shutdown()`.
|
|
782
941
|
|
|
783
942
|
```javascript
|
|
784
|
-
|
|
785
|
-
await client.unsubscribe();
|
|
943
|
+
await client.shutdown();
|
|
786
944
|
```
|
|
787
945
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
1. Completion and commitment of all in-flight work
|
|
791
|
-
2. Quick rebalancing, allowing other consumers to take over partitions
|
|
792
|
-
3. Proper release of resources
|
|
793
|
-
|
|
794
|
-
Implement shutdown handling in your application:
|
|
946
|
+
Handle application shutdown:
|
|
795
947
|
|
|
796
948
|
```javascript
|
|
797
|
-
const {ProsodyClient} = require(
|
|
949
|
+
const { ProsodyClient } = require("@prosody-events/prosody");
|
|
798
950
|
|
|
799
951
|
async function main() {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
const messageHandler = {
|
|
806
|
-
onMessage: async (context, message, signal) => {
|
|
807
|
-
// Process the message
|
|
808
|
-
}
|
|
809
|
-
};
|
|
810
|
-
|
|
811
|
-
client.subscribe(messageHandler);
|
|
952
|
+
const client = await ProsodyClient.create({
|
|
953
|
+
groupId: "my-consumer-group",
|
|
954
|
+
subscribedTopics: "my-topic",
|
|
955
|
+
});
|
|
812
956
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
957
|
+
const messageHandler = {
|
|
958
|
+
onMessage: async (context, message, signal) => {
|
|
959
|
+
// Process the message.
|
|
960
|
+
return null;
|
|
961
|
+
},
|
|
962
|
+
onExcise: async () => null,
|
|
963
|
+
onTimer: async () => {},
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
client.subscribe(messageHandler);
|
|
967
|
+
|
|
968
|
+
// Resolve this promise after a shutdown signal.
|
|
969
|
+
const shutdownPromise = new Promise((resolve) => {
|
|
970
|
+
const shutdown = async (signal) => {
|
|
971
|
+
console.log(`Received ${signal}. Client shutdown starts.`);
|
|
972
|
+
await client.shutdown();
|
|
973
|
+
resolve();
|
|
974
|
+
};
|
|
820
975
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
976
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
977
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
978
|
+
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
979
|
+
});
|
|
825
980
|
|
|
826
|
-
|
|
827
|
-
|
|
981
|
+
// Wait for a shutdown signal.
|
|
982
|
+
await shutdownPromise;
|
|
828
983
|
}
|
|
829
984
|
|
|
830
985
|
main().catch(console.error);
|
|
@@ -852,19 +1007,26 @@ Example of using AbortSignal in message processing:
|
|
|
852
1007
|
|
|
853
1008
|
```javascript
|
|
854
1009
|
const messageHandler = {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1010
|
+
onMessage: async (context, message, signal) => {
|
|
1011
|
+
// Pass the signal to fetch calls
|
|
1012
|
+
const response = await fetch("https://api.example.com", { signal });
|
|
1013
|
+
const data = await response.json();
|
|
1014
|
+
|
|
1015
|
+
// Pass the signal to database operations
|
|
1016
|
+
await db.query(
|
|
1017
|
+
"INSERT INTO messages (payload) VALUES ($1)",
|
|
1018
|
+
[message.payload],
|
|
1019
|
+
{ signal },
|
|
1020
|
+
);
|
|
1021
|
+
|
|
1022
|
+
// Process the data...
|
|
1023
|
+
|
|
1024
|
+
// Send a message, passing the abort signal
|
|
1025
|
+
await client.send("topic", "key", { data: "value" }, signal);
|
|
1026
|
+
return null;
|
|
1027
|
+
},
|
|
1028
|
+
onExcise: async () => null,
|
|
1029
|
+
onTimer: async () => {},
|
|
868
1030
|
};
|
|
869
1031
|
```
|
|
870
1032
|
|
|
@@ -885,16 +1047,16 @@ Prosody uses an automated release process managed by GitHub Actions. Here's an o
|
|
|
885
1047
|
1. **Trigger**: The release process is triggered automatically on pushes to the `main` branch.
|
|
886
1048
|
|
|
887
1049
|
2. **Release Please**: The process starts with the "Release Please" action, which:
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1050
|
+
- Analyzes commit messages since the last release.
|
|
1051
|
+
- Creates or updates a release pull request with changelog updates and version bumps.
|
|
1052
|
+
- When the PR is merged, it creates a GitHub release and a git tag.
|
|
891
1053
|
|
|
892
1054
|
3. **Build Process**: If a new release is created, the following build jobs are triggered:
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1055
|
+
- Linux builds for x86_64 and aarch64 (glibc).
|
|
1056
|
+
- Windows build for x64.
|
|
1057
|
+
- macOS build for aarch64 (Apple Silicon).
|
|
896
1058
|
|
|
897
|
-
4. **Testing**: The built binaries are tested on Linux (x86_64 and aarch64) with Node.js
|
|
1059
|
+
4. **Testing**: The built binaries are tested on Linux (x86_64 and aarch64) with Node.js 24.
|
|
898
1060
|
|
|
899
1061
|
5. **Artifact Upload**: Each build job uploads its artifacts (Node.js native addons) to GitHub Actions.
|
|
900
1062
|
|
|
@@ -924,20 +1086,37 @@ your changes before merging to `main`.
|
|
|
924
1086
|
|
|
925
1087
|
### ProsodyClient
|
|
926
1088
|
|
|
927
|
-
- `
|
|
928
|
-
- `send(topic: string, key: string, payload:
|
|
1089
|
+
- `ProsodyClient.create(config: Configuration): Promise<ProsodyClient>`: Initialize a client without blocking the Node.js event loop.
|
|
1090
|
+
- `send<P>(topic: string, key: string, payload: P & JsonCompatible<P>, signal?: AbortSignal): Promise<void>`: Send a statically checked JSON-compatible message to a specified
|
|
929
1091
|
topic.
|
|
930
|
-
- `
|
|
1092
|
+
- `excise(topic: string, key: string, signal?: AbortSignal): Promise<void>`: Send an excise record for a key.
|
|
1093
|
+
- `request<R>(topic, key, payload: JsonValue, options): Promise<ReadonlyMap<string, Outcome<R>>>`: Return one outcome for each subsystem.
|
|
1094
|
+
- `requestExcise<R>(topic, key, options): Promise<ReadonlyMap<string, Outcome<R>>>`: Return one excise outcome for each subsystem.
|
|
1095
|
+
- `consumerState(): Promise<ConsumerState>`: Get the current state of the consumer.
|
|
1096
|
+
- `assignedPartitionCount(): Promise<number>`: Get the assigned partition count.
|
|
1097
|
+
- `isStalled(): Promise<boolean>`: Test whether the consumer is stalled.
|
|
931
1098
|
- `sourceSystem: string`: Get the source system identifier configured for the client.
|
|
932
|
-
- `
|
|
933
|
-
- `
|
|
1099
|
+
- `state<T>(subsystem: string, definition: ValueDefinition<T>): Promise<PublishedValue<T>>`: Open a read-only published value.
|
|
1100
|
+
- `state<V>(subsystem: string, definition: MapDefinition<V>): Promise<PublishedMap<V>>`: Open a read-only published map.
|
|
1101
|
+
- `state<T>(subsystem: string, definition: DequeDefinition<T>): Promise<PublishedDeque<T>>`: Open a read-only published deque.
|
|
1102
|
+
- `subscribe<P = JsonValue, R = JsonValue>(eventHandler: EventHandler<P, R>): Promise<void>`: Subscribe with typed payload and response values.
|
|
1103
|
+
- `unsubscribe(): Promise<void>`: Stop the consumer. You can subscribe again later.
|
|
1104
|
+
- `shutdown(): Promise<void>`: Stop all client services. Concurrent and repeated calls await the same operation.
|
|
1105
|
+
|
|
1106
|
+
### AdminClient
|
|
1107
|
+
|
|
1108
|
+
- `new AdminClient(bootstrapServers)`: Create an admin client for the specified Kafka servers.
|
|
1109
|
+
- `createTopic(name, partitions, replicationFactor)`: Create a Kafka topic.
|
|
1110
|
+
- `deleteTopic(name)`: Delete a Kafka topic.
|
|
934
1111
|
|
|
935
1112
|
### EventHandler
|
|
936
1113
|
|
|
937
1114
|
Interface for handling messages and timers:
|
|
938
1115
|
|
|
939
|
-
- `
|
|
940
|
-
- `
|
|
1116
|
+
- `EventHandler<P = JsonValue, R = JsonValue>` carries the payload and response types through each callback.
|
|
1117
|
+
- `onMessage: (context, message, signal) => MaybePromise<R & JsonCompatible<R>>`: Handle incoming messages.
|
|
1118
|
+
- `onExcise: (context, message, signal) => MaybePromise<R & JsonCompatible<R>>`: Handle excise records.
|
|
1119
|
+
- `onTimer: (context, timer, signal) => MaybePromise<void>`: Handle timer events.
|
|
941
1120
|
|
|
942
1121
|
### Message
|
|
943
1122
|
|
|
@@ -948,11 +1127,21 @@ Represents a Kafka message with the following properties:
|
|
|
948
1127
|
- `offset: bigint`: The message offset within the partition.
|
|
949
1128
|
- `timestamp: Date`: The timestamp when the message was created or sent.
|
|
950
1129
|
- `key: string`: The message key.
|
|
951
|
-
- `payload:
|
|
1130
|
+
- `payload: P`: The statically typed message payload.
|
|
1131
|
+
|
|
1132
|
+
`Message` takes an optional payload type parameter, `Message<P>`, used by handlers and message-backed state collections to type `payload`. Unparameterized `Message` is `Message<JsonValue>`, preserving useful JSON safety without requiring an application-specific payload type.
|
|
1133
|
+
|
|
1134
|
+
`JsonValue` describes arbitrary JSON data. `JsonCompatible<T>` checks a known application type recursively.
|
|
1135
|
+
|
|
1136
|
+
Ordinary interfaces work with `send()`. TypeScript rejects functions, `undefined`, `Date`, symbols, bigints, and invalid nested fields before serialization.
|
|
1137
|
+
|
|
1138
|
+
### ExciseMessage
|
|
1139
|
+
|
|
1140
|
+
An `ExciseMessage` has `topic`, `partition`, `offset`, `timestamp`, and `key` properties. It has no `payload` property.
|
|
952
1141
|
|
|
953
1142
|
### Context
|
|
954
1143
|
|
|
955
|
-
Represents the
|
|
1144
|
+
Represents the current event context:
|
|
956
1145
|
|
|
957
1146
|
- `onCancel(): Promise<void>`: A method that resolves when the context is cancelled.
|
|
958
1147
|
- `shouldCancel: boolean`: A property indicating whether the context has been cancelled.
|
|
@@ -965,6 +1154,10 @@ Timer scheduling methods:
|
|
|
965
1154
|
- `clearScheduled(): Promise<void>`: Removes all scheduled timers
|
|
966
1155
|
- `scheduled(): Promise<Date[]>`: Returns an array of all scheduled timer times
|
|
967
1156
|
|
|
1157
|
+
Keyed-state binding:
|
|
1158
|
+
|
|
1159
|
+
- `state(definition): ValueState<T> | MapState<V> | DequeState<T>`: Bind a registered collection for the current attempt. Message definitions return handles that contain `Message<P>`. An unregistered or mismatched definition throws `PermanentStateError`. See [Keyed State](#keyed-state-2).
|
|
1160
|
+
|
|
968
1161
|
### Timer
|
|
969
1162
|
|
|
970
1163
|
Represents a timer that has fired, provided to the `onTimer` method:
|
|
@@ -972,6 +1165,117 @@ Represents a timer that has fired, provided to the `onTimer` method:
|
|
|
972
1165
|
- `key: string`: The entity key identifying what this timer belongs to
|
|
973
1166
|
- `time: Date`: The time when this timer was scheduled to fire
|
|
974
1167
|
|
|
1168
|
+
### Requests
|
|
1169
|
+
|
|
1170
|
+
- `RequestOptions`: Contains `subsystems`, `timeoutMs`, and an optional `signal`.
|
|
1171
|
+
- `Outcome<T>`: A `Success<T>` or `Failure` result for one subsystem.
|
|
1172
|
+
- `Success<T>`: Contains `ok: true` and `value: T`.
|
|
1173
|
+
- `Failure`: Contains `ok: false` and a `ResponseError`.
|
|
1174
|
+
- `ResponseError`: A handler, timeout, format-mismatch, or malformed-response error.
|
|
1175
|
+
|
|
1176
|
+
### Configuration types
|
|
1177
|
+
|
|
1178
|
+
- `Configuration`: Contains the client settings. See [Configuration](CONFIGURATION.md).
|
|
1179
|
+
- `ConsumerState`: Identifies the consumer lifecycle state.
|
|
1180
|
+
- `Mode`: Selects the client processing mode.
|
|
1181
|
+
- `ReadCacheConfiguration`: Configures the default published-state cache.
|
|
1182
|
+
- `ReadCacheOptions`: Overrides the cache for one published collection.
|
|
1183
|
+
|
|
1184
|
+
### Payload types
|
|
1185
|
+
|
|
1186
|
+
- `JsonPrimitive`: A JSON null, boolean, number, or string.
|
|
1187
|
+
- `JsonValue`: Any recursively JSON-compatible value.
|
|
1188
|
+
- `JsonCompatible<T>`: Rejects non-JSON members in a known application type.
|
|
1189
|
+
- `MaybePromise<T>`: A value or a promise-like value.
|
|
1190
|
+
|
|
1191
|
+
### Keyed State
|
|
1192
|
+
|
|
1193
|
+
Definition constructors (each returns a frozen definition object used both in `Configuration.stateCollections` and with `context.state()`):
|
|
1194
|
+
|
|
1195
|
+
- `value<T = JsonValue>(name: string, options?: PublishedStateDefinitionOptions): ValueDefinition<T>`
|
|
1196
|
+
- `map<V = JsonValue>(name: string, options?: MapDefinitionOptions): MapDefinition<V>`
|
|
1197
|
+
- `deque<T = JsonValue>(name: string, options?: DequeDefinitionOptions): DequeDefinition<T>`
|
|
1198
|
+
- `messageValue<P = JsonValue>(name: string, options?: StateDefinitionOptions): MessageValueDefinition<P>`
|
|
1199
|
+
- `messageMap<P = JsonValue>(name: string, options?: MessageMapDefinitionOptions): MessageMapDefinition<P>`
|
|
1200
|
+
- `messageDeque<P = JsonValue>(name: string, options?: MessageDequeDefinitionOptions): MessageDequeDefinition<P>`
|
|
1201
|
+
|
|
1202
|
+
`StateDefinitionOptions`: `{ ttlSeconds?: number; readUncommitted?: boolean }`. `PublishedStateDefinitionOptions` adds `{ published?: boolean; readCache?: { ttlMs: number } | false }` for JSON definitions. Map and deque option types add `keysetLimit` and `capacity`, respectively; their message equivalents omit publication options.
|
|
1203
|
+
|
|
1204
|
+
`ValueState<T>`:
|
|
1205
|
+
|
|
1206
|
+
- `get(): Promise<T | null>`
|
|
1207
|
+
- `set(value: T): Promise<void>`
|
|
1208
|
+
- `clear(): Promise<void>`
|
|
1209
|
+
- `commit(): Promise<void>`
|
|
1210
|
+
- `rollback(): Promise<void>`
|
|
1211
|
+
|
|
1212
|
+
`MapState<V>` (keys are `string`):
|
|
1213
|
+
|
|
1214
|
+
- `get(key: string): Promise<V | null>`
|
|
1215
|
+
- `getMany(keys: readonly string[]): Promise<(V | null)[]>`
|
|
1216
|
+
- `has(key: string): Promise<boolean>`
|
|
1217
|
+
- `set(key: string, value: V): Promise<void>`
|
|
1218
|
+
- `delete(key: string): Promise<void>`
|
|
1219
|
+
- `clear(): Promise<void>`
|
|
1220
|
+
- `entries(direction?: ScanDirection): AsyncIterableIterator<[string, V]>`
|
|
1221
|
+
- `keys(direction?: ScanDirection): AsyncIterableIterator<string>`
|
|
1222
|
+
- `values(direction?: ScanDirection): AsyncIterableIterator<V>`
|
|
1223
|
+
- `[Symbol.asyncIterator](): AsyncIterableIterator<[string, V]>`
|
|
1224
|
+
- `commit(): Promise<void>`
|
|
1225
|
+
- `rollback(): Promise<void>`
|
|
1226
|
+
|
|
1227
|
+
`DequeState<T>`:
|
|
1228
|
+
|
|
1229
|
+
- `push(item: T): Promise<void>`
|
|
1230
|
+
- `unshift(item: T): Promise<void>`
|
|
1231
|
+
- `pop(): Promise<T | null>`
|
|
1232
|
+
- `shift(): Promise<T | null>`
|
|
1233
|
+
- `length(): Promise<number>`
|
|
1234
|
+
- `isEmpty(): Promise<boolean>`
|
|
1235
|
+
- `clear(): Promise<void>`
|
|
1236
|
+
- `at(index: number): Promise<T | null>`
|
|
1237
|
+
- `values(direction?: ScanDirection): AsyncIterableIterator<T>`
|
|
1238
|
+
- `[Symbol.asyncIterator](): AsyncIterableIterator<T>`
|
|
1239
|
+
- `commit(): Promise<void>`
|
|
1240
|
+
- `rollback(): Promise<void>`
|
|
1241
|
+
|
|
1242
|
+
`ScanDirection`: `"forward" | "backward"`.
|
|
1243
|
+
|
|
1244
|
+
Published readers take the user key as their first argument. `PublishedValue<T>` provides `get`. `PublishedMap<V>` provides `get`, `getMany`, `has`, `entries`, `keys`, and `values`. `PublishedDeque<T>` provides `at`, `length`, `isEmpty`, and `values`. The scan methods return `AsyncIterableIterator` directly.
|
|
1245
|
+
|
|
1246
|
+
`StateCollectionConfig` defines one `stateCollections` entry. It contains `name`, `kind`, `payload`, and the applicable collection options. Use a definition constructor to create this value.
|
|
1247
|
+
|
|
1248
|
+
JSON definitions also accept `readCache`. This option applies when the definition opens published state. It is not part of `StateCollectionConfig`.
|
|
1249
|
+
|
|
1250
|
+
The public definition types are `ValueDefinition<T>`, `MapDefinition<V>`, `DequeDefinition<T>`, `MessageValueDefinition<P>`, `MessageMapDefinition<P>`, and `MessageDequeDefinition<P>`.
|
|
1251
|
+
|
|
1252
|
+
All definitions expose `name`, `kind`, `payload`, `ttlSeconds`, and `readUncommitted`. JSON definitions also expose `published` and `readCache`. Map definitions expose `keysetLimit`. Deque definitions expose `capacity`.
|
|
1253
|
+
|
|
1254
|
+
Errors:
|
|
1255
|
+
|
|
1256
|
+
- `TransientStateError extends TransientError`: Reports a keyed-state error that Prosody can retry.
|
|
1257
|
+
- `PermanentStateError extends PermanentError`: Reports a keyed-state error that another attempt cannot resolve.
|
|
1258
|
+
- `isStateError(error: unknown): error is PermanentStateError | TransientStateError`: type-guard narrowing an error to either state error class.
|
|
1259
|
+
|
|
1260
|
+
Handler error types and decorators:
|
|
1261
|
+
|
|
1262
|
+
- `EventHandlerError`: Base class with an abstract `isPermanent` property.
|
|
1263
|
+
- `TransientError`: Has `isPermanent: false` and marks an error as retriable.
|
|
1264
|
+
- `PermanentError`: Has `isPermanent: true` and marks an error as final.
|
|
1265
|
+
- `transient(...errorTypes)`: Creates a transient-error decorator.
|
|
1266
|
+
- `permanent(...errorTypes)`: Creates a permanent-error decorator.
|
|
1267
|
+
|
|
1268
|
+
### Logging and telemetry
|
|
1269
|
+
|
|
1270
|
+
- `Logger`: Provides `error`, `warn`, `info`, `debug`, and `trace` methods.
|
|
1271
|
+
- `initialize()`: Prepare the logging and tracing system during application startup.
|
|
1272
|
+
- `loggerIsSet()`: Test whether the application configured a logger.
|
|
1273
|
+
- `setLogger(logger)`: Replaces the logger.
|
|
1274
|
+
- `setLoggerIfUnset(logger)`: Sets the logger only when no logger exists.
|
|
1275
|
+
- `getCurrentLogger()`: Returns the current JavaScript logger.
|
|
1276
|
+
- `flushTelemetry()`: Exports pending telemetry.
|
|
1277
|
+
- `shutdownTelemetry()`: Exports pending telemetry and stops its providers.
|
|
1278
|
+
|
|
975
1279
|
## License
|
|
976
1280
|
|
|
977
1281
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|