@prosody-events/prosody 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (6) hide show
  1. package/README.md +346 -239
  2. package/bindings.d.ts +430 -174
  3. package/bindings.js +211 -83
  4. package/index.d.ts +231 -34
  5. package/index.js +699 -246
  6. 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} [onMessage] - Async callback function to handle incoming messages. Receives (context, message, signal).
19
- * @property {Function} [onTimer] - Async callback function to handle timer events. Receives (context, timer, signal).
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
- * Creates a new ProsodyClient instance.
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 options for the client.
188
+ * @param {Configuration} config - The client configuration.
189
+ * @returns {Promise<ProsodyClient>} The initialized client.
175
190
  */
176
- constructor(config) {
177
- this.nativeClient = new NativeClient(config);
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
- onMessage = (context, message, _signal) => {
253
- getCurrentLogger()?.error(
254
- "ProsodyClient: Received a message but no onMessage handler was " +
255
- "provided in subscribe(). To handle messages, implement the onMessage " +
256
- "method in your EventHandler:",
257
- {
258
- topic: message.topic,
259
- partition: message.partition,
260
- offset: message.offset,
261
- key: message.key,
262
- solution:
263
- "Add onMessage: async (context, message, signal) => " +
264
- "{ /* your logic here */ } to your subscribe() call",
265
- },
266
- );
267
- },
268
- onTimer = (context, timer, _signal) => {
269
- getCurrentLogger()?.error(
270
- "ProsodyClient: Received a timer event but no onTimer handler was " +
271
- "provided in subscribe(). To handle timers, implement the onTimer " +
272
- "method in your EventHandler:",
273
- {
274
- key: timer.key,
275
- time: timer.time,
276
- solution:
277
- "Add onTimer: async (context, timer, signal) => " +
278
- "{ /* your logic here */ } to your subscribe() call",
279
- },
280
- );
281
- },
282
- } = eventHandler;
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
- const ctx = propagation.extract(otelContext.active(), carrier);
297
- await otelContext.with(ctx, async () => {
298
- await tracer.startActiveSpan("onMessage", async (span) => {
299
- const controller = new AbortController();
300
- let completed = false;
301
-
302
- // Signal abort when cancellation occurs (before handler completes)
303
- nativeContext.onCancel().then(() => {
304
- if (!completed) {
305
- span.setAttribute("cancelled", true);
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
- await otelContext.with(ctx, async () => {
344
- await tracer.startActiveSpan("onTimer", async (span) => {
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
- * Unsubscribes from receiving messages and shuts down the consumer.
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: any[]) => Error)} exceptionTypes - The error types to be treated as transient.
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: any[]) => Error)} exceptionTypes - The error types to be treated as permanent.
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
- function stateDefinition(name, kind, payload, options = {}) {
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
- return Object.freeze(definition);
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(name, "value", "message", options);
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(name, "deque", "message", options);
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
- * The iterator is valid only within the handler invocation (attempt) that
788
- * opened it; a cursor leaked past the attempt errors on its next pull.
789
- * @param {object} cursor - The native scan cursor.
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(cursor, transform) {
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 cursor.nextChunk(injectedCarrier());
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
- * Typed handle over a single-value keyed-state collection, vended by
886
- * {@link Context#state}. Core records one semantic span per operation; this
887
- * binding propagates context without adding an N-API span. Handles are valid
888
- * only within the handler invocation (attempt) that vended them.
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
- class ValueState {
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 {import('./bindings').NativeValueState} native - The vended native handle.
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.native.set(value, carrier));
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) => this.native.set(key, value, 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
- (entry) => entry,
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
- (entry) => entry[1],
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) => this.native.pushBack(item, 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) => this.native.pushFront(item, 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
- let name, kind, payload;
1401
- try {
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: the definition could not be read (malformed or hostile object)",
1898
+ "state: definition must come from a Prosody state definition constructor",
1425
1899
  );
1426
1900
  }
1427
- const cacheKey = `${kind}:${payload}:${name}`;
1901
+ const cacheKey = definition;
1428
1902
  const cached = this.stateHandles.get(cacheKey);
1429
1903
  if (cached !== undefined) return cached;
1430
- const message = payload === "message";
1431
- let handle;
1432
- switch (kind) {
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
  };