@prosody-events/prosody 0.5.0 → 0.6.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 +157 -55
- package/bindings.d.ts +302 -404
- package/bindings.js +168 -85
- package/index.d.ts +30 -962
- package/index.js +42 -1898
- package/lib/client.d.ts +232 -0
- package/lib/client.js +347 -0
- package/lib/context.d.ts +161 -0
- package/lib/context.js +86 -0
- package/lib/errors.d.ts +103 -0
- package/lib/errors.js +102 -0
- package/lib/logging.d.ts +75 -0
- package/lib/logging.js +99 -0
- package/lib/payload.d.ts +59 -0
- package/lib/state/bridge.js +103 -0
- package/lib/state/codec.js +206 -0
- package/lib/state/definitions.d.ts +233 -0
- package/lib/state/definitions.js +151 -0
- package/lib/state/deque.d.ts +75 -0
- package/lib/state/deque.js +77 -0
- package/lib/state/handle.d.ts +25 -0
- package/lib/state/handle.js +26 -0
- package/lib/state/iterator.js +66 -0
- package/lib/state/map.d.ts +94 -0
- package/lib/state/map.js +79 -0
- package/lib/state/published.d.ts +113 -0
- package/lib/state/published.js +147 -0
- package/lib/state/query.d.ts +85 -0
- package/lib/state/query.js +206 -0
- package/lib/state/set.d.ts +52 -0
- package/lib/state/set.js +54 -0
- package/lib/state/value.d.ts +33 -0
- package/lib/state/value.js +31 -0
- package/package.json +21 -20
package/index.js
CHANGED
|
@@ -4,1910 +4,51 @@
|
|
|
4
4
|
* Provides functionality for sending messages, subscribing to topics, and managing consumer state.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* @typedef {Object} Logger
|
|
9
|
-
* @property {Function} error - Function for logging error messages. Called with (message, metadata).
|
|
10
|
-
* @property {Function} warn - Function for logging warning messages. Called with (message, metadata).
|
|
11
|
-
* @property {Function} info - Function for logging informational messages. Called with (message, metadata).
|
|
12
|
-
* @property {Function} debug - Function for logging debug messages. Called with (message, metadata).
|
|
13
|
-
* @property {Function} trace - Function for logging trace messages. Called with (message, metadata).
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @typedef {Object} EventHandler
|
|
18
|
-
* @property {Function} onMessage - Handles a message and returns its response.
|
|
19
|
-
* @property {Function} onExcise - Handles an excise record and returns its response.
|
|
20
|
-
* @property {Function} onTimer - Handles a timer and returns no value.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @typedef {import('./bindings').Configuration} Configuration
|
|
25
|
-
* @typedef {import('./bindings').ConsumerState} ConsumerState
|
|
26
|
-
* @typedef {import('./bindings').Context} Context
|
|
27
|
-
* @typedef {import('./bindings').ExciseMessage} ExciseMessage
|
|
28
|
-
* @typedef {import('./bindings').Message} Message
|
|
29
|
-
* @typedef {import('./bindings').Timer} Timer
|
|
30
|
-
* @typedef {import('./bindings').Mode} Mode
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
const { types } = require("node:util");
|
|
34
|
-
|
|
35
|
-
const {
|
|
36
|
-
context: otelContext,
|
|
37
|
-
propagation,
|
|
38
|
-
trace,
|
|
39
|
-
SpanStatusCode,
|
|
40
|
-
} = require("@opentelemetry/api");
|
|
41
|
-
|
|
42
7
|
const {
|
|
43
8
|
AdminClient,
|
|
44
|
-
Message: NativeMessage,
|
|
45
|
-
Mode,
|
|
46
|
-
NativeClient,
|
|
47
9
|
ConsumerState,
|
|
48
|
-
|
|
49
|
-
initialize,
|
|
10
|
+
Mode,
|
|
50
11
|
flushTelemetry,
|
|
51
|
-
|
|
52
|
-
setLogger: setLoggerInternal,
|
|
53
|
-
setLoggerIfUnset: setLoggerIfUnsetInternal,
|
|
12
|
+
initialize,
|
|
54
13
|
shutdownTelemetry,
|
|
55
14
|
} = require("./bindings");
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const defaultLogger = {
|
|
94
|
-
error: (message, metadata) =>
|
|
95
|
-
metadata !== undefined
|
|
96
|
-
? console.error(message, metadata)
|
|
97
|
-
: console.error(message),
|
|
98
|
-
warn: (message, metadata) =>
|
|
99
|
-
metadata !== undefined
|
|
100
|
-
? console.warn(message, metadata)
|
|
101
|
-
: console.warn(message),
|
|
102
|
-
info: (message, metadata) =>
|
|
103
|
-
metadata !== undefined
|
|
104
|
-
? console.info(message, metadata)
|
|
105
|
-
: console.info(message),
|
|
106
|
-
debug: (message, metadata) =>
|
|
107
|
-
metadata !== undefined
|
|
108
|
-
? console.debug(message, metadata)
|
|
109
|
-
: console.debug(message),
|
|
110
|
-
trace: (message, metadata) =>
|
|
111
|
-
metadata !== undefined
|
|
112
|
-
? console.debug(message, metadata)
|
|
113
|
-
: console.debug(message),
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// Keep reference to current logger for use in handlers
|
|
117
|
-
let currentLogger = defaultLogger;
|
|
118
|
-
|
|
119
|
-
function transformLogger(logger) {
|
|
120
|
-
return {
|
|
121
|
-
info: ([msg, meta]) => logger.info(msg, meta),
|
|
122
|
-
error: ([msg, meta]) => logger.error(msg, meta),
|
|
123
|
-
debug: ([msg, meta]) => logger.debug(msg, meta),
|
|
124
|
-
warn: ([msg, meta]) => logger.warn(msg, meta),
|
|
125
|
-
trace: ([msg, meta]) => logger.trace(msg, meta),
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Gets the current configured logger.
|
|
131
|
-
* @returns {Logger|null|undefined} The current logger instance, or null/undefined if no logger is configured.
|
|
132
|
-
*/
|
|
133
|
-
function getCurrentLogger() {
|
|
134
|
-
return currentLogger;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
initialize();
|
|
138
|
-
setLoggerIfUnset(defaultLogger);
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Sets a new JavaScript logger for the Prosody client.
|
|
142
|
-
*
|
|
143
|
-
* This function configures the logging system to use the provided JavaScript logger
|
|
144
|
-
* for all log output. The logger must implement all required log levels.
|
|
145
|
-
*
|
|
146
|
-
* @param {Logger} logger - The JavaScript logger object.
|
|
147
|
-
* @throws {Error} If creating the new JavaScript logger fails.
|
|
148
|
-
*/
|
|
149
|
-
function setLogger(logger) {
|
|
150
|
-
currentLogger = logger;
|
|
151
|
-
setLoggerInternal(transformLogger(logger));
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Sets a JavaScript logger only if no logger is currently configured.
|
|
156
|
-
*
|
|
157
|
-
* This function is useful for providing a default logger without overriding
|
|
158
|
-
* an existing one that may have been set earlier.
|
|
159
|
-
*
|
|
160
|
-
* @param {Logger} logger - The JavaScript logger object.
|
|
161
|
-
* @returns {boolean} True if the logger was set (no previous logger existed), false if a logger was already configured.
|
|
162
|
-
* @throws {Error} If creating the new JavaScript logger fails.
|
|
163
|
-
*/
|
|
164
|
-
function setLoggerIfUnset(logger) {
|
|
165
|
-
const wasSet = setLoggerIfUnsetInternal(transformLogger(logger));
|
|
166
|
-
if (wasSet) {
|
|
167
|
-
currentLogger = logger;
|
|
168
|
-
}
|
|
169
|
-
return wasSet;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Main client for interacting with Prosody messaging system.
|
|
174
|
-
* Provides functionality for sending messages, subscribing to topics, and managing consumer state.
|
|
175
|
-
*/
|
|
176
|
-
class ProsodyClient {
|
|
177
|
-
/**
|
|
178
|
-
* Use {@link ProsodyClient.create}.
|
|
179
|
-
* @private
|
|
180
|
-
*/
|
|
181
|
-
constructor() {
|
|
182
|
-
throw new TypeError("Use await ProsodyClient.create(config)");
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Creates a Prosody client without blocking the Node.js event loop.
|
|
187
|
-
*
|
|
188
|
-
* @param {Configuration} config - The client configuration.
|
|
189
|
-
* @returns {Promise<ProsodyClient>} The initialized client.
|
|
190
|
-
*/
|
|
191
|
-
static async create(config) {
|
|
192
|
-
const client = Object.create(ProsodyClient.prototype);
|
|
193
|
-
client.nativeClient = await NativeClient.create(config);
|
|
194
|
-
return client;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Gets the source system identifier configured for the client.
|
|
199
|
-
*
|
|
200
|
-
* @returns {string} The source system identifier.
|
|
201
|
-
*/
|
|
202
|
-
get sourceSystem() {
|
|
203
|
-
return this.nativeClient.sourceSystem;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Gets the current state of the consumer.
|
|
208
|
-
*
|
|
209
|
-
* @returns {Promise<ConsumerState>} The current state of the consumer.
|
|
210
|
-
* @throws {Error} If the operation fails.
|
|
211
|
-
*/
|
|
212
|
-
consumerState() {
|
|
213
|
-
return this.nativeClient.consumerState();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Gets the number of partitions assigned to the consumer.
|
|
218
|
-
*
|
|
219
|
-
* @returns {Promise<number>} The number of assigned partitions, or 0 if the consumer is not in the Running state.
|
|
220
|
-
* @throws {Error} If the operation fails.
|
|
221
|
-
*/
|
|
222
|
-
assignedPartitionCount() {
|
|
223
|
-
return this.nativeClient.assignedPartitionCount();
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Checks if the consumer is stalled.
|
|
228
|
-
*
|
|
229
|
-
* @returns {Promise<boolean>} Whether the consumer is stalled, or false if the consumer is not in the Running state.
|
|
230
|
-
* @throws {Error} If the operation fails.
|
|
231
|
-
*/
|
|
232
|
-
isStalled() {
|
|
233
|
-
return this.nativeClient.isStalled();
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Opens a read-only view of another consumer group's published collection.
|
|
238
|
-
* @param {string} subsystem - The publisher's subsystem.
|
|
239
|
-
* @param {Readonly<object>} definition - A JSON value, map, or deque definition.
|
|
240
|
-
* @returns {Promise<PublishedValue|PublishedMap|PublishedDeque>} The reader.
|
|
241
|
-
*/
|
|
242
|
-
async state(subsystem, definition) {
|
|
243
|
-
const access = stateDefinitionAccess.get(definition);
|
|
244
|
-
if (access?.published === undefined) {
|
|
245
|
-
throw new TypeError(
|
|
246
|
-
"definition must be a JSON value, map, or deque definition",
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
const readCache = definition.readCache;
|
|
250
|
-
return access.published(
|
|
251
|
-
this.nativeClient,
|
|
252
|
-
subsystem,
|
|
253
|
-
definition.name,
|
|
254
|
-
readCache && readCache.ttlMs,
|
|
255
|
-
readCache === false,
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Sends a message to a specified topic.
|
|
261
|
-
*
|
|
262
|
-
* @param {string} topic - The topic to send the message to.
|
|
263
|
-
* @param {string} key - The key of the message.
|
|
264
|
-
* @param {*} payload - The payload of the message. Serialized here; Kafka
|
|
265
|
-
* receives the bytes verbatim.
|
|
266
|
-
* @param {AbortSignal} [signal] - Optional abort signal to cancel the send operation. When aborted, the promise will reject with the abort reason.
|
|
267
|
-
* @returns {Promise<void>} A promise that resolves when the message has been successfully sent.
|
|
268
|
-
* @throws {Error} If the send operation fails or is aborted.
|
|
269
|
-
*/
|
|
270
|
-
async send(topic, key, payload, signal) {
|
|
271
|
-
const carrier = {};
|
|
272
|
-
propagation.inject(otelContext.active(), carrier);
|
|
273
|
-
|
|
274
|
-
await this.nativeClient.send(
|
|
275
|
-
topic,
|
|
276
|
-
key,
|
|
277
|
-
toJson(payload, TransientError),
|
|
278
|
-
eventMetadata(payload),
|
|
279
|
-
carrier,
|
|
280
|
-
signal && onAbort(signal),
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Sends an excise record for a key.
|
|
286
|
-
*
|
|
287
|
-
* @param {string} topic - The topic name.
|
|
288
|
-
* @param {string} key - The key to delete from compacted views.
|
|
289
|
-
* @param {AbortSignal} [signal] - An optional abort signal.
|
|
290
|
-
* @returns {Promise<void>} A promise that resolves after Prosody sends the record.
|
|
291
|
-
*/
|
|
292
|
-
async excise(topic, key, signal) {
|
|
293
|
-
const carrier = {};
|
|
294
|
-
propagation.inject(otelContext.active(), carrier);
|
|
295
|
-
await this.nativeClient.excise(
|
|
296
|
-
topic,
|
|
297
|
-
key,
|
|
298
|
-
carrier,
|
|
299
|
-
signal && onAbort(signal),
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Sends a request and returns one outcome for each subsystem.
|
|
305
|
-
*
|
|
306
|
-
* @param {string} topic - The Kafka topic.
|
|
307
|
-
* @param {string} key - The message key.
|
|
308
|
-
* @param {*} payload - The JSON request payload.
|
|
309
|
-
* @param {{subsystems: readonly string[], timeoutMs: number, signal?: AbortSignal}} options - Request policy.
|
|
310
|
-
* @returns {Promise<ReadonlyMap<string, {ok: true, value: *}|{ok: false, error: {kind: "handler"|"timeout"|"formatMismatch"|"malformedResponse", message: string}}>>} One outcome per subsystem.
|
|
311
|
-
* @throws {Error} If the input is invalid, sending fails, shutdown starts, or the signal aborts.
|
|
312
|
-
*/
|
|
313
|
-
async request(topic, key, payload, options) {
|
|
314
|
-
const carrier = {};
|
|
315
|
-
propagation.inject(otelContext.active(), carrier);
|
|
316
|
-
const results = await this.nativeClient.request(
|
|
317
|
-
{
|
|
318
|
-
topic,
|
|
319
|
-
key,
|
|
320
|
-
payload: toJson(payload, TransientError),
|
|
321
|
-
metadata: eventMetadata(payload),
|
|
322
|
-
subsystems: options.subsystems,
|
|
323
|
-
timeoutMs: options.timeoutMs,
|
|
324
|
-
},
|
|
325
|
-
carrier,
|
|
326
|
-
options.signal && onAbort(options.signal),
|
|
327
|
-
);
|
|
328
|
-
return responseOutcomes(results);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** Sends an excise request and returns one outcome for each subsystem. */
|
|
332
|
-
async requestExcise(topic, key, options) {
|
|
333
|
-
const carrier = {};
|
|
334
|
-
propagation.inject(otelContext.active(), carrier);
|
|
335
|
-
const results = await this.nativeClient.requestExcise(
|
|
336
|
-
{
|
|
337
|
-
topic,
|
|
338
|
-
key,
|
|
339
|
-
subsystems: options.subsystems,
|
|
340
|
-
timeoutMs: options.timeoutMs,
|
|
341
|
-
},
|
|
342
|
-
carrier,
|
|
343
|
-
options.signal && onAbort(options.signal),
|
|
344
|
-
);
|
|
345
|
-
return responseOutcomes(results);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Subscribes to receive messages using the provided event handler.
|
|
350
|
-
*
|
|
351
|
-
* @param {EventHandler} eventHandler - The event handler to process received messages and timers.
|
|
352
|
-
* @returns {Promise<void>} A promise that resolves when the subscription is successfully established and the consumer is ready to receive messages.
|
|
353
|
-
* @throws {TypeError} If any required handler method is missing.
|
|
354
|
-
* @throws {Error} If the subscription fails to establish.
|
|
355
|
-
*/
|
|
356
|
-
async subscribe(eventHandler) {
|
|
357
|
-
for (const name of ["onMessage", "onExcise", "onTimer"]) {
|
|
358
|
-
if (typeof eventHandler?.[name] !== "function") {
|
|
359
|
-
throw new TypeError(`EventHandler.${name} must be a function`);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const tracer = trace.getTracer("prosody");
|
|
364
|
-
const { onExcise, onMessage, onTimer } = eventHandler;
|
|
365
|
-
const handleRecord = async (
|
|
366
|
-
nativeContext,
|
|
367
|
-
message,
|
|
368
|
-
carrier,
|
|
369
|
-
handler,
|
|
370
|
-
spanName,
|
|
371
|
-
eventType,
|
|
372
|
-
) => {
|
|
373
|
-
const ctx = propagation.extract(otelContext.active(), carrier);
|
|
374
|
-
return otelContext.with(ctx, () =>
|
|
375
|
-
tracer.startActiveSpan(spanName, async (span) => {
|
|
376
|
-
const controller = new AbortController();
|
|
377
|
-
let completed = false;
|
|
378
|
-
nativeContext.onCancel().then(() => {
|
|
379
|
-
if (!completed) {
|
|
380
|
-
span.setAttribute("cancelled", true);
|
|
381
|
-
controller.abort(new Error(`${eventType} cancelled`));
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
try {
|
|
386
|
-
const result = await handler(
|
|
387
|
-
new Context(nativeContext),
|
|
388
|
-
message,
|
|
389
|
-
controller.signal,
|
|
390
|
-
);
|
|
391
|
-
return toJson(result ?? null, PermanentError);
|
|
392
|
-
} catch (error) {
|
|
393
|
-
const cause = error.cause ?? error;
|
|
394
|
-
getCurrentLogger()?.error(`${eventType} handler error`, cause);
|
|
395
|
-
span.recordException(cause);
|
|
396
|
-
span.setStatus({
|
|
397
|
-
code: SpanStatusCode.ERROR,
|
|
398
|
-
message: cause.message,
|
|
399
|
-
});
|
|
400
|
-
captureException(error, eventType, {
|
|
401
|
-
topic: message.topic,
|
|
402
|
-
partition: message.partition,
|
|
403
|
-
key: message.key,
|
|
404
|
-
offset: message.offset,
|
|
405
|
-
});
|
|
406
|
-
throw error;
|
|
407
|
-
} finally {
|
|
408
|
-
completed = true;
|
|
409
|
-
span.end();
|
|
410
|
-
}
|
|
411
|
-
}),
|
|
412
|
-
);
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
await this.nativeClient.subscribe({
|
|
416
|
-
isPermanent: ([err]) => {
|
|
417
|
-
try {
|
|
418
|
-
return err instanceof EventHandlerError && err.isPermanent;
|
|
419
|
-
} catch {
|
|
420
|
-
return false;
|
|
421
|
-
}
|
|
422
|
-
},
|
|
423
|
-
|
|
424
|
-
onMessage: async (err, [nativeContext, message, carrier]) => {
|
|
425
|
-
if (err) throw err;
|
|
426
|
-
return handleRecord(
|
|
427
|
-
nativeContext,
|
|
428
|
-
withParsedPayload(message),
|
|
429
|
-
carrier,
|
|
430
|
-
onMessage,
|
|
431
|
-
"onMessage",
|
|
432
|
-
"message",
|
|
433
|
-
);
|
|
434
|
-
},
|
|
435
|
-
|
|
436
|
-
onExcise: async (err, [nativeContext, message, carrier]) => {
|
|
437
|
-
if (err) throw err;
|
|
438
|
-
return handleRecord(
|
|
439
|
-
nativeContext,
|
|
440
|
-
message,
|
|
441
|
-
carrier,
|
|
442
|
-
onExcise,
|
|
443
|
-
"onExcise",
|
|
444
|
-
"excise",
|
|
445
|
-
);
|
|
446
|
-
},
|
|
447
|
-
|
|
448
|
-
onTimer: async (err, [nativeContext, timer, carrier]) => {
|
|
449
|
-
if (err) throw err;
|
|
450
|
-
|
|
451
|
-
const ctx = propagation.extract(otelContext.active(), carrier);
|
|
452
|
-
return otelContext.with(ctx, async () => {
|
|
453
|
-
return tracer.startActiveSpan("onTimer", async (span) => {
|
|
454
|
-
const controller = new AbortController();
|
|
455
|
-
let completed = false;
|
|
456
|
-
|
|
457
|
-
// Signal abort when cancellation occurs (before handler completes)
|
|
458
|
-
nativeContext.onCancel().then(() => {
|
|
459
|
-
if (!completed) {
|
|
460
|
-
span.setAttribute("cancelled", true);
|
|
461
|
-
controller.abort(new Error("timer cancelled"));
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
try {
|
|
466
|
-
const context = new Context(nativeContext);
|
|
467
|
-
await onTimer(context, timer, controller.signal);
|
|
468
|
-
return "null";
|
|
469
|
-
} catch (error) {
|
|
470
|
-
getCurrentLogger()?.error(
|
|
471
|
-
"Timer handler error",
|
|
472
|
-
error.cause ?? error,
|
|
473
|
-
);
|
|
474
|
-
const cause = error.cause ?? error;
|
|
475
|
-
span.recordException(cause);
|
|
476
|
-
span.setStatus({
|
|
477
|
-
code: SpanStatusCode.ERROR,
|
|
478
|
-
message: cause.message,
|
|
479
|
-
});
|
|
480
|
-
captureException(error, "timer", {
|
|
481
|
-
key: timer.key,
|
|
482
|
-
time: timer.time,
|
|
483
|
-
});
|
|
484
|
-
throw error;
|
|
485
|
-
} finally {
|
|
486
|
-
completed = true;
|
|
487
|
-
span.end();
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
|
-
});
|
|
491
|
-
},
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* Stops the consumer. You can subscribe again later.
|
|
497
|
-
*
|
|
498
|
-
* @returns {Promise<void>} A promise that resolves when the unsubscribe operation is complete.
|
|
499
|
-
* @throws {Error} If the unsubscribe operation fails.
|
|
500
|
-
*/
|
|
501
|
-
async unsubscribe() {
|
|
502
|
-
await this.nativeClient.unsubscribe();
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* Shuts down the client and all its services.
|
|
507
|
-
* Concurrent and repeated calls await the same shutdown operation.
|
|
508
|
-
*
|
|
509
|
-
* @returns {Promise<void>} A promise that resolves when shutdown is complete.
|
|
510
|
-
* @throws {Error} If shutdown fails.
|
|
511
|
-
*/
|
|
512
|
-
async shutdown() {
|
|
513
|
-
this.shutdownPromise ??= this.nativeClient.shutdown();
|
|
514
|
-
await this.shutdownPromise;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// napi-rs can only surface rejections whose value is an object, function, or
|
|
519
|
-
// symbol — napi_create_reference fails on primitives and the rejection becomes
|
|
520
|
-
// an opaque `InvalidArg: Create Error reference failed`. Coerce primitive
|
|
521
|
-
// reasons into Error instances so the original reason is preserved across the
|
|
522
|
-
// napi boundary.
|
|
523
|
-
const toAbortError = (reason) =>
|
|
524
|
-
reason instanceof Error
|
|
525
|
-
? reason
|
|
526
|
-
: new Error(reason === undefined ? "aborted" : String(reason));
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Creates a promise that rejects when the abort signal is triggered.
|
|
530
|
-
* @param {AbortSignal} signal - The abort signal to monitor.
|
|
531
|
-
* @returns {Promise<never>} A promise that rejects with the abort reason.
|
|
532
|
-
* @private
|
|
533
|
-
*/
|
|
534
|
-
const onAbort = (signal) =>
|
|
535
|
-
new Promise((_, reject) => {
|
|
536
|
-
if (signal.aborted) reject(toAbortError(signal.reason));
|
|
537
|
-
else
|
|
538
|
-
signal.addEventListener(
|
|
539
|
-
"abort",
|
|
540
|
-
() => reject(toAbortError(signal.reason)),
|
|
541
|
-
{ once: true },
|
|
542
|
-
);
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
/**
|
|
546
|
-
* Base class for event handler errors.
|
|
547
|
-
* Provides a common interface for determining if an error is permanent.
|
|
548
|
-
* @extends Error
|
|
549
|
-
*/
|
|
550
|
-
class EventHandlerError extends Error {
|
|
551
|
-
constructor(message) {
|
|
552
|
-
super(message);
|
|
553
|
-
this.name = this.constructor.name;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* Indicates whether the error is permanent and should not be retried.
|
|
558
|
-
* @abstract
|
|
559
|
-
* @returns {boolean} True if permanent, false if transient.
|
|
560
|
-
*/
|
|
561
|
-
get isPermanent() {
|
|
562
|
-
throw new Error("Subclasses must implement isPermanent");
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Represents a transient error that may be resolved by retrying.
|
|
568
|
-
* @extends EventHandlerError
|
|
569
|
-
*/
|
|
570
|
-
class TransientError extends EventHandlerError {
|
|
571
|
-
/**
|
|
572
|
-
* @returns {boolean} Always returns false, indicating the error is not permanent.
|
|
573
|
-
*/
|
|
574
|
-
get isPermanent() {
|
|
575
|
-
return false;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Represents a permanent error that should not be retried.
|
|
581
|
-
* @extends EventHandlerError
|
|
582
|
-
*/
|
|
583
|
-
class PermanentError extends EventHandlerError {
|
|
584
|
-
/**
|
|
585
|
-
* @returns {boolean} Always returns true, indicating the error is permanent.
|
|
586
|
-
*/
|
|
587
|
-
get isPermanent() {
|
|
588
|
-
return true;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function responseOutcome(outcome) {
|
|
593
|
-
if (typeof outcome !== "string") return { ok: false, error: outcome };
|
|
594
|
-
|
|
595
|
-
try {
|
|
596
|
-
return { ok: true, value: JSON.parse(outcome) };
|
|
597
|
-
} catch (cause) {
|
|
598
|
-
return {
|
|
599
|
-
ok: false,
|
|
600
|
-
error: { kind: "malformedResponse", message: cause.message },
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
function responseOutcomes(results) {
|
|
606
|
-
const outcomes = new Map();
|
|
607
|
-
for (const { subsystem, outcome } of results)
|
|
608
|
-
outcomes.set(subsystem, responseOutcome(outcome));
|
|
609
|
-
return outcomes;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/**
|
|
613
|
-
* Represents a transient keyed-state failure that may succeed on a later
|
|
614
|
-
* attempt — a store read/write timeout, AND every caller mistake (a
|
|
615
|
-
* null/unrepresentable write, an item-shape mismatch, an out-of-range index, an
|
|
616
|
-
* invalid scan direction). Caller mistakes are transient on purpose: retrying
|
|
617
|
-
* keeps the failure visible and never discards the message (see the
|
|
618
|
-
* error-classification rule in CLAUDE.md). Thrown by state handles and scan
|
|
619
|
-
* iterators. Because it subclasses {@link TransientError}, rethrowing it from a
|
|
620
|
-
* handler classifies the event transient through the existing error bridge with
|
|
621
|
-
* no bridge change.
|
|
622
|
-
* @extends TransientError
|
|
623
|
-
*/
|
|
624
|
-
class TransientStateError extends TransientError {}
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* Represents a permanent keyed-state failure — one a retry cannot resolve in
|
|
628
|
-
* the running process: an unregistered collection name, a registered-identity
|
|
629
|
-
* mismatch, or a duplicate registration. Caller mistakes (a null/unrepresentable
|
|
630
|
-
* write, an item-shape mismatch, a bad index, an invalid direction) are NOT
|
|
631
|
-
* permanent — they are {@link TransientStateError} so they retry and stay
|
|
632
|
-
* visible rather than discarding the message (see the error-classification rule
|
|
633
|
-
* in CLAUDE.md). A handler may also throw this to declare its own failure
|
|
634
|
-
* permanent. Because it subclasses {@link PermanentError}, rethrowing it from a
|
|
635
|
-
* handler classifies the event permanent through the existing error bridge with
|
|
636
|
-
* no bridge change.
|
|
637
|
-
* @extends PermanentError
|
|
638
|
-
*/
|
|
639
|
-
class PermanentStateError extends PermanentError {}
|
|
640
|
-
|
|
641
|
-
const STATE_ERROR_NAMES = new Set([
|
|
642
|
-
"PermanentStateError",
|
|
643
|
-
"TransientStateError",
|
|
644
|
-
]);
|
|
645
|
-
|
|
646
|
-
/**
|
|
647
|
-
* Checks whether a value is a keyed-state error of either category.
|
|
648
|
-
*
|
|
649
|
-
* Name-branded: it matches on the error's `name`, so it recognizes state errors
|
|
650
|
-
* across both category classes (and across realms/duplicate module copies)
|
|
651
|
-
* without caring which category the error carries. It uses a realm-neutral
|
|
652
|
-
* native-error check so an error minted in another realm (a Node vm context,
|
|
653
|
-
* worker thread, or duplicate module copy) is still recognized — a bare
|
|
654
|
-
* `instanceof Error` would reject those.
|
|
655
|
-
*
|
|
656
|
-
* @param {unknown} error - The value to test.
|
|
657
|
-
* @returns {boolean} True when the value is a keyed-state error.
|
|
658
|
-
*/
|
|
659
|
-
function isStateError(error) {
|
|
660
|
-
return types.isNativeError(error) && STATE_ERROR_NAMES.has(error.name);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* Helper function to create error decorators.
|
|
665
|
-
* @param {Function} ErrorClass - The error class to wrap exceptions with.
|
|
666
|
-
* @returns {Function} A decorator function that wraps specified exceptions.
|
|
667
|
-
* @private
|
|
668
|
-
*/
|
|
669
|
-
function createErrorDecorator(ErrorClass) {
|
|
670
|
-
return function decorator(...exceptionTypes) {
|
|
671
|
-
return function (originalMethod, context) {
|
|
672
|
-
if (context.kind !== "method" && context.kind !== "function") {
|
|
673
|
-
throw new TypeError(
|
|
674
|
-
`@${ErrorClass.name} can only decorate methods or functions`,
|
|
675
|
-
);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function handleError(error) {
|
|
679
|
-
if (exceptionTypes.some((type) => error instanceof type)) {
|
|
680
|
-
const wrapped = new ErrorClass(error.message);
|
|
681
|
-
wrapped.cause = error;
|
|
682
|
-
return wrapped;
|
|
683
|
-
}
|
|
684
|
-
return error;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
if (originalMethod.constructor.name === "AsyncFunction") {
|
|
688
|
-
return async function (...args) {
|
|
689
|
-
try {
|
|
690
|
-
return await originalMethod.apply(this, args);
|
|
691
|
-
} catch (error) {
|
|
692
|
-
throw handleError(error);
|
|
693
|
-
}
|
|
694
|
-
};
|
|
695
|
-
} else {
|
|
696
|
-
return function (...args) {
|
|
697
|
-
try {
|
|
698
|
-
return originalMethod.apply(this, args);
|
|
699
|
-
} catch (error) {
|
|
700
|
-
throw handleError(error);
|
|
701
|
-
}
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
};
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
/**
|
|
709
|
-
* Decorator factory for marking errors as transient.
|
|
710
|
-
* Can be applied to methods to automatically wrap specified error types as transient.
|
|
711
|
-
* @param {...(new(...args: never[]) => Error)} exceptionTypes - The error types to be treated as transient.
|
|
712
|
-
* @returns {Function} A decorator function.
|
|
713
|
-
*/
|
|
714
|
-
const transient = createErrorDecorator(TransientError);
|
|
715
|
-
|
|
716
|
-
/**
|
|
717
|
-
* Decorator factory for marking errors as permanent.
|
|
718
|
-
* Can be applied to methods to automatically wrap specified error types as permanent.
|
|
719
|
-
* @param {...(new(...args: never[]) => Error)} exceptionTypes - The error types to be treated as permanent.
|
|
720
|
-
* @returns {Function} A decorator function.
|
|
721
|
-
*/
|
|
722
|
-
const permanent = createErrorDecorator(PermanentError);
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* Injects the active OpenTelemetry context into a fresh carrier for one native
|
|
726
|
-
* operation. Native glue activates the carrier without recording a binding
|
|
727
|
-
* span, so the core semantic operation span joins the JavaScript trace directly.
|
|
728
|
-
* @returns {Record<string, string>} The populated carrier.
|
|
729
|
-
* @private
|
|
730
|
-
*/
|
|
731
|
-
function injectedCarrier() {
|
|
732
|
-
const carrier = {};
|
|
733
|
-
propagation.inject(otelContext.active(), carrier);
|
|
734
|
-
return carrier;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
/**
|
|
738
|
-
* Converts a category-tagged native state error into the matching typed state
|
|
739
|
-
* error. The category crosses the FFI boundary structurally: the native layer
|
|
740
|
-
* sets the error's `cause` to an error whose message is exactly `"permanent"`
|
|
741
|
-
* or `"transient"` — a data channel distinct from the human-readable message,
|
|
742
|
-
* which is never parsed. Untagged errors (e.g. an argument-conversion
|
|
743
|
-
* TypeError) pass through unchanged.
|
|
744
|
-
*
|
|
745
|
-
* The native-error checks are realm-neutral (`util.types.isNativeError`) rather
|
|
746
|
-
* than `instanceof Error`: a napi error is minted in the addon's realm, so under
|
|
747
|
-
* a Node vm context, worker thread, or duplicate module copy a plain
|
|
748
|
-
* `instanceof Error` would be false and the category tag would be missed. The
|
|
749
|
-
* category is still matched exactly against the closed `"permanent"`/
|
|
750
|
-
* `"transient"` token set on the cause.
|
|
751
|
-
* @param {unknown} error - The error thrown by the native layer.
|
|
752
|
-
* @returns {unknown} The typed state error, or the original error if untagged.
|
|
753
|
-
* @private
|
|
754
|
-
*/
|
|
755
|
-
function toStateError(error) {
|
|
756
|
-
if (!types.isNativeError(error)) return error;
|
|
757
|
-
const cause = error.cause;
|
|
758
|
-
const category = types.isNativeError(cause) ? cause.message : undefined;
|
|
759
|
-
if (category !== "permanent" && category !== "transient") return error;
|
|
760
|
-
const wrapped =
|
|
761
|
-
category === "permanent"
|
|
762
|
-
? new PermanentStateError(error.message)
|
|
763
|
-
: new TransientStateError(error.message);
|
|
764
|
-
wrapped.cause = error;
|
|
765
|
-
return wrapped;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
/**
|
|
769
|
-
* Runs one async native state operation with a fresh carrier, translating a
|
|
770
|
-
* category-tagged failure into the matching typed state error.
|
|
771
|
-
* @param {(carrier: Record<string, string>) => Promise<*>} operation - The native call.
|
|
772
|
-
* @returns {Promise<*>} The operation's resolved value.
|
|
773
|
-
* @private
|
|
774
|
-
*/
|
|
775
|
-
/**
|
|
776
|
-
* Renders a value for a diagnostic message without ever throwing. `JSON.stringify`
|
|
777
|
-
* throws on a BigInt or a cyclic object and a template literal throws on a
|
|
778
|
-
* Symbol, so interpolating a hostile argument directly would raise a raw
|
|
779
|
-
* `TypeError` — bypassing the promise/state-error contract. Falls back to the
|
|
780
|
-
* value's `typeof` when it cannot be serialized.
|
|
781
|
-
* @param {*} value - The value to describe.
|
|
782
|
-
* @returns {string} A safe, human-readable rendering.
|
|
783
|
-
* @private
|
|
784
|
-
*/
|
|
785
|
-
function describeValue(value) {
|
|
786
|
-
try {
|
|
787
|
-
const rendered = JSON.stringify(value);
|
|
788
|
-
return rendered === undefined ? typeof value : rendered;
|
|
789
|
-
} catch {
|
|
790
|
-
return typeof value;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
async function stateOp(operation) {
|
|
795
|
-
try {
|
|
796
|
-
return await operation(injectedCarrier());
|
|
797
|
-
} catch (error) {
|
|
798
|
-
throw toStateError(error);
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
/**
|
|
803
|
-
* Runs one synchronous native call (a handle vend or a scan open), translating
|
|
804
|
-
* a category-tagged failure into the matching typed state error.
|
|
805
|
-
* @param {() => *} operation - The native call.
|
|
806
|
-
* @returns {*} The operation's return value.
|
|
807
|
-
* @private
|
|
808
|
-
*/
|
|
809
|
-
function stateSync(operation) {
|
|
810
|
-
try {
|
|
811
|
-
return operation();
|
|
812
|
-
} catch (error) {
|
|
813
|
-
throw toStateError(error);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
/**
|
|
818
|
-
* Builds a frozen state-collection definition. The definition is the single
|
|
819
|
-
* source of typing: the same frozen object is placed in
|
|
820
|
-
* `Configuration.stateCollections` (so the collection is registered once) and
|
|
821
|
-
* passed to `Context.state()` to vend a typed handle. Validation (name/ttl/
|
|
822
|
-
* keyset rules, duplicate names) is core-owned and happens at client
|
|
823
|
-
* construction and registration — this layer only shapes and freezes.
|
|
824
|
-
* @param {string} name - The collection name.
|
|
825
|
-
* @param {string} kind - `"value"`, `"map"`, or `"deque"`.
|
|
826
|
-
* @param {string} payload - `"json"` or `"message"`.
|
|
827
|
-
* @param {object} [options] - Optional ttlSeconds / readUncommitted, map-only
|
|
828
|
-
* keysetLimit, and deque-only capacity.
|
|
829
|
-
* @returns {Readonly<object>} The frozen definition.
|
|
830
|
-
* @private
|
|
831
|
-
*/
|
|
832
|
-
const stateDefinitionAccess = new WeakMap();
|
|
833
|
-
const VALUE_ACCESS = Object.freeze({
|
|
834
|
-
owned: (context, collection) =>
|
|
835
|
-
new ValueState(context.valueState(collection), jsonItems),
|
|
836
|
-
published: async (client, subsystem, collection, ttl, disabled) =>
|
|
837
|
-
new PublishedValue(
|
|
838
|
-
await client.publishedValue(subsystem, collection, ttl, disabled),
|
|
839
|
-
),
|
|
840
|
-
});
|
|
841
|
-
const MAP_ACCESS = Object.freeze({
|
|
842
|
-
owned: (context, collection) =>
|
|
843
|
-
new MapState(context.mapState(collection), jsonItems),
|
|
844
|
-
published: async (client, subsystem, collection, ttl, disabled) =>
|
|
845
|
-
new PublishedMap(
|
|
846
|
-
await client.publishedMap(subsystem, collection, ttl, disabled),
|
|
847
|
-
),
|
|
848
|
-
});
|
|
849
|
-
const DEQUE_ACCESS = Object.freeze({
|
|
850
|
-
owned: (context, collection) =>
|
|
851
|
-
new DequeState(context.dequeState(collection), jsonItems),
|
|
852
|
-
published: async (client, subsystem, collection, ttl, disabled) =>
|
|
853
|
-
new PublishedDeque(
|
|
854
|
-
await client.publishedDeque(subsystem, collection, ttl, disabled),
|
|
855
|
-
),
|
|
856
|
-
});
|
|
857
|
-
const MESSAGE_VALUE_ACCESS = Object.freeze({
|
|
858
|
-
owned: (context, collection) =>
|
|
859
|
-
new ValueState(context.messageValueState(collection), messageItems),
|
|
860
|
-
});
|
|
861
|
-
const MESSAGE_MAP_ACCESS = Object.freeze({
|
|
862
|
-
owned: (context, collection) =>
|
|
863
|
-
new MapState(context.messageMapState(collection), messageItems),
|
|
864
|
-
});
|
|
865
|
-
const MESSAGE_DEQUE_ACCESS = Object.freeze({
|
|
866
|
-
owned: (context, collection) =>
|
|
867
|
-
new DequeState(context.messageDequeState(collection), messageItems),
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
function stateDefinition(name, kind, payload, access, options = {}) {
|
|
871
|
-
const definition = { name, kind, payload };
|
|
872
|
-
if (options.ttlSeconds !== undefined)
|
|
873
|
-
definition.ttlSeconds = options.ttlSeconds;
|
|
874
|
-
if (options.readUncommitted !== undefined)
|
|
875
|
-
definition.readUncommitted = options.readUncommitted;
|
|
876
|
-
if (options.published !== undefined) definition.published = options.published;
|
|
877
|
-
if (options.readCache !== undefined) definition.readCache = options.readCache;
|
|
878
|
-
if (options.keysetLimit !== undefined)
|
|
879
|
-
definition.keysetLimit = options.keysetLimit;
|
|
880
|
-
if (options.capacity !== undefined) definition.capacity = options.capacity;
|
|
881
|
-
const frozen = Object.freeze(definition);
|
|
882
|
-
stateDefinitionAccess.set(frozen, access);
|
|
883
|
-
return frozen;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
/**
|
|
887
|
-
* Declares a single-value JSON collection. The type parameter annotates the
|
|
888
|
-
* stored value; it is compile-time only — payloads cross as plain JSON with no
|
|
889
|
-
* runtime validation.
|
|
890
|
-
* @param {string} name - The collection name (unique per client).
|
|
891
|
-
* @param {object} [options] - `ttlSeconds` (whole seconds) and `readUncommitted`.
|
|
892
|
-
* @returns {Readonly<object>} A frozen definition for `stateCollections` and `state()`.
|
|
893
|
-
*/
|
|
894
|
-
function value(name, options) {
|
|
895
|
-
return stateDefinition(name, "value", "json", VALUE_ACCESS, options);
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
/**
|
|
899
|
-
* Declares an ordered-map JSON collection. Map keys are always strings; the
|
|
900
|
-
* type parameter annotates the stored value (compile-time only).
|
|
901
|
-
* @param {string} name - The collection name (unique per client).
|
|
902
|
-
* @param {object} [options] - `ttlSeconds`, `readUncommitted`, and map-only `keysetLimit`.
|
|
903
|
-
* @returns {Readonly<object>} A frozen definition for `stateCollections` and `state()`.
|
|
904
|
-
*/
|
|
905
|
-
function map(name, options) {
|
|
906
|
-
return stateDefinition(name, "map", "json", MAP_ACCESS, options);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
/**
|
|
910
|
-
* Declares a double-ended-queue JSON collection. The type parameter annotates
|
|
911
|
-
* the stored element (compile-time only).
|
|
912
|
-
* @param {string} name - The collection name (unique per client).
|
|
913
|
-
* @param {object} [options] - `ttlSeconds` (whole seconds), `readUncommitted`,
|
|
914
|
-
* and deque-only `capacity` (bounded backlog; enforced lazily on push).
|
|
915
|
-
* @returns {Readonly<object>} A frozen definition for `stateCollections` and `state()`.
|
|
916
|
-
*/
|
|
917
|
-
function deque(name, options) {
|
|
918
|
-
return stateDefinition(name, "deque", "json", DEQUE_ACCESS, options);
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
/**
|
|
922
|
-
* Declares a single-value message collection: each stored item is the full
|
|
923
|
-
* Kafka {@link Message} the handler received. The type parameter annotates the
|
|
924
|
-
* message payload (compile-time only).
|
|
925
|
-
* @param {string} name - The collection name (unique per client).
|
|
926
|
-
* @param {object} [options] - `ttlSeconds` (whole seconds) and `readUncommitted`.
|
|
927
|
-
* @returns {Readonly<object>} A frozen definition for `stateCollections` and `state()`.
|
|
928
|
-
*/
|
|
929
|
-
function messageValue(name, options) {
|
|
930
|
-
return stateDefinition(
|
|
931
|
-
name,
|
|
932
|
-
"value",
|
|
933
|
-
"message",
|
|
934
|
-
MESSAGE_VALUE_ACCESS,
|
|
935
|
-
options,
|
|
936
|
-
);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
/**
|
|
940
|
-
* Declares an ordered-map message collection. Map keys are always strings; each
|
|
941
|
-
* stored value is the full Kafka {@link Message}. The type parameter annotates
|
|
942
|
-
* the message payload (compile-time only).
|
|
943
|
-
* @param {string} name - The collection name (unique per client).
|
|
944
|
-
* @param {object} [options] - `ttlSeconds`, `readUncommitted`, and map-only `keysetLimit`.
|
|
945
|
-
* @returns {Readonly<object>} A frozen definition for `stateCollections` and `state()`.
|
|
946
|
-
*/
|
|
947
|
-
function messageMap(name, options) {
|
|
948
|
-
return stateDefinition(name, "map", "message", MESSAGE_MAP_ACCESS, options);
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
/**
|
|
952
|
-
* Declares a double-ended-queue message collection: each stored element is the
|
|
953
|
-
* full Kafka {@link Message}. The type parameter annotates the message payload
|
|
954
|
-
* (compile-time only).
|
|
955
|
-
* @param {string} name - The collection name (unique per client).
|
|
956
|
-
* @param {object} [options] - `ttlSeconds` (whole seconds), `readUncommitted`,
|
|
957
|
-
* and deque-only `capacity` (bounded backlog; enforced lazily on push).
|
|
958
|
-
* @returns {Readonly<object>} A frozen definition for `stateCollections` and `state()`.
|
|
959
|
-
*/
|
|
960
|
-
function messageDeque(name, options) {
|
|
961
|
-
return stateDefinition(
|
|
962
|
-
name,
|
|
963
|
-
"deque",
|
|
964
|
-
"message",
|
|
965
|
-
MESSAGE_DEQUE_ACCESS,
|
|
966
|
-
options,
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
/**
|
|
971
|
-
* Adapts a chunked native scan cursor to the item-oriented JS async-iterator
|
|
972
|
-
* protocol. A fresh carrier is propagated per native chunk without recording
|
|
973
|
-
* a span, while individual `next()` calls drain the retained chunk without
|
|
974
|
-
* crossing N-API.
|
|
975
|
-
* Native `null` (exhausted) maps to `{ done: true }`. Early exit from a `for await` loop
|
|
976
|
-
* (`break`/`return`/`throw`) invokes `return()`, which awaits the native
|
|
977
|
-
* `close()`; exhaustion and a pull error also close the cursor. Once finished,
|
|
978
|
-
* the cursor is never touched again. A pull error is never masked by the
|
|
979
|
-
* cleanup close (that close is best-effort). A close failure on the exhaustion
|
|
980
|
-
* or early-exit path is normalized through `toStateError`, so every keyed-state
|
|
981
|
-
* failure a caller can observe is a `PermanentStateError`/`TransientStateError`.
|
|
982
|
-
*
|
|
983
|
-
* Owned cursors remain attempt-fenced. Published cursors remain valid with
|
|
984
|
-
* their standalone reader.
|
|
985
|
-
* @param {object|(() => Promise<object>)} source - The native scan cursor, or
|
|
986
|
-
* a lazy asynchronous cursor opener.
|
|
987
|
-
* @param {(item: *) => *} transform - Maps each raw item to the yielded value.
|
|
988
|
-
* @returns {AsyncIterableIterator<*>} The async iterator.
|
|
989
|
-
* @private
|
|
990
|
-
*/
|
|
991
|
-
function stateIterator(source, transform) {
|
|
992
|
-
let cursor;
|
|
993
|
-
let finished = false;
|
|
994
|
-
let chunk = [];
|
|
995
|
-
let offset = 0;
|
|
996
|
-
let queue = Promise.resolve();
|
|
997
|
-
const openCursor = async () => {
|
|
998
|
-
if (cursor === undefined) {
|
|
999
|
-
cursor = typeof source === "function" ? await source() : source;
|
|
1000
|
-
}
|
|
1001
|
-
return cursor;
|
|
1002
|
-
};
|
|
1003
|
-
// Serialize the complete iterator protocol, not just native pulls. Without
|
|
1004
|
-
// this queue, concurrent next() continuations can both reset `offset` after
|
|
1005
|
-
// awaiting the same chunk and yield the same first item. return() shares the
|
|
1006
|
-
// queue so it cannot close the cursor underneath an active next().
|
|
1007
|
-
const enqueue = (operation) => {
|
|
1008
|
-
const result = queue.then(operation, operation);
|
|
1009
|
-
// A rejected operation must not poison later cleanup or done checks.
|
|
1010
|
-
queue = result.then(
|
|
1011
|
-
() => undefined,
|
|
1012
|
-
() => undefined,
|
|
1013
|
-
);
|
|
1014
|
-
return result;
|
|
1015
|
-
};
|
|
1016
|
-
// Best-effort close used after a pull or transform failure: it must never
|
|
1017
|
-
// mask the primary error. `try/catch` (not `.catch()`) so a synchronous throw
|
|
1018
|
-
// from `close()` is swallowed too.
|
|
1019
|
-
const closeQuietly = async () => {
|
|
1020
|
-
if (cursor === undefined) return;
|
|
1021
|
-
try {
|
|
1022
|
-
await cursor.close();
|
|
1023
|
-
} catch {
|
|
1024
|
-
/* the primary error is already propagating */
|
|
1025
|
-
}
|
|
1026
|
-
};
|
|
1027
|
-
// Close on clean exhaustion / early exit, where there is no primary error to
|
|
1028
|
-
// mask: a close failure surfaces through the state-error model.
|
|
1029
|
-
const closeOrThrow = async () => {
|
|
1030
|
-
if (cursor === undefined) return;
|
|
1031
|
-
try {
|
|
1032
|
-
await cursor.close();
|
|
1033
|
-
} catch (error) {
|
|
1034
|
-
throw toStateError(error);
|
|
1035
|
-
}
|
|
1036
|
-
};
|
|
1037
|
-
return {
|
|
1038
|
-
next() {
|
|
1039
|
-
return enqueue(async () => {
|
|
1040
|
-
if (finished) return { value: undefined, done: true };
|
|
1041
|
-
while (offset >= chunk.length) {
|
|
1042
|
-
chunk = [];
|
|
1043
|
-
offset = 0;
|
|
1044
|
-
try {
|
|
1045
|
-
chunk = await (await openCursor()).nextChunk(injectedCarrier());
|
|
1046
|
-
} catch (error) {
|
|
1047
|
-
finished = true;
|
|
1048
|
-
await closeQuietly();
|
|
1049
|
-
throw toStateError(error);
|
|
1050
|
-
}
|
|
1051
|
-
if (chunk === null) {
|
|
1052
|
-
chunk = [];
|
|
1053
|
-
finished = true;
|
|
1054
|
-
await closeOrThrow();
|
|
1055
|
-
return { value: undefined, done: true };
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
const item = chunk[offset];
|
|
1059
|
-
// Release consumed values even while the rest of a large chunk remains.
|
|
1060
|
-
chunk[offset] = undefined;
|
|
1061
|
-
offset += 1;
|
|
1062
|
-
try {
|
|
1063
|
-
return { value: transform(item), done: false };
|
|
1064
|
-
} catch (error) {
|
|
1065
|
-
// A transform failure is a binding defect, not a store error; close the
|
|
1066
|
-
// cursor and mark the iterator done rather than leaving it live.
|
|
1067
|
-
finished = true;
|
|
1068
|
-
await closeQuietly();
|
|
1069
|
-
throw error;
|
|
1070
|
-
}
|
|
1071
|
-
});
|
|
1072
|
-
},
|
|
1073
|
-
return(value) {
|
|
1074
|
-
return enqueue(async () => {
|
|
1075
|
-
if (!finished) {
|
|
1076
|
-
finished = true;
|
|
1077
|
-
chunk = [];
|
|
1078
|
-
offset = 0;
|
|
1079
|
-
await closeOrThrow();
|
|
1080
|
-
}
|
|
1081
|
-
return { value, done: true };
|
|
1082
|
-
});
|
|
1083
|
-
},
|
|
1084
|
-
[Symbol.asyncIterator]() {
|
|
1085
|
-
return this;
|
|
1086
|
-
},
|
|
1087
|
-
};
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
/**
|
|
1091
|
-
* Read-only handle over a published single-value collection. It is independent
|
|
1092
|
-
* of subscription and remains valid for the lifetime of its client.
|
|
1093
|
-
*/
|
|
1094
|
-
class PublishedValue {
|
|
1095
|
-
constructor(native) {
|
|
1096
|
-
this.native = native;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
async get(key) {
|
|
1100
|
-
return jsonItems.decode(
|
|
1101
|
-
await stateOp((carrier) => this.native.get(key, carrier)),
|
|
1102
|
-
);
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
class PublishedMap {
|
|
1107
|
-
constructor(native) {
|
|
1108
|
-
this.native = native;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
async get(key, mapKey) {
|
|
1112
|
-
return jsonItems.decode(
|
|
1113
|
-
await stateOp((carrier) => this.native.get(key, mapKey, carrier)),
|
|
1114
|
-
);
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
async getMany(key, mapKeys) {
|
|
1118
|
-
const values = await stateOp((carrier) =>
|
|
1119
|
-
this.native.getMany(key, mapKeys, carrier),
|
|
1120
|
-
);
|
|
1121
|
-
return values.map(jsonItems.decode);
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
has(key, mapKey) {
|
|
1125
|
-
return stateOp((carrier) => this.native.contains(key, mapKey, carrier));
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
entries(key, direction = "forward") {
|
|
1129
|
-
return stateIterator(
|
|
1130
|
-
() => stateOp((carrier) => this.native.scan(key, direction, carrier)),
|
|
1131
|
-
([mapKey, value]) => [mapKey, jsonItems.decode(value)],
|
|
1132
|
-
);
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
keys(key, direction = "forward") {
|
|
1136
|
-
return stateIterator(
|
|
1137
|
-
() => stateOp((carrier) => this.native.keys(key, direction, carrier)),
|
|
1138
|
-
(mapKey) => mapKey,
|
|
1139
|
-
);
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
values(key, direction = "forward") {
|
|
1143
|
-
return stateIterator(
|
|
1144
|
-
() => stateOp((carrier) => this.native.scan(key, direction, carrier)),
|
|
1145
|
-
(entry) => jsonItems.decode(entry[1]),
|
|
1146
|
-
);
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
/**
|
|
1151
|
-
* The native `payload` getter, which answers the raw JSON text read from the
|
|
1152
|
-
* wire. Captured before {@link withParsedPayload} shadows it per instance.
|
|
1153
|
-
* @private
|
|
1154
|
-
*/
|
|
1155
|
-
const rawPayload = Object.getOwnPropertyDescriptor(
|
|
1156
|
-
NativeMessage.prototype,
|
|
1157
|
-
"payload",
|
|
1158
|
-
).get;
|
|
1159
|
-
|
|
1160
|
-
/**
|
|
1161
|
-
* Replaces a message's raw-text `payload` with the parsed document.
|
|
1162
|
-
*
|
|
1163
|
-
* Rust hands the payload across as the bytes it read from the wire, never
|
|
1164
|
-
* parsing them; the parse happens here, on first read, and the result is kept.
|
|
1165
|
-
* A handler that only inspects metadata therefore never pays for one.
|
|
1166
|
-
*
|
|
1167
|
-
* A payload that is not JSON raises a permanent error: no retry makes bytes
|
|
1168
|
-
* parse. Reading `payload` is where that surfaces, since nothing before it
|
|
1169
|
-
* looks at the document. That outcome is kept too, so a handler reading a bad
|
|
1170
|
-
* payload twice re-raises rather than re-parsing.
|
|
1171
|
-
* @param {object} message - The native message.
|
|
1172
|
-
* @returns {object} The same message, with `payload` parsed on demand.
|
|
1173
|
-
* @private
|
|
1174
|
-
*/
|
|
1175
|
-
function withParsedPayload(message) {
|
|
1176
|
-
let document;
|
|
1177
|
-
let failure;
|
|
1178
|
-
let parsed = false;
|
|
1179
|
-
// Not enumerable, matching the five native getters: none of a message's
|
|
1180
|
-
// fields are own properties, so spread, `Object.keys`, and `JSON.stringify`
|
|
1181
|
-
// see none of them. An enumerable own `payload` would make `JSON.stringify`
|
|
1182
|
-
// of a message serialize the whole parsed document.
|
|
1183
|
-
Object.defineProperty(message, "payload", {
|
|
1184
|
-
configurable: true,
|
|
1185
|
-
enumerable: false,
|
|
1186
|
-
get() {
|
|
1187
|
-
if (!parsed) {
|
|
1188
|
-
try {
|
|
1189
|
-
document = parseJson(
|
|
1190
|
-
rawPayload.call(this),
|
|
1191
|
-
PermanentError,
|
|
1192
|
-
"message payload is not JSON",
|
|
1193
|
-
);
|
|
1194
|
-
} catch (error) {
|
|
1195
|
-
failure = error;
|
|
1196
|
-
}
|
|
1197
|
-
parsed = true;
|
|
1198
|
-
}
|
|
1199
|
-
if (failure !== undefined) throw failure;
|
|
1200
|
-
return document;
|
|
1201
|
-
},
|
|
1202
|
-
});
|
|
1203
|
-
return message;
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
/**
|
|
1207
|
-
* Serializes a JSON value.
|
|
1208
|
-
*
|
|
1209
|
-
* It returns `undefined` for a function, a symbol, or `undefined` itself. It
|
|
1210
|
-
* throws for a BigInt or a cycle.
|
|
1211
|
-
* @param {*} value - The value to serialize.
|
|
1212
|
-
* @param {Function} ErrorClass - The error to raise on failure.
|
|
1213
|
-
* @returns {string} The JSON text.
|
|
1214
|
-
* @private
|
|
1215
|
-
*/
|
|
1216
|
-
function toJson(value, ErrorClass) {
|
|
1217
|
-
let json;
|
|
1218
|
-
try {
|
|
1219
|
-
json = JSON.stringify(value);
|
|
1220
|
-
} catch (error) {
|
|
1221
|
-
throw new ErrorClass(
|
|
1222
|
-
`value is not representable as JSON: ${error.message}`,
|
|
1223
|
-
);
|
|
1224
|
-
}
|
|
1225
|
-
if (json === undefined) {
|
|
1226
|
-
throw new ErrorClass(
|
|
1227
|
-
"value is not representable as JSON (functions, symbols, and " +
|
|
1228
|
-
"`undefined` have no JSON form)",
|
|
1229
|
-
);
|
|
1230
|
-
}
|
|
1231
|
-
return json;
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
/** Parses JSON text and maps invalid input onto the caller's error class. */
|
|
1235
|
-
function parseJson(text, ErrorClass, context) {
|
|
1236
|
-
try {
|
|
1237
|
-
return JSON.parse(text);
|
|
1238
|
-
} catch (error) {
|
|
1239
|
-
throw new ErrorClass(`${context}: ${error.message}`);
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
/**
|
|
1244
|
-
* Serializes a value for a JSON collection.
|
|
1245
|
-
*
|
|
1246
|
-
* A collection stores whatever JSON it is given, so the only rejections here
|
|
1247
|
-
* are values with no JSON form at all. Both are caller mistakes and reject
|
|
1248
|
-
* transient: the event retries and the mistake stays visible rather than
|
|
1249
|
-
* discarding the message.
|
|
1250
|
-
*
|
|
1251
|
-
* Everything else follows `JSON.stringify`, which is now the serializer of
|
|
1252
|
-
* record. Inside a container a function-valued property is dropped and an
|
|
1253
|
-
* `undefined` element becomes `null`. `NaN` and the infinities become `null`
|
|
1254
|
-
* anywhere they appear. A `toJSON` method decides what its value serializes to.
|
|
1255
|
-
* The previous Rust-side conversion rejected each of those instead.
|
|
1256
|
-
* @param {*} value - The value to store.
|
|
1257
|
-
* @returns {string} The document's JSON text.
|
|
1258
|
-
* @throws {TransientStateError} If the value has no JSON representation.
|
|
1259
|
-
* @private
|
|
1260
|
-
*/
|
|
1261
|
-
function encodeJson(value) {
|
|
1262
|
-
if (value instanceof NativeMessage) {
|
|
1263
|
-
throw new TransientStateError(
|
|
1264
|
-
"a Kafka message cannot be stored in a JSON collection; declare it with " +
|
|
1265
|
-
"messageValue/messageMap/messageDeque instead",
|
|
1266
|
-
);
|
|
1267
|
-
}
|
|
1268
|
-
return toJson(value, TransientStateError);
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
/**
|
|
1272
|
-
* Reads the event metadata a payload carries.
|
|
1273
|
-
*
|
|
1274
|
-
* When the payload is an object with a string `id` or `type`, prosody uses
|
|
1275
|
-
* them. Anything else carries no metadata. Each field is read once, so a getter
|
|
1276
|
-
* that answers differently on a second read cannot make the metadata disagree
|
|
1277
|
-
* with itself.
|
|
1278
|
-
* @param {*} payload - The payload about to be sent.
|
|
1279
|
-
* @returns {{eventId?: string, eventType?: string}} The metadata.
|
|
1280
|
-
* @private
|
|
1281
|
-
*/
|
|
1282
|
-
function eventMetadata(payload) {
|
|
1283
|
-
const id = payload?.id;
|
|
1284
|
-
const type = payload?.type;
|
|
1285
|
-
return {
|
|
1286
|
-
eventId: typeof id === "string" ? id : undefined,
|
|
1287
|
-
eventType: typeof type === "string" ? type : undefined,
|
|
1288
|
-
};
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
/**
|
|
1292
|
-
* Checks that a message collection is being handed an actual message.
|
|
1293
|
-
*
|
|
1294
|
-
* A message collection stores the Kafka message itself, so it accepts only a
|
|
1295
|
-
* `Message` — an object merely shaped like one is rejected. A message read back
|
|
1296
|
-
* out of a collection is a `Message` and stores fine.
|
|
1297
|
-
* What it stores is the message's own wire bytes, so assigning to a message's
|
|
1298
|
-
* fields, or mutating its parsed `payload`, does not change what is written.
|
|
1299
|
-
* @param {*} value - The value to store.
|
|
1300
|
-
* @returns {object} The message.
|
|
1301
|
-
* @throws {TransientStateError} If the value is not a Kafka message.
|
|
1302
|
-
* @private
|
|
1303
|
-
*/
|
|
1304
|
-
function requireMessage(value) {
|
|
1305
|
-
if (!(value instanceof NativeMessage)) {
|
|
1306
|
-
throw new TransientStateError(
|
|
1307
|
-
"expected a Kafka message; a JSON value cannot be stored in a message collection",
|
|
1308
|
-
);
|
|
1309
|
-
}
|
|
1310
|
-
return value;
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
/**
|
|
1314
|
-
* Builds the item codec for one collection payload type.
|
|
1315
|
-
*
|
|
1316
|
-
* The native handle has one payload type. Each write method accepts only that
|
|
1317
|
-
* type.
|
|
1318
|
-
* @param {(item: *) => *} encode - Prepares an item for a write.
|
|
1319
|
-
* @param {(item: *) => *} decode - Turns a read item into what the caller asked for.
|
|
1320
|
-
* @returns {Readonly<object>} The frozen codec.
|
|
1321
|
-
* @private
|
|
1322
|
-
*/
|
|
1323
|
-
function itemCodec(encode, decode) {
|
|
1324
|
-
return Object.freeze({
|
|
1325
|
-
decode: (item) => (item === null ? null : decode(item)),
|
|
1326
|
-
set: (native, item, carrier) => native.set(encode(item), carrier),
|
|
1327
|
-
setKey: (native, key, item, carrier) =>
|
|
1328
|
-
native.set(key, encode(item), carrier),
|
|
1329
|
-
pushBack: (native, item, carrier) => native.pushBack(encode(item), carrier),
|
|
1330
|
-
pushFront: (native, item, carrier) =>
|
|
1331
|
-
native.pushFront(encode(item), carrier),
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
class PublishedDeque {
|
|
1336
|
-
constructor(native) {
|
|
1337
|
-
this.native = native;
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
length(key) {
|
|
1341
|
-
return stateOp((carrier) => this.native.length(key, carrier));
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
isEmpty(key) {
|
|
1345
|
-
return stateOp((carrier) => this.native.isEmpty(key, carrier));
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
async at(key, index) {
|
|
1349
|
-
if (!Number.isSafeInteger(index)) {
|
|
1350
|
-
throw new TransientStateError(
|
|
1351
|
-
`at: index must be a safe integer, got ${describeValue(index)}`,
|
|
1352
|
-
);
|
|
1353
|
-
}
|
|
1354
|
-
if (index === 0) {
|
|
1355
|
-
return jsonItems.decode(
|
|
1356
|
-
await stateOp((carrier) => this.native.peekFront(key, carrier)),
|
|
1357
|
-
);
|
|
1358
|
-
}
|
|
1359
|
-
if (index === -1) {
|
|
1360
|
-
return jsonItems.decode(
|
|
1361
|
-
await stateOp((carrier) => this.native.peekBack(key, carrier)),
|
|
1362
|
-
);
|
|
1363
|
-
}
|
|
1364
|
-
let position = index;
|
|
1365
|
-
if (position < 0) {
|
|
1366
|
-
position += await this.length(key);
|
|
1367
|
-
if (position < 0) return null;
|
|
1368
|
-
}
|
|
1369
|
-
if (position > 0xffffffff) return null;
|
|
1370
|
-
return jsonItems.decode(
|
|
1371
|
-
await stateOp((carrier) => this.native.get(key, position, carrier)),
|
|
1372
|
-
);
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
values(key, direction = "forward") {
|
|
1376
|
-
return stateIterator(
|
|
1377
|
-
() => stateOp((carrier) => this.native.scan(key, direction, carrier)),
|
|
1378
|
-
jsonItems.decode,
|
|
1379
|
-
);
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
/** JSON documents cross as their text. @private */
|
|
1384
|
-
const jsonItems = itemCodec(encodeJson, (text) =>
|
|
1385
|
-
parseJson(
|
|
1386
|
-
text,
|
|
1387
|
-
PermanentStateError,
|
|
1388
|
-
"stored JSON document could not be parsed",
|
|
1389
|
-
),
|
|
1390
|
-
);
|
|
1391
|
-
|
|
1392
|
-
/** Kafka messages cross as the `Message` object itself. @private */
|
|
1393
|
-
const messageItems = itemCodec(requireMessage, withParsedPayload);
|
|
1394
|
-
|
|
1395
|
-
/**
|
|
1396
|
-
* What every keyed-state handle shares: the native handle it wraps, the item
|
|
1397
|
-
* codec for its payload flavour, and the two transaction verbs.
|
|
1398
|
-
*
|
|
1399
|
-
* Core records one semantic span per operation; this binding propagates context
|
|
1400
|
-
* without adding an N-API span. Handles are valid only within the handler
|
|
1401
|
-
* invocation (attempt) that vended them.
|
|
1402
|
-
*/
|
|
1403
|
-
class StateHandle {
|
|
1404
|
-
/**
|
|
1405
|
-
* @param {object} native - The vended native handle.
|
|
1406
|
-
* @param {object} items - The item codec for the collection's payload flavour.
|
|
1407
|
-
*/
|
|
1408
|
-
constructor(native, items) {
|
|
1409
|
-
this.native = native;
|
|
1410
|
-
this.items = items;
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
/**
|
|
1414
|
-
* Durably commits the buffered operations mid-handler (at-least-once; the
|
|
1415
|
-
* committed floor survives a later rollback or a failed event).
|
|
1416
|
-
* @returns {Promise<void>} Resolves with no value — the erased seam drops the outcome.
|
|
1417
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized commit failure.
|
|
1418
|
-
*/
|
|
1419
|
-
commit() {
|
|
1420
|
-
return stateOp((carrier) => this.native.commit(carrier));
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
/**
|
|
1424
|
-
* Discards buffered uncommitted operations back to the last committed floor.
|
|
1425
|
-
* @returns {Promise<void>} Resolves with no value.
|
|
1426
|
-
*/
|
|
1427
|
-
rollback() {
|
|
1428
|
-
return stateOp((carrier) => this.native.rollback(carrier));
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
/**
|
|
1433
|
-
* Typed handle over a single-value keyed-state collection, vended by
|
|
1434
|
-
* {@link Context#state}. Core records one semantic span per operation; this
|
|
1435
|
-
* binding propagates context without adding an N-API span. Handles are valid
|
|
1436
|
-
* only within the handler invocation (attempt) that vended them.
|
|
1437
|
-
*/
|
|
1438
|
-
class ValueState extends StateHandle {
|
|
1439
|
-
/**
|
|
1440
|
-
* Reads the current value.
|
|
1441
|
-
* @returns {Promise<*|null>} The stored value, or null when absent/cleared.
|
|
1442
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1443
|
-
*/
|
|
1444
|
-
get() {
|
|
1445
|
-
return stateOp((carrier) => this.native.get(carrier)).then(
|
|
1446
|
-
this.items.decode,
|
|
1447
|
-
);
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
/**
|
|
1451
|
-
* Buffers a write of the value. Writing JSON `null` (or an unrepresentable
|
|
1452
|
-
* value) is a caller mistake, rejected with a {@link TransientStateError}
|
|
1453
|
-
* naming `clear()` — use {@link ValueState#clear} to delete instead. The
|
|
1454
|
-
* error is transient so it retries and stays visible rather than discarding
|
|
1455
|
-
* the message and losing data.
|
|
1456
|
-
* @param {*} value - The value to store.
|
|
1457
|
-
* @returns {Promise<void>}
|
|
1458
|
-
* @throws {TransientStateError} On a null/unrepresentable/shape mistake or a
|
|
1459
|
-
* transient store failure; {@link PermanentStateError} only if the store
|
|
1460
|
-
* reports one.
|
|
1461
|
-
*/
|
|
1462
|
-
set(value) {
|
|
1463
|
-
return stateOp((carrier) => this.items.set(this.native, value, carrier));
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
/**
|
|
1467
|
-
* Deletes the stored value.
|
|
1468
|
-
* @returns {Promise<void>}
|
|
1469
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1470
|
-
*/
|
|
1471
|
-
clear() {
|
|
1472
|
-
return stateOp((carrier) => this.native.clear(carrier));
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
/**
|
|
1477
|
-
* Typed handle over an ordered-map keyed-state collection, vended by
|
|
1478
|
-
* {@link Context#state}. Map keys are always strings. Core records one semantic
|
|
1479
|
-
* span per operation; this binding propagates context without adding an N-API
|
|
1480
|
-
* span. Handles and iterators are valid only within the handler invocation.
|
|
1481
|
-
*/
|
|
1482
|
-
class MapState extends StateHandle {
|
|
1483
|
-
/**
|
|
1484
|
-
* Reads the value for `key`.
|
|
1485
|
-
* @param {string} key - The map key.
|
|
1486
|
-
* @returns {Promise<*|null>} The value, or null when the key is absent.
|
|
1487
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1488
|
-
*/
|
|
1489
|
-
get(key) {
|
|
1490
|
-
return stateOp((carrier) => this.native.get(key, carrier)).then(
|
|
1491
|
-
this.items.decode,
|
|
1492
|
-
);
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
/**
|
|
1496
|
-
* Reads several keys in a single call. Returns an array with one entry per
|
|
1497
|
-
* key, in the same order you asked, so `result[i]` is the value for
|
|
1498
|
-
* `keys[i]`. A key that isn't there comes back as `null`, and a key you list
|
|
1499
|
-
* more than once is answered at each spot. The whole read happens as one
|
|
1500
|
-
* step, so no other change to this event's state can slip in partway through.
|
|
1501
|
-
* @param {string[]} keys - The keys to read, in order.
|
|
1502
|
-
* @returns {Promise<Array<*|null>>} One entry per key, in the order asked.
|
|
1503
|
-
* @throws {PermanentStateError|TransientStateError} If the read fails.
|
|
1504
|
-
*/
|
|
1505
|
-
getMany(keys) {
|
|
1506
|
-
return stateOp((carrier) => this.native.getMany(keys, carrier)).then(
|
|
1507
|
-
(items) => items.map(this.items.decode),
|
|
1508
|
-
);
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
/**
|
|
1512
|
-
* Reports whether `key` currently has a stored value. A presence check that
|
|
1513
|
-
* skips the value decode and the resolver: for a message-backed map it
|
|
1514
|
-
* answers with zero Kafka fetches and can report `true` for a message that
|
|
1515
|
-
* can no longer be fetched. Not zero-I/O — a cache miss can still reach the
|
|
1516
|
-
* store — but cheaper than {@link MapState#get} when you only need presence.
|
|
1517
|
-
* @param {string} key - The map key.
|
|
1518
|
-
* @returns {Promise<boolean>} True when the key is present.
|
|
1519
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1520
|
-
*/
|
|
1521
|
-
has(key) {
|
|
1522
|
-
return stateOp((carrier) => this.native.contains(key, carrier));
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
/**
|
|
1526
|
-
* Inserts or overwrites `key`. Writing JSON `null` (or an unrepresentable
|
|
1527
|
-
* value) is a caller mistake, rejected with a {@link TransientStateError} —
|
|
1528
|
-
* use {@link MapState#delete} to remove an entry instead. The error is
|
|
1529
|
-
* transient so it retries and stays visible rather than discarding the
|
|
1530
|
-
* message and losing data.
|
|
1531
|
-
* @param {string} key - The map key.
|
|
1532
|
-
* @param {*} value - The value to store.
|
|
1533
|
-
* @returns {Promise<void>}
|
|
1534
|
-
* @throws {TransientStateError} On a null/unrepresentable/shape mistake or a
|
|
1535
|
-
* transient store failure; {@link PermanentStateError} only if the store
|
|
1536
|
-
* reports one.
|
|
1537
|
-
*/
|
|
1538
|
-
set(key, value) {
|
|
1539
|
-
return stateOp((carrier) =>
|
|
1540
|
-
this.items.setKey(this.native, key, value, carrier),
|
|
1541
|
-
);
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
/**
|
|
1545
|
-
* Removes `key`.
|
|
1546
|
-
*
|
|
1547
|
-
* Deliberate divergence from `Map#delete`: this returns void, NOT a boolean
|
|
1548
|
-
* "was present" flag — surfacing that boolean would force a hidden read on
|
|
1549
|
-
* every delete. The underlying native operation is named `remove`, which is
|
|
1550
|
-
* the verb core's null-write rejection message uses.
|
|
1551
|
-
* @param {string} key - The map key.
|
|
1552
|
-
* @returns {Promise<void>}
|
|
1553
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1554
|
-
*/
|
|
1555
|
-
delete(key) {
|
|
1556
|
-
return stateOp((carrier) => this.native.remove(key, carrier));
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
/**
|
|
1560
|
-
* Removes every entry.
|
|
1561
|
-
* @returns {Promise<void>}
|
|
1562
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1563
|
-
*/
|
|
1564
|
-
clear() {
|
|
1565
|
-
return stateOp((carrier) => this.native.clear(carrier));
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
/**
|
|
1569
|
-
* Opens an async iterator over the live entries in key order. Each yielded
|
|
1570
|
-
* item is a `[key, value]` pair. Valid only within the handler invocation
|
|
1571
|
-
* (attempt) that opened it; early exit closes the underlying cursor.
|
|
1572
|
-
* @param {"forward"|"backward"} [direction="forward"] - The scan direction.
|
|
1573
|
-
* @returns {AsyncIterableIterator<[string, *]>} The entries iterator.
|
|
1574
|
-
* @throws {TransientStateError} If the direction token is invalid (a caller
|
|
1575
|
-
* mistake — retries, not discarded).
|
|
1576
|
-
*/
|
|
1577
|
-
entries(direction = "forward") {
|
|
1578
|
-
return stateIterator(
|
|
1579
|
-
stateSync(() => this.native.scan(direction, injectedCarrier())),
|
|
1580
|
-
([key, item]) => [key, this.items.decode(item)],
|
|
1581
|
-
);
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
/**
|
|
1585
|
-
* Opens an async iterator over the live keys in key order. Skips the value
|
|
1586
|
-
* decode and the resolver, so a message-backed map enumerates keys with zero
|
|
1587
|
-
* Kafka fetches; it still reads presence, so it is not zero-I/O. Valid only
|
|
1588
|
-
* within the handler invocation (attempt) that opened it; early exit closes
|
|
1589
|
-
* the underlying cursor.
|
|
1590
|
-
* @param {"forward"|"backward"} [direction="forward"] - The scan direction.
|
|
1591
|
-
* @returns {AsyncIterableIterator<string>} The keys iterator.
|
|
1592
|
-
* @throws {TransientStateError} If the direction token is invalid (a caller
|
|
1593
|
-
* mistake — retries, not discarded).
|
|
1594
|
-
*/
|
|
1595
|
-
keys(direction = "forward") {
|
|
1596
|
-
return stateIterator(
|
|
1597
|
-
stateSync(() => this.native.keys(direction, injectedCarrier())),
|
|
1598
|
-
(key) => key,
|
|
1599
|
-
);
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
/**
|
|
1603
|
-
* Opens an async iterator over the live values in key order. Valid only
|
|
1604
|
-
* within the handler invocation (attempt) that opened it; early exit closes
|
|
1605
|
-
* the underlying cursor.
|
|
1606
|
-
* @param {"forward"|"backward"} [direction="forward"] - The scan direction.
|
|
1607
|
-
* @returns {AsyncIterableIterator<*>} The values iterator.
|
|
1608
|
-
* @throws {TransientStateError} If the direction token is invalid (a caller
|
|
1609
|
-
* mistake — retries, not discarded).
|
|
1610
|
-
*/
|
|
1611
|
-
values(direction = "forward") {
|
|
1612
|
-
return stateIterator(
|
|
1613
|
-
stateSync(() => this.native.scan(direction, injectedCarrier())),
|
|
1614
|
-
([, item]) => this.items.decode(item),
|
|
1615
|
-
);
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
/**
|
|
1619
|
-
* Forward iteration over `[key, value]` entries — equivalent to
|
|
1620
|
-
* `entries("forward")`. Valid only within the handler invocation (attempt).
|
|
1621
|
-
* @returns {AsyncIterableIterator<[string, *]>} The entries iterator.
|
|
1622
|
-
*/
|
|
1623
|
-
[Symbol.asyncIterator]() {
|
|
1624
|
-
return this.entries();
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
/**
|
|
1629
|
-
* Typed handle over a double-ended-queue keyed-state collection, vended by
|
|
1630
|
-
* {@link Context#state}. Core records one semantic span per operation; this
|
|
1631
|
-
* binding propagates context without adding an N-API span. Handles and
|
|
1632
|
-
* iterators are valid only within the handler invocation that vended them.
|
|
1633
|
-
*/
|
|
1634
|
-
class DequeState extends StateHandle {
|
|
1635
|
-
/**
|
|
1636
|
-
* Appends an element at the back. Writing JSON `null` is a caller mistake
|
|
1637
|
-
* rejected with a {@link TransientStateError}, so an uncaught handler bug
|
|
1638
|
-
* retries instead of committing the offset and losing the message.
|
|
1639
|
-
* @param {*} item - The element to append.
|
|
1640
|
-
* @returns {Promise<void>}
|
|
1641
|
-
* @throws {TransientStateError} If `item` is JSON `null` or otherwise invalid.
|
|
1642
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1643
|
-
*/
|
|
1644
|
-
push(item) {
|
|
1645
|
-
return stateOp((carrier) =>
|
|
1646
|
-
this.items.pushBack(this.native, item, carrier),
|
|
1647
|
-
);
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
/**
|
|
1651
|
-
* Prepends an element at the front. Writing JSON `null` is a caller mistake
|
|
1652
|
-
* rejected with a {@link TransientStateError}, so an uncaught handler bug
|
|
1653
|
-
* retries instead of committing the offset and losing the message.
|
|
1654
|
-
* @param {*} item - The element to prepend.
|
|
1655
|
-
* @returns {Promise<void>}
|
|
1656
|
-
* @throws {TransientStateError} If `item` is JSON `null` or otherwise invalid.
|
|
1657
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1658
|
-
*/
|
|
1659
|
-
unshift(item) {
|
|
1660
|
-
return stateOp((carrier) =>
|
|
1661
|
-
this.items.pushFront(this.native, item, carrier),
|
|
1662
|
-
);
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
/**
|
|
1666
|
-
* Removes and returns the back element.
|
|
1667
|
-
* @returns {Promise<*|null>} The removed element, or null when empty.
|
|
1668
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1669
|
-
*/
|
|
1670
|
-
pop() {
|
|
1671
|
-
return stateOp((carrier) => this.native.popBack(carrier)).then(
|
|
1672
|
-
this.items.decode,
|
|
1673
|
-
);
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
/**
|
|
1677
|
-
* Removes and returns the front element.
|
|
1678
|
-
* @returns {Promise<*|null>} The removed element, or null when empty.
|
|
1679
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1680
|
-
*/
|
|
1681
|
-
shift() {
|
|
1682
|
-
return stateOp((carrier) => this.native.popFront(carrier)).then(
|
|
1683
|
-
this.items.decode,
|
|
1684
|
-
);
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
/**
|
|
1688
|
-
* Returns the number of live elements.
|
|
1689
|
-
* @returns {Promise<number>} The element count.
|
|
1690
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1691
|
-
*/
|
|
1692
|
-
length() {
|
|
1693
|
-
return stateOp((carrier) => this.native.len(carrier));
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
/**
|
|
1697
|
-
* Reports whether the deque holds no live elements.
|
|
1698
|
-
* @returns {Promise<boolean>} True when the deque is empty.
|
|
1699
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1700
|
-
*/
|
|
1701
|
-
isEmpty() {
|
|
1702
|
-
return stateOp((carrier) => this.native.isEmpty(carrier));
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
/**
|
|
1706
|
-
* Removes every element.
|
|
1707
|
-
* @returns {Promise<void>}
|
|
1708
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1709
|
-
*/
|
|
1710
|
-
clear() {
|
|
1711
|
-
return stateOp((carrier) => this.native.clear(carrier));
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
/**
|
|
1715
|
-
* Reads the element at `index`, like `Array.prototype.at`. A non-negative
|
|
1716
|
-
* `index` counts from the front (`0` is the front element); a negative
|
|
1717
|
-
* `index` counts back from the end (`-1` is the back element). Any in-range
|
|
1718
|
-
* position resolves to its element; any out-of-range position — including
|
|
1719
|
-
* every index on an empty deque — resolves to null, the same absence sentinel
|
|
1720
|
-
* `pop`/`shift` use.
|
|
1721
|
-
*
|
|
1722
|
-
* `index` must be a safe integer; a fractional, `NaN`, or infinite value is a
|
|
1723
|
-
* caller mistake, rejected with a {@link TransientStateError} so it retries
|
|
1724
|
-
* and stays visible rather than silently reading the wrong element (a bare
|
|
1725
|
-
* `u32` conversion would truncate `1.5` to `1`). The error is transient — a
|
|
1726
|
-
* caller mistake never discards the message.
|
|
1727
|
-
*
|
|
1728
|
-
* The endpoints `at(0)` and `at(-1)` ride the front/back peeks — a single
|
|
1729
|
-
* read, the same core primitive the other clients use; `at(-1)` makes no
|
|
1730
|
-
* length read. Any other negative index is resolved against the current
|
|
1731
|
-
* {@link DequeState#length}, so it makes two boundary crossings (a length
|
|
1732
|
-
* read, then the element read); any other non-negative index makes one.
|
|
1733
|
-
* Within a handler attempt the deque has a single owner, so nothing else
|
|
1734
|
-
* mutates it between the two reads.
|
|
1735
|
-
* @param {number} index - The position: front-relative if `>= 0`, else back-relative.
|
|
1736
|
-
* @returns {Promise<*|null>} The element, or null when the position is out of range.
|
|
1737
|
-
* @throws {TransientStateError} On an index mistake or a transient store
|
|
1738
|
-
* failure; {@link PermanentStateError} only if the store reports one.
|
|
1739
|
-
*/
|
|
1740
|
-
async at(index) {
|
|
1741
|
-
if (!Number.isSafeInteger(index)) {
|
|
1742
|
-
throw new TransientStateError(
|
|
1743
|
-
`at: index must be a safe integer, got ${describeValue(index)}`,
|
|
1744
|
-
);
|
|
1745
|
-
}
|
|
1746
|
-
if (index === 0)
|
|
1747
|
-
return stateOp((carrier) => this.native.peekFront(carrier)).then(
|
|
1748
|
-
this.items.decode,
|
|
1749
|
-
);
|
|
1750
|
-
if (index === -1)
|
|
1751
|
-
return stateOp((carrier) => this.native.peekBack(carrier)).then(
|
|
1752
|
-
this.items.decode,
|
|
1753
|
-
);
|
|
1754
|
-
let position = index;
|
|
1755
|
-
if (position < 0) {
|
|
1756
|
-
position += await this.length();
|
|
1757
|
-
// Still negative: the deque is shorter than |index|, so nothing is there.
|
|
1758
|
-
if (position < 0) return null;
|
|
1759
|
-
}
|
|
1760
|
-
// Beyond the addressable u32 range can only be past the end, never a wrap.
|
|
1761
|
-
if (position > 0xffffffff) return null;
|
|
1762
|
-
return stateOp((carrier) => this.native.get(position, carrier)).then(
|
|
1763
|
-
this.items.decode,
|
|
1764
|
-
);
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
/**
|
|
1768
|
-
* Opens an async iterator over the live elements in index order. Valid only
|
|
1769
|
-
* within the handler invocation (attempt) that opened it; early exit closes
|
|
1770
|
-
* the underlying cursor.
|
|
1771
|
-
* @param {"forward"|"backward"} [direction="forward"] - The scan direction.
|
|
1772
|
-
* @returns {AsyncIterableIterator<*>} The values iterator.
|
|
1773
|
-
* @throws {TransientStateError} If the direction token is invalid (a caller
|
|
1774
|
-
* mistake — retries, not discarded).
|
|
1775
|
-
*/
|
|
1776
|
-
values(direction = "forward") {
|
|
1777
|
-
return stateIterator(
|
|
1778
|
-
stateSync(() => this.native.scan(direction, injectedCarrier())),
|
|
1779
|
-
(item) => this.items.decode(item),
|
|
1780
|
-
);
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
/**
|
|
1784
|
-
* Forward iteration over the elements — equivalent to `values("forward")`.
|
|
1785
|
-
* Valid only within the handler invocation (attempt).
|
|
1786
|
-
* @returns {AsyncIterableIterator<*>} The values iterator.
|
|
1787
|
-
*/
|
|
1788
|
-
[Symbol.asyncIterator]() {
|
|
1789
|
-
return this.values();
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
/**
|
|
1794
|
-
* Context class that automatically injects OpenTelemetry context for all operations.
|
|
1795
|
-
* This wraps the native Context with automatic OTEL context propagation.
|
|
1796
|
-
*/
|
|
1797
|
-
class Context {
|
|
1798
|
-
constructor(nativeContext) {
|
|
1799
|
-
this.nativeContext = nativeContext;
|
|
1800
|
-
// Cache of vended state wrappers, keyed by collection name (names are
|
|
1801
|
-
// unique per registration), so repeated state(def) calls within one event
|
|
1802
|
-
// return the same handle.
|
|
1803
|
-
this.stateHandles = new Map();
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
/**
|
|
1807
|
-
* Checks whether cancellation has been signaled.
|
|
1808
|
-
* Cancellation includes both message-level cancellation (e.g., timeout) and partition shutdown.
|
|
1809
|
-
* @returns {boolean} True if cancellation was requested, otherwise false.
|
|
1810
|
-
*/
|
|
1811
|
-
get shouldCancel() {
|
|
1812
|
-
return this.nativeContext.shouldCancel;
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
/**
|
|
1816
|
-
* Waits for a cancellation signal.
|
|
1817
|
-
* Cancellation includes both message-level cancellation (e.g., timeout) and partition shutdown.
|
|
1818
|
-
* @returns {Promise<void>} A promise that resolves when cancellation is signaled.
|
|
1819
|
-
*/
|
|
1820
|
-
async onCancel() {
|
|
1821
|
-
return this.nativeContext.onCancel();
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
/**
|
|
1825
|
-
* Schedule a timer at the given time.
|
|
1826
|
-
* @param {Date} time - The UTC timestamp to schedule.
|
|
1827
|
-
* @returns {Promise<void>} A promise that resolves when the timer has been scheduled.
|
|
1828
|
-
* @throws {Error} If time conversion or scheduling fails.
|
|
1829
|
-
*/
|
|
1830
|
-
async schedule(time) {
|
|
1831
|
-
return this.nativeContext.schedule(time, injectedCarrier());
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
/**
|
|
1835
|
-
* Clear existing timers and schedule a new one at the given time.
|
|
1836
|
-
* @param {Date} time - The UTC timestamp to schedule.
|
|
1837
|
-
* @returns {Promise<void>} A promise that resolves when the timer has been scheduled.
|
|
1838
|
-
* @throws {Error} If time conversion or scheduling fails.
|
|
1839
|
-
*/
|
|
1840
|
-
async clearAndSchedule(time) {
|
|
1841
|
-
return this.nativeContext.clearAndSchedule(time, injectedCarrier());
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
/**
|
|
1845
|
-
* Unschedules the timer for the specified time.
|
|
1846
|
-
* @param {Date} time - The time to unschedule.
|
|
1847
|
-
* @returns {Promise<void>} A promise that resolves when the timer has been unscheduled.
|
|
1848
|
-
* @throws {Error} If unscheduling fails.
|
|
1849
|
-
*/
|
|
1850
|
-
async unschedule(time) {
|
|
1851
|
-
return this.nativeContext.unschedule(time, injectedCarrier());
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
/**
|
|
1855
|
-
* Clears all scheduled timers.
|
|
1856
|
-
* @returns {Promise<void>} A promise that resolves when all timers have been cleared.
|
|
1857
|
-
* @throws {Error} If clearing schedules fails.
|
|
1858
|
-
*/
|
|
1859
|
-
async clearScheduled() {
|
|
1860
|
-
return this.nativeContext.clearScheduled(injectedCarrier());
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
/**
|
|
1864
|
-
* Retrieves all scheduled times.
|
|
1865
|
-
* @returns {Promise<Date[]>} An array of scheduled times as Date objects.
|
|
1866
|
-
* @throws {Error} If retrieval fails.
|
|
1867
|
-
*/
|
|
1868
|
-
async scheduled() {
|
|
1869
|
-
return this.nativeContext.scheduled(injectedCarrier());
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
/**
|
|
1873
|
-
* Binds a registered keyed-state collection for this event and returns a
|
|
1874
|
-
* typed handle over it.
|
|
1875
|
-
*
|
|
1876
|
-
* Pass a definition built by one of the definition constructors ({@link value},
|
|
1877
|
-
* {@link map}, {@link deque}, {@link messageValue}, {@link messageMap},
|
|
1878
|
-
* {@link messageDeque}) — the same frozen object placed in
|
|
1879
|
-
* `Configuration.stateCollections`. The returned handle (and any iterator it
|
|
1880
|
-
* opens) is scoped to this single event attempt; do not retain it past the
|
|
1881
|
-
* handler invocation. Handles are cached per context by definition identity
|
|
1882
|
-
* (kind, payload, and name), so repeated calls for the same definition return
|
|
1883
|
-
* the same wrapper; a mismatched definition reusing a name misses the cache
|
|
1884
|
-
* and is rejected core-side at vend.
|
|
1885
|
-
*
|
|
1886
|
-
* @param {object} definition - A frozen definition from a definition constructor.
|
|
1887
|
-
* @returns {ValueState|MapState|DequeState} The typed state handle.
|
|
1888
|
-
* @throws {TransientStateError} If the definition is malformed — a missing or
|
|
1889
|
-
* non-string `name`, or an unrecognized `kind`/`payload` (a caller mistake,
|
|
1890
|
-
* so transient rather than a message-discarding permanent).
|
|
1891
|
-
* @throws {PermanentStateError} If the collection name is unregistered or its
|
|
1892
|
-
* durably-registered schema (kind/payload) mismatches (rejected core-side).
|
|
1893
|
-
*/
|
|
1894
|
-
state(definition) {
|
|
1895
|
-
const access = stateDefinitionAccess.get(definition);
|
|
1896
|
-
if (access === undefined) {
|
|
1897
|
-
throw new TransientStateError(
|
|
1898
|
-
"state: definition must come from a Prosody state definition constructor",
|
|
1899
|
-
);
|
|
1900
|
-
}
|
|
1901
|
-
const cacheKey = definition;
|
|
1902
|
-
const cached = this.stateHandles.get(cacheKey);
|
|
1903
|
-
if (cached !== undefined) return cached;
|
|
1904
|
-
const handle = stateSync(() =>
|
|
1905
|
-
access.owned(this.nativeContext, definition.name),
|
|
1906
|
-
);
|
|
1907
|
-
this.stateHandles.set(cacheKey, handle);
|
|
1908
|
-
return handle;
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
15
|
+
const {
|
|
16
|
+
getCurrentLogger,
|
|
17
|
+
loggerIsSet,
|
|
18
|
+
setLogger,
|
|
19
|
+
setLoggerIfUnset,
|
|
20
|
+
} = require("./lib/logging");
|
|
21
|
+
const {
|
|
22
|
+
EventHandlerError,
|
|
23
|
+
PermanentError,
|
|
24
|
+
PermanentStateError,
|
|
25
|
+
TransientError,
|
|
26
|
+
TransientStateError,
|
|
27
|
+
isStateError,
|
|
28
|
+
permanent,
|
|
29
|
+
transient,
|
|
30
|
+
} = require("./lib/errors");
|
|
31
|
+
const {
|
|
32
|
+
deque,
|
|
33
|
+
map,
|
|
34
|
+
messageDeque,
|
|
35
|
+
messageMap,
|
|
36
|
+
messageValue,
|
|
37
|
+
set,
|
|
38
|
+
value,
|
|
39
|
+
} = require("./lib/state/definitions");
|
|
40
|
+
const { ValueState } = require("./lib/state/value");
|
|
41
|
+
const { MapState } = require("./lib/state/map");
|
|
42
|
+
const { SetState } = require("./lib/state/set");
|
|
43
|
+
const { DequeState } = require("./lib/state/deque");
|
|
44
|
+
const {
|
|
45
|
+
PublishedDeque,
|
|
46
|
+
PublishedMap,
|
|
47
|
+
PublishedSet,
|
|
48
|
+
PublishedValue,
|
|
49
|
+
} = require("./lib/state/published");
|
|
50
|
+
const { Context } = require("./lib/context");
|
|
51
|
+
const { ProsodyClient } = require("./lib/client");
|
|
1911
52
|
|
|
1912
53
|
module.exports = {
|
|
1913
54
|
AdminClient,
|
|
@@ -1922,8 +63,10 @@ module.exports = {
|
|
|
1922
63
|
ProsodyClient,
|
|
1923
64
|
PublishedDeque,
|
|
1924
65
|
PublishedMap,
|
|
66
|
+
PublishedSet,
|
|
1925
67
|
PublishedValue,
|
|
1926
68
|
TransientError,
|
|
69
|
+
SetState,
|
|
1927
70
|
TransientStateError,
|
|
1928
71
|
ValueState,
|
|
1929
72
|
deque,
|
|
@@ -1937,6 +80,7 @@ module.exports = {
|
|
|
1937
80
|
messageMap,
|
|
1938
81
|
messageValue,
|
|
1939
82
|
permanent,
|
|
83
|
+
set,
|
|
1940
84
|
setLogger,
|
|
1941
85
|
setLoggerIfUnset,
|
|
1942
86
|
shutdownTelemetry,
|