@prosody-events/prosody 0.4.0 → 0.5.1
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 +346 -239
- package/bindings.d.ts +430 -174
- package/bindings.js +211 -83
- package/index.d.ts +231 -34
- package/index.js +699 -246
- package/package.json +21 -21
package/index.js
CHANGED
|
@@ -15,14 +15,16 @@
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* @typedef {Object} EventHandler
|
|
18
|
-
* @property {Function}
|
|
19
|
-
* @property {Function}
|
|
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.
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* @typedef {import('./bindings').Configuration} Configuration
|
|
24
25
|
* @typedef {import('./bindings').ConsumerState} ConsumerState
|
|
25
26
|
* @typedef {import('./bindings').Context} Context
|
|
27
|
+
* @typedef {import('./bindings').ExciseMessage} ExciseMessage
|
|
26
28
|
* @typedef {import('./bindings').Message} Message
|
|
27
29
|
* @typedef {import('./bindings').Timer} Timer
|
|
28
30
|
* @typedef {import('./bindings').Mode} Mode
|
|
@@ -38,14 +40,18 @@ const {
|
|
|
38
40
|
} = require("@opentelemetry/api");
|
|
39
41
|
|
|
40
42
|
const {
|
|
43
|
+
AdminClient,
|
|
44
|
+
Message: NativeMessage,
|
|
41
45
|
Mode,
|
|
42
46
|
NativeClient,
|
|
43
47
|
ConsumerState,
|
|
44
48
|
NativeContext,
|
|
45
49
|
initialize,
|
|
50
|
+
flushTelemetry,
|
|
46
51
|
loggerIsSet,
|
|
47
52
|
setLogger: setLoggerInternal,
|
|
48
53
|
setLoggerIfUnset: setLoggerIfUnsetInternal,
|
|
54
|
+
shutdownTelemetry,
|
|
49
55
|
} = require("./bindings");
|
|
50
56
|
|
|
51
57
|
let _sentry = undefined;
|
|
@@ -169,12 +175,23 @@ function setLoggerIfUnset(logger) {
|
|
|
169
175
|
*/
|
|
170
176
|
class ProsodyClient {
|
|
171
177
|
/**
|
|
172
|
-
*
|
|
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.
|
|
173
187
|
*
|
|
174
|
-
* @param {Configuration} config - The configuration
|
|
188
|
+
* @param {Configuration} config - The client configuration.
|
|
189
|
+
* @returns {Promise<ProsodyClient>} The initialized client.
|
|
175
190
|
*/
|
|
176
|
-
|
|
177
|
-
|
|
191
|
+
static async create(config) {
|
|
192
|
+
const client = Object.create(ProsodyClient.prototype);
|
|
193
|
+
client.nativeClient = await NativeClient.create(config);
|
|
194
|
+
return client;
|
|
178
195
|
}
|
|
179
196
|
|
|
180
197
|
/**
|
|
@@ -216,12 +233,36 @@ class ProsodyClient {
|
|
|
216
233
|
return this.nativeClient.isStalled();
|
|
217
234
|
}
|
|
218
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
|
+
|
|
219
259
|
/**
|
|
220
260
|
* Sends a message to a specified topic.
|
|
221
261
|
*
|
|
222
262
|
* @param {string} topic - The topic to send the message to.
|
|
223
263
|
* @param {string} key - The key of the message.
|
|
224
|
-
* @param {*} payload - The payload of the message.
|
|
264
|
+
* @param {*} payload - The payload of the message. Serialized here; Kafka
|
|
265
|
+
* receives the bytes verbatim.
|
|
225
266
|
* @param {AbortSignal} [signal] - Optional abort signal to cancel the send operation. When aborted, the promise will reject with the abort reason.
|
|
226
267
|
* @returns {Promise<void>} A promise that resolves when the message has been successfully sent.
|
|
227
268
|
* @throws {Error} If the send operation fails or is aborted.
|
|
@@ -233,53 +274,143 @@ class ProsodyClient {
|
|
|
233
274
|
await this.nativeClient.send(
|
|
234
275
|
topic,
|
|
235
276
|
key,
|
|
236
|
-
payload,
|
|
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,
|
|
237
298
|
carrier,
|
|
238
299
|
signal && onAbort(signal),
|
|
239
300
|
);
|
|
240
301
|
}
|
|
241
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
|
+
|
|
242
348
|
/**
|
|
243
349
|
* Subscribes to receive messages using the provided event handler.
|
|
244
350
|
*
|
|
245
351
|
* @param {EventHandler} eventHandler - The event handler to process received messages and timers.
|
|
246
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.
|
|
247
354
|
* @throws {Error} If the subscription fails to establish.
|
|
248
355
|
*/
|
|
249
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
|
+
|
|
250
363
|
const tracer = trace.getTracer("prosody");
|
|
251
|
-
const {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
+
};
|
|
283
414
|
|
|
284
415
|
await this.nativeClient.subscribe({
|
|
285
416
|
isPermanent: ([err]) => {
|
|
@@ -292,56 +423,34 @@ class ProsodyClient {
|
|
|
292
423
|
|
|
293
424
|
onMessage: async (err, [nativeContext, message, carrier]) => {
|
|
294
425
|
if (err) throw err;
|
|
426
|
+
return handleRecord(
|
|
427
|
+
nativeContext,
|
|
428
|
+
withParsedPayload(message),
|
|
429
|
+
carrier,
|
|
430
|
+
onMessage,
|
|
431
|
+
"onMessage",
|
|
432
|
+
"message",
|
|
433
|
+
);
|
|
434
|
+
},
|
|
295
435
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
controller.abort(new Error("message cancelled"));
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
try {
|
|
311
|
-
const context = new Context(nativeContext);
|
|
312
|
-
await onMessage(context, message, controller.signal);
|
|
313
|
-
} catch (error) {
|
|
314
|
-
getCurrentLogger()?.error(
|
|
315
|
-
"Message handler error",
|
|
316
|
-
error.cause ?? error,
|
|
317
|
-
);
|
|
318
|
-
const cause = error.cause ?? error;
|
|
319
|
-
span.recordException(cause);
|
|
320
|
-
span.setStatus({
|
|
321
|
-
code: SpanStatusCode.ERROR,
|
|
322
|
-
message: cause.message,
|
|
323
|
-
});
|
|
324
|
-
captureException(error, "message", {
|
|
325
|
-
topic: message.topic,
|
|
326
|
-
partition: message.partition,
|
|
327
|
-
key: message.key,
|
|
328
|
-
offset: message.offset,
|
|
329
|
-
});
|
|
330
|
-
throw error;
|
|
331
|
-
} finally {
|
|
332
|
-
completed = true;
|
|
333
|
-
span.end();
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
});
|
|
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
|
+
);
|
|
337
446
|
},
|
|
338
447
|
|
|
339
448
|
onTimer: async (err, [nativeContext, timer, carrier]) => {
|
|
340
449
|
if (err) throw err;
|
|
341
450
|
|
|
342
451
|
const ctx = propagation.extract(otelContext.active(), carrier);
|
|
343
|
-
|
|
344
|
-
|
|
452
|
+
return otelContext.with(ctx, async () => {
|
|
453
|
+
return tracer.startActiveSpan("onTimer", async (span) => {
|
|
345
454
|
const controller = new AbortController();
|
|
346
455
|
let completed = false;
|
|
347
456
|
|
|
@@ -356,6 +465,7 @@ class ProsodyClient {
|
|
|
356
465
|
try {
|
|
357
466
|
const context = new Context(nativeContext);
|
|
358
467
|
await onTimer(context, timer, controller.signal);
|
|
468
|
+
return "null";
|
|
359
469
|
} catch (error) {
|
|
360
470
|
getCurrentLogger()?.error(
|
|
361
471
|
"Timer handler error",
|
|
@@ -383,7 +493,7 @@ class ProsodyClient {
|
|
|
383
493
|
}
|
|
384
494
|
|
|
385
495
|
/**
|
|
386
|
-
*
|
|
496
|
+
* Stops the consumer. You can subscribe again later.
|
|
387
497
|
*
|
|
388
498
|
* @returns {Promise<void>} A promise that resolves when the unsubscribe operation is complete.
|
|
389
499
|
* @throws {Error} If the unsubscribe operation fails.
|
|
@@ -391,6 +501,18 @@ class ProsodyClient {
|
|
|
391
501
|
async unsubscribe() {
|
|
392
502
|
await this.nativeClient.unsubscribe();
|
|
393
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
|
+
}
|
|
394
516
|
}
|
|
395
517
|
|
|
396
518
|
// napi-rs can only surface rejections whose value is an object, function, or
|
|
@@ -467,6 +589,26 @@ class PermanentError extends EventHandlerError {
|
|
|
467
589
|
}
|
|
468
590
|
}
|
|
469
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
|
+
|
|
470
612
|
/**
|
|
471
613
|
* Represents a transient keyed-state failure that may succeed on a later
|
|
472
614
|
* attempt — a store read/write timeout, AND every caller mistake (a
|
|
@@ -566,7 +708,7 @@ function createErrorDecorator(ErrorClass) {
|
|
|
566
708
|
/**
|
|
567
709
|
* Decorator factory for marking errors as transient.
|
|
568
710
|
* Can be applied to methods to automatically wrap specified error types as transient.
|
|
569
|
-
* @param {...(new(...args:
|
|
711
|
+
* @param {...(new(...args: never[]) => Error)} exceptionTypes - The error types to be treated as transient.
|
|
570
712
|
* @returns {Function} A decorator function.
|
|
571
713
|
*/
|
|
572
714
|
const transient = createErrorDecorator(TransientError);
|
|
@@ -574,7 +716,7 @@ const transient = createErrorDecorator(TransientError);
|
|
|
574
716
|
/**
|
|
575
717
|
* Decorator factory for marking errors as permanent.
|
|
576
718
|
* Can be applied to methods to automatically wrap specified error types as permanent.
|
|
577
|
-
* @param {...(new(...args:
|
|
719
|
+
* @param {...(new(...args: never[]) => Error)} exceptionTypes - The error types to be treated as permanent.
|
|
578
720
|
* @returns {Function} A decorator function.
|
|
579
721
|
*/
|
|
580
722
|
const permanent = createErrorDecorator(PermanentError);
|
|
@@ -687,16 +829,58 @@ function stateSync(operation) {
|
|
|
687
829
|
* @returns {Readonly<object>} The frozen definition.
|
|
688
830
|
* @private
|
|
689
831
|
*/
|
|
690
|
-
|
|
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 = {}) {
|
|
691
871
|
const definition = { name, kind, payload };
|
|
692
872
|
if (options.ttlSeconds !== undefined)
|
|
693
873
|
definition.ttlSeconds = options.ttlSeconds;
|
|
694
874
|
if (options.readUncommitted !== undefined)
|
|
695
875
|
definition.readUncommitted = options.readUncommitted;
|
|
876
|
+
if (options.published !== undefined) definition.published = options.published;
|
|
877
|
+
if (options.readCache !== undefined) definition.readCache = options.readCache;
|
|
696
878
|
if (options.keysetLimit !== undefined)
|
|
697
879
|
definition.keysetLimit = options.keysetLimit;
|
|
698
880
|
if (options.capacity !== undefined) definition.capacity = options.capacity;
|
|
699
|
-
|
|
881
|
+
const frozen = Object.freeze(definition);
|
|
882
|
+
stateDefinitionAccess.set(frozen, access);
|
|
883
|
+
return frozen;
|
|
700
884
|
}
|
|
701
885
|
|
|
702
886
|
/**
|
|
@@ -708,7 +892,7 @@ function stateDefinition(name, kind, payload, options = {}) {
|
|
|
708
892
|
* @returns {Readonly<object>} A frozen definition for `stateCollections` and `state()`.
|
|
709
893
|
*/
|
|
710
894
|
function value(name, options) {
|
|
711
|
-
return stateDefinition(name, "value", "json", options);
|
|
895
|
+
return stateDefinition(name, "value", "json", VALUE_ACCESS, options);
|
|
712
896
|
}
|
|
713
897
|
|
|
714
898
|
/**
|
|
@@ -719,7 +903,7 @@ function value(name, options) {
|
|
|
719
903
|
* @returns {Readonly<object>} A frozen definition for `stateCollections` and `state()`.
|
|
720
904
|
*/
|
|
721
905
|
function map(name, options) {
|
|
722
|
-
return stateDefinition(name, "map", "json", options);
|
|
906
|
+
return stateDefinition(name, "map", "json", MAP_ACCESS, options);
|
|
723
907
|
}
|
|
724
908
|
|
|
725
909
|
/**
|
|
@@ -731,7 +915,7 @@ function map(name, options) {
|
|
|
731
915
|
* @returns {Readonly<object>} A frozen definition for `stateCollections` and `state()`.
|
|
732
916
|
*/
|
|
733
917
|
function deque(name, options) {
|
|
734
|
-
return stateDefinition(name, "deque", "json", options);
|
|
918
|
+
return stateDefinition(name, "deque", "json", DEQUE_ACCESS, options);
|
|
735
919
|
}
|
|
736
920
|
|
|
737
921
|
/**
|
|
@@ -743,7 +927,13 @@ function deque(name, options) {
|
|
|
743
927
|
* @returns {Readonly<object>} A frozen definition for `stateCollections` and `state()`.
|
|
744
928
|
*/
|
|
745
929
|
function messageValue(name, options) {
|
|
746
|
-
return stateDefinition(
|
|
930
|
+
return stateDefinition(
|
|
931
|
+
name,
|
|
932
|
+
"value",
|
|
933
|
+
"message",
|
|
934
|
+
MESSAGE_VALUE_ACCESS,
|
|
935
|
+
options,
|
|
936
|
+
);
|
|
747
937
|
}
|
|
748
938
|
|
|
749
939
|
/**
|
|
@@ -755,7 +945,7 @@ function messageValue(name, options) {
|
|
|
755
945
|
* @returns {Readonly<object>} A frozen definition for `stateCollections` and `state()`.
|
|
756
946
|
*/
|
|
757
947
|
function messageMap(name, options) {
|
|
758
|
-
return stateDefinition(name, "map", "message", options);
|
|
948
|
+
return stateDefinition(name, "map", "message", MESSAGE_MAP_ACCESS, options);
|
|
759
949
|
}
|
|
760
950
|
|
|
761
951
|
/**
|
|
@@ -768,7 +958,13 @@ function messageMap(name, options) {
|
|
|
768
958
|
* @returns {Readonly<object>} A frozen definition for `stateCollections` and `state()`.
|
|
769
959
|
*/
|
|
770
960
|
function messageDeque(name, options) {
|
|
771
|
-
return stateDefinition(
|
|
961
|
+
return stateDefinition(
|
|
962
|
+
name,
|
|
963
|
+
"deque",
|
|
964
|
+
"message",
|
|
965
|
+
MESSAGE_DEQUE_ACCESS,
|
|
966
|
+
options,
|
|
967
|
+
);
|
|
772
968
|
}
|
|
773
969
|
|
|
774
970
|
/**
|
|
@@ -784,18 +980,26 @@ function messageDeque(name, options) {
|
|
|
784
980
|
* or early-exit path is normalized through `toStateError`, so every keyed-state
|
|
785
981
|
* failure a caller can observe is a `PermanentStateError`/`TransientStateError`.
|
|
786
982
|
*
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
* @param {object}
|
|
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.
|
|
790
987
|
* @param {(item: *) => *} transform - Maps each raw item to the yielded value.
|
|
791
988
|
* @returns {AsyncIterableIterator<*>} The async iterator.
|
|
792
989
|
* @private
|
|
793
990
|
*/
|
|
794
|
-
function stateIterator(
|
|
991
|
+
function stateIterator(source, transform) {
|
|
992
|
+
let cursor;
|
|
795
993
|
let finished = false;
|
|
796
994
|
let chunk = [];
|
|
797
995
|
let offset = 0;
|
|
798
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
|
+
};
|
|
799
1003
|
// Serialize the complete iterator protocol, not just native pulls. Without
|
|
800
1004
|
// this queue, concurrent next() continuations can both reset `offset` after
|
|
801
1005
|
// awaiting the same chunk and yield the same first item. return() shares the
|
|
@@ -813,6 +1017,7 @@ function stateIterator(cursor, transform) {
|
|
|
813
1017
|
// mask the primary error. `try/catch` (not `.catch()`) so a synchronous throw
|
|
814
1018
|
// from `close()` is swallowed too.
|
|
815
1019
|
const closeQuietly = async () => {
|
|
1020
|
+
if (cursor === undefined) return;
|
|
816
1021
|
try {
|
|
817
1022
|
await cursor.close();
|
|
818
1023
|
} catch {
|
|
@@ -822,6 +1027,7 @@ function stateIterator(cursor, transform) {
|
|
|
822
1027
|
// Close on clean exhaustion / early exit, where there is no primary error to
|
|
823
1028
|
// mask: a close failure surfaces through the state-error model.
|
|
824
1029
|
const closeOrThrow = async () => {
|
|
1030
|
+
if (cursor === undefined) return;
|
|
825
1031
|
try {
|
|
826
1032
|
await cursor.close();
|
|
827
1033
|
} catch (error) {
|
|
@@ -836,7 +1042,7 @@ function stateIterator(cursor, transform) {
|
|
|
836
1042
|
chunk = [];
|
|
837
1043
|
offset = 0;
|
|
838
1044
|
try {
|
|
839
|
-
chunk = await
|
|
1045
|
+
chunk = await (await openCursor()).nextChunk(injectedCarrier());
|
|
840
1046
|
} catch (error) {
|
|
841
1047
|
finished = true;
|
|
842
1048
|
await closeQuietly();
|
|
@@ -882,26 +1088,363 @@ function stateIterator(cursor, transform) {
|
|
|
882
1088
|
}
|
|
883
1089
|
|
|
884
1090
|
/**
|
|
885
|
-
*
|
|
886
|
-
*
|
|
887
|
-
|
|
888
|
-
|
|
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
|
|
889
1322
|
*/
|
|
890
|
-
|
|
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 {
|
|
891
1404
|
/**
|
|
892
|
-
* @param {
|
|
1405
|
+
* @param {object} native - The vended native handle.
|
|
1406
|
+
* @param {object} items - The item codec for the collection's payload flavour.
|
|
893
1407
|
*/
|
|
894
|
-
constructor(native) {
|
|
1408
|
+
constructor(native, items) {
|
|
895
1409
|
this.native = native;
|
|
1410
|
+
this.items = items;
|
|
896
1411
|
}
|
|
897
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 {
|
|
898
1439
|
/**
|
|
899
1440
|
* Reads the current value.
|
|
900
1441
|
* @returns {Promise<*|null>} The stored value, or null when absent/cleared.
|
|
901
1442
|
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
902
1443
|
*/
|
|
903
1444
|
get() {
|
|
904
|
-
return stateOp((carrier) => this.native.get(carrier))
|
|
1445
|
+
return stateOp((carrier) => this.native.get(carrier)).then(
|
|
1446
|
+
this.items.decode,
|
|
1447
|
+
);
|
|
905
1448
|
}
|
|
906
1449
|
|
|
907
1450
|
/**
|
|
@@ -917,7 +1460,7 @@ class ValueState {
|
|
|
917
1460
|
* reports one.
|
|
918
1461
|
*/
|
|
919
1462
|
set(value) {
|
|
920
|
-
return stateOp((carrier) => this.
|
|
1463
|
+
return stateOp((carrier) => this.items.set(this.native, value, carrier));
|
|
921
1464
|
}
|
|
922
1465
|
|
|
923
1466
|
/**
|
|
@@ -928,24 +1471,6 @@ class ValueState {
|
|
|
928
1471
|
clear() {
|
|
929
1472
|
return stateOp((carrier) => this.native.clear(carrier));
|
|
930
1473
|
}
|
|
931
|
-
|
|
932
|
-
/**
|
|
933
|
-
* Durably commits the buffered operations mid-handler (at-least-once; the
|
|
934
|
-
* committed floor survives a later rollback or a failed event).
|
|
935
|
-
* @returns {Promise<void>} Resolves with no value — the erased seam drops the outcome.
|
|
936
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized commit failure.
|
|
937
|
-
*/
|
|
938
|
-
commit() {
|
|
939
|
-
return stateOp((carrier) => this.native.commit(carrier));
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
/**
|
|
943
|
-
* Discards buffered uncommitted operations back to the last committed floor.
|
|
944
|
-
* @returns {Promise<void>} Resolves with no value.
|
|
945
|
-
*/
|
|
946
|
-
rollback() {
|
|
947
|
-
return stateOp((carrier) => this.native.rollback(carrier));
|
|
948
|
-
}
|
|
949
1474
|
}
|
|
950
1475
|
|
|
951
1476
|
/**
|
|
@@ -954,14 +1479,7 @@ class ValueState {
|
|
|
954
1479
|
* span per operation; this binding propagates context without adding an N-API
|
|
955
1480
|
* span. Handles and iterators are valid only within the handler invocation.
|
|
956
1481
|
*/
|
|
957
|
-
class MapState {
|
|
958
|
-
/**
|
|
959
|
-
* @param {import('./bindings').NativeMapState} native - The vended native handle.
|
|
960
|
-
*/
|
|
961
|
-
constructor(native) {
|
|
962
|
-
this.native = native;
|
|
963
|
-
}
|
|
964
|
-
|
|
1482
|
+
class MapState extends StateHandle {
|
|
965
1483
|
/**
|
|
966
1484
|
* Reads the value for `key`.
|
|
967
1485
|
* @param {string} key - The map key.
|
|
@@ -969,7 +1487,9 @@ class MapState {
|
|
|
969
1487
|
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
970
1488
|
*/
|
|
971
1489
|
get(key) {
|
|
972
|
-
return stateOp((carrier) => this.native.get(key, carrier))
|
|
1490
|
+
return stateOp((carrier) => this.native.get(key, carrier)).then(
|
|
1491
|
+
this.items.decode,
|
|
1492
|
+
);
|
|
973
1493
|
}
|
|
974
1494
|
|
|
975
1495
|
/**
|
|
@@ -983,7 +1503,9 @@ class MapState {
|
|
|
983
1503
|
* @throws {PermanentStateError|TransientStateError} If the read fails.
|
|
984
1504
|
*/
|
|
985
1505
|
getMany(keys) {
|
|
986
|
-
return stateOp((carrier) => this.native.getMany(keys, carrier))
|
|
1506
|
+
return stateOp((carrier) => this.native.getMany(keys, carrier)).then(
|
|
1507
|
+
(items) => items.map(this.items.decode),
|
|
1508
|
+
);
|
|
987
1509
|
}
|
|
988
1510
|
|
|
989
1511
|
/**
|
|
@@ -1014,7 +1536,9 @@ class MapState {
|
|
|
1014
1536
|
* reports one.
|
|
1015
1537
|
*/
|
|
1016
1538
|
set(key, value) {
|
|
1017
|
-
return stateOp((carrier) =>
|
|
1539
|
+
return stateOp((carrier) =>
|
|
1540
|
+
this.items.setKey(this.native, key, value, carrier),
|
|
1541
|
+
);
|
|
1018
1542
|
}
|
|
1019
1543
|
|
|
1020
1544
|
/**
|
|
@@ -1053,7 +1577,7 @@ class MapState {
|
|
|
1053
1577
|
entries(direction = "forward") {
|
|
1054
1578
|
return stateIterator(
|
|
1055
1579
|
stateSync(() => this.native.scan(direction, injectedCarrier())),
|
|
1056
|
-
(
|
|
1580
|
+
([key, item]) => [key, this.items.decode(item)],
|
|
1057
1581
|
);
|
|
1058
1582
|
}
|
|
1059
1583
|
|
|
@@ -1087,7 +1611,7 @@ class MapState {
|
|
|
1087
1611
|
values(direction = "forward") {
|
|
1088
1612
|
return stateIterator(
|
|
1089
1613
|
stateSync(() => this.native.scan(direction, injectedCarrier())),
|
|
1090
|
-
(
|
|
1614
|
+
([, item]) => this.items.decode(item),
|
|
1091
1615
|
);
|
|
1092
1616
|
}
|
|
1093
1617
|
|
|
@@ -1099,24 +1623,6 @@ class MapState {
|
|
|
1099
1623
|
[Symbol.asyncIterator]() {
|
|
1100
1624
|
return this.entries();
|
|
1101
1625
|
}
|
|
1102
|
-
|
|
1103
|
-
/**
|
|
1104
|
-
* Durably commits the buffered operations mid-handler (at-least-once; the
|
|
1105
|
-
* committed floor survives a later rollback or a failed event).
|
|
1106
|
-
* @returns {Promise<void>} Resolves with no value — the erased seam drops the outcome.
|
|
1107
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized commit failure.
|
|
1108
|
-
*/
|
|
1109
|
-
commit() {
|
|
1110
|
-
return stateOp((carrier) => this.native.commit(carrier));
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
/**
|
|
1114
|
-
* Discards buffered uncommitted operations back to the last committed floor.
|
|
1115
|
-
* @returns {Promise<void>} Resolves with no value.
|
|
1116
|
-
*/
|
|
1117
|
-
rollback() {
|
|
1118
|
-
return stateOp((carrier) => this.native.rollback(carrier));
|
|
1119
|
-
}
|
|
1120
1626
|
}
|
|
1121
1627
|
|
|
1122
1628
|
/**
|
|
@@ -1125,14 +1631,7 @@ class MapState {
|
|
|
1125
1631
|
* binding propagates context without adding an N-API span. Handles and
|
|
1126
1632
|
* iterators are valid only within the handler invocation that vended them.
|
|
1127
1633
|
*/
|
|
1128
|
-
class DequeState {
|
|
1129
|
-
/**
|
|
1130
|
-
* @param {import('./bindings').NativeDequeState} native - The vended native handle.
|
|
1131
|
-
*/
|
|
1132
|
-
constructor(native) {
|
|
1133
|
-
this.native = native;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1634
|
+
class DequeState extends StateHandle {
|
|
1136
1635
|
/**
|
|
1137
1636
|
* Appends an element at the back. Writing JSON `null` is a caller mistake
|
|
1138
1637
|
* rejected with a {@link TransientStateError}, so an uncaught handler bug
|
|
@@ -1143,7 +1642,9 @@ class DequeState {
|
|
|
1143
1642
|
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1144
1643
|
*/
|
|
1145
1644
|
push(item) {
|
|
1146
|
-
return stateOp((carrier) =>
|
|
1645
|
+
return stateOp((carrier) =>
|
|
1646
|
+
this.items.pushBack(this.native, item, carrier),
|
|
1647
|
+
);
|
|
1147
1648
|
}
|
|
1148
1649
|
|
|
1149
1650
|
/**
|
|
@@ -1156,7 +1657,9 @@ class DequeState {
|
|
|
1156
1657
|
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1157
1658
|
*/
|
|
1158
1659
|
unshift(item) {
|
|
1159
|
-
return stateOp((carrier) =>
|
|
1660
|
+
return stateOp((carrier) =>
|
|
1661
|
+
this.items.pushFront(this.native, item, carrier),
|
|
1662
|
+
);
|
|
1160
1663
|
}
|
|
1161
1664
|
|
|
1162
1665
|
/**
|
|
@@ -1165,7 +1668,9 @@ class DequeState {
|
|
|
1165
1668
|
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1166
1669
|
*/
|
|
1167
1670
|
pop() {
|
|
1168
|
-
return stateOp((carrier) => this.native.popBack(carrier))
|
|
1671
|
+
return stateOp((carrier) => this.native.popBack(carrier)).then(
|
|
1672
|
+
this.items.decode,
|
|
1673
|
+
);
|
|
1169
1674
|
}
|
|
1170
1675
|
|
|
1171
1676
|
/**
|
|
@@ -1174,7 +1679,9 @@ class DequeState {
|
|
|
1174
1679
|
* @throws {PermanentStateError|TransientStateError} On a categorized store failure.
|
|
1175
1680
|
*/
|
|
1176
1681
|
shift() {
|
|
1177
|
-
return stateOp((carrier) => this.native.popFront(carrier))
|
|
1682
|
+
return stateOp((carrier) => this.native.popFront(carrier)).then(
|
|
1683
|
+
this.items.decode,
|
|
1684
|
+
);
|
|
1178
1685
|
}
|
|
1179
1686
|
|
|
1180
1687
|
/**
|
|
@@ -1237,9 +1744,13 @@ class DequeState {
|
|
|
1237
1744
|
);
|
|
1238
1745
|
}
|
|
1239
1746
|
if (index === 0)
|
|
1240
|
-
return stateOp((carrier) => this.native.peekFront(carrier))
|
|
1747
|
+
return stateOp((carrier) => this.native.peekFront(carrier)).then(
|
|
1748
|
+
this.items.decode,
|
|
1749
|
+
);
|
|
1241
1750
|
if (index === -1)
|
|
1242
|
-
return stateOp((carrier) => this.native.peekBack(carrier))
|
|
1751
|
+
return stateOp((carrier) => this.native.peekBack(carrier)).then(
|
|
1752
|
+
this.items.decode,
|
|
1753
|
+
);
|
|
1243
1754
|
let position = index;
|
|
1244
1755
|
if (position < 0) {
|
|
1245
1756
|
position += await this.length();
|
|
@@ -1248,7 +1759,9 @@ class DequeState {
|
|
|
1248
1759
|
}
|
|
1249
1760
|
// Beyond the addressable u32 range can only be past the end, never a wrap.
|
|
1250
1761
|
if (position > 0xffffffff) return null;
|
|
1251
|
-
return stateOp((carrier) => this.native.get(position, carrier))
|
|
1762
|
+
return stateOp((carrier) => this.native.get(position, carrier)).then(
|
|
1763
|
+
this.items.decode,
|
|
1764
|
+
);
|
|
1252
1765
|
}
|
|
1253
1766
|
|
|
1254
1767
|
/**
|
|
@@ -1263,7 +1776,7 @@ class DequeState {
|
|
|
1263
1776
|
values(direction = "forward") {
|
|
1264
1777
|
return stateIterator(
|
|
1265
1778
|
stateSync(() => this.native.scan(direction, injectedCarrier())),
|
|
1266
|
-
(item) => item,
|
|
1779
|
+
(item) => this.items.decode(item),
|
|
1267
1780
|
);
|
|
1268
1781
|
}
|
|
1269
1782
|
|
|
@@ -1275,24 +1788,6 @@ class DequeState {
|
|
|
1275
1788
|
[Symbol.asyncIterator]() {
|
|
1276
1789
|
return this.values();
|
|
1277
1790
|
}
|
|
1278
|
-
|
|
1279
|
-
/**
|
|
1280
|
-
* Durably commits the buffered operations mid-handler (at-least-once; the
|
|
1281
|
-
* committed floor survives a later rollback or a failed event).
|
|
1282
|
-
* @returns {Promise<void>} Resolves with no value — the erased seam drops the outcome.
|
|
1283
|
-
* @throws {PermanentStateError|TransientStateError} On a categorized commit failure.
|
|
1284
|
-
*/
|
|
1285
|
-
commit() {
|
|
1286
|
-
return stateOp((carrier) => this.native.commit(carrier));
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
/**
|
|
1290
|
-
* Discards buffered uncommitted operations back to the last committed floor.
|
|
1291
|
-
* @returns {Promise<void>} Resolves with no value.
|
|
1292
|
-
*/
|
|
1293
|
-
rollback() {
|
|
1294
|
-
return stateOp((carrier) => this.native.rollback(carrier));
|
|
1295
|
-
}
|
|
1296
1791
|
}
|
|
1297
1792
|
|
|
1298
1793
|
/**
|
|
@@ -1397,72 +1892,25 @@ class Context {
|
|
|
1397
1892
|
* durably-registered schema (kind/payload) mismatches (rejected core-side).
|
|
1398
1893
|
*/
|
|
1399
1894
|
state(definition) {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
// Read + validate the definition shape. Wrapped so a hostile definition
|
|
1403
|
-
// (e.g. throwing property getters) still surfaces as a classified caller
|
|
1404
|
-
// mistake rather than a raw synchronous throw.
|
|
1405
|
-
({ name, kind, payload } = definition ?? {});
|
|
1406
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
1407
|
-
throw new TransientStateError(
|
|
1408
|
-
`state: definition.name must be a non-empty string, got ${describeValue(name)}`,
|
|
1409
|
-
);
|
|
1410
|
-
}
|
|
1411
|
-
if (kind !== "value" && kind !== "map" && kind !== "deque") {
|
|
1412
|
-
throw new TransientStateError(
|
|
1413
|
-
`state: unknown collection kind ${describeValue(kind)}`,
|
|
1414
|
-
);
|
|
1415
|
-
}
|
|
1416
|
-
if (payload !== "json" && payload !== "message") {
|
|
1417
|
-
throw new TransientStateError(
|
|
1418
|
-
`state: unknown collection payload ${describeValue(payload)}`,
|
|
1419
|
-
);
|
|
1420
|
-
}
|
|
1421
|
-
} catch (error) {
|
|
1422
|
-
if (error instanceof EventHandlerError) throw error;
|
|
1895
|
+
const access = stateDefinitionAccess.get(definition);
|
|
1896
|
+
if (access === undefined) {
|
|
1423
1897
|
throw new TransientStateError(
|
|
1424
|
-
"state:
|
|
1898
|
+
"state: definition must come from a Prosody state definition constructor",
|
|
1425
1899
|
);
|
|
1426
1900
|
}
|
|
1427
|
-
const cacheKey =
|
|
1901
|
+
const cacheKey = definition;
|
|
1428
1902
|
const cached = this.stateHandles.get(cacheKey);
|
|
1429
1903
|
if (cached !== undefined) return cached;
|
|
1430
|
-
const
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
case "value":
|
|
1434
|
-
handle = new ValueState(
|
|
1435
|
-
stateSync(() =>
|
|
1436
|
-
message
|
|
1437
|
-
? this.nativeContext.messageValueState(name)
|
|
1438
|
-
: this.nativeContext.valueState(name),
|
|
1439
|
-
),
|
|
1440
|
-
);
|
|
1441
|
-
break;
|
|
1442
|
-
case "map":
|
|
1443
|
-
handle = new MapState(
|
|
1444
|
-
stateSync(() =>
|
|
1445
|
-
message
|
|
1446
|
-
? this.nativeContext.messageMapState(name)
|
|
1447
|
-
: this.nativeContext.mapState(name),
|
|
1448
|
-
),
|
|
1449
|
-
);
|
|
1450
|
-
break;
|
|
1451
|
-
default:
|
|
1452
|
-
handle = new DequeState(
|
|
1453
|
-
stateSync(() =>
|
|
1454
|
-
message
|
|
1455
|
-
? this.nativeContext.messageDequeState(name)
|
|
1456
|
-
: this.nativeContext.dequeState(name),
|
|
1457
|
-
),
|
|
1458
|
-
);
|
|
1459
|
-
}
|
|
1904
|
+
const handle = stateSync(() =>
|
|
1905
|
+
access.owned(this.nativeContext, definition.name),
|
|
1906
|
+
);
|
|
1460
1907
|
this.stateHandles.set(cacheKey, handle);
|
|
1461
1908
|
return handle;
|
|
1462
1909
|
}
|
|
1463
1910
|
}
|
|
1464
1911
|
|
|
1465
1912
|
module.exports = {
|
|
1913
|
+
AdminClient,
|
|
1466
1914
|
ConsumerState,
|
|
1467
1915
|
Context,
|
|
1468
1916
|
DequeState,
|
|
@@ -1472,11 +1920,15 @@ module.exports = {
|
|
|
1472
1920
|
PermanentError,
|
|
1473
1921
|
PermanentStateError,
|
|
1474
1922
|
ProsodyClient,
|
|
1923
|
+
PublishedDeque,
|
|
1924
|
+
PublishedMap,
|
|
1925
|
+
PublishedValue,
|
|
1475
1926
|
TransientError,
|
|
1476
1927
|
TransientStateError,
|
|
1477
1928
|
ValueState,
|
|
1478
1929
|
deque,
|
|
1479
1930
|
getCurrentLogger,
|
|
1931
|
+
flushTelemetry,
|
|
1480
1932
|
initialize,
|
|
1481
1933
|
isStateError,
|
|
1482
1934
|
loggerIsSet,
|
|
@@ -1487,6 +1939,7 @@ module.exports = {
|
|
|
1487
1939
|
permanent,
|
|
1488
1940
|
setLogger,
|
|
1489
1941
|
setLoggerIfUnset,
|
|
1942
|
+
shutdownTelemetry,
|
|
1490
1943
|
transient,
|
|
1491
1944
|
value,
|
|
1492
1945
|
};
|