@prosody-events/prosody 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,7 +62,7 @@ async function main() {
62
62
 
63
63
  onMessage: async (context, message, signal) => {
64
64
  // Process the received message
65
- console.log(`Received message: ${JSON.stringify(message)}`);
65
+ console.log(`Received message: ${JSON.stringify(message.payload)}`);
66
66
 
67
67
  // Schedule a timer for delayed processing
68
68
  if (message.payload.scheduleFollowup) {
@@ -221,7 +221,7 @@ Or via environment variables:
221
221
 
222
222
  ```bash
223
223
  PROSODY_PROBE_PORT=8000 # Set to 'none' to disable
224
- PROSODY_STALL_THRESHOLD=15s # Default stall detection threshold
224
+ PROSODY_STALL_THRESHOLD=15s # The default is 5m
225
225
  ```
226
226
 
227
227
  ### Important Notes
@@ -234,14 +234,14 @@ PROSODY_STALL_THRESHOLD=15s # Default stall detection threshold
234
234
  issues.
235
235
  5. The probe server is only active when consuming messages (not for producer-only usage).
236
236
 
237
- You can monitor the stall state programmatically using the client's properties:
237
+ You can monitor the stall state programmatically through the client's async methods:
238
238
 
239
239
  ```javascript
240
240
  // Get the number of partitions currently assigned to this consumer
241
- const partitionCount = client.assignedPartitionCount;
241
+ const partitionCount = await client.assignedPartitionCount();
242
242
 
243
243
  // You can use these in your own health checks or monitoring
244
- if (client.isStalled) {
244
+ if (await client.isStalled()) {
245
245
  console.warn("Consumer has stalled partitions");
246
246
  }
247
247
  ```
@@ -309,19 +309,19 @@ All messages must be processed. Retries indefinitely. Uses defer and monopolizat
309
309
  **Middleware stack:**
310
310
 
311
311
  ```
312
- Kafka → Deduplication → Retry → Defer → Monopolization → Shutdown → Scheduler → Timeout → Telemetry → Handler
312
+ Kafka → Retry → Defer → Monopolization → Deduplication → Cancellation → Scheduler → Timeout → Telemetry → Handler
313
313
  ```
314
314
 
315
- | Layer | Purpose |
316
- | -------------- | ------------------------------------------------- |
317
- | Deduplication | Skips messages whose ID was already processed |
318
- | Retry | Retries transient errors indefinitely |
319
- | Defer | Stores failing messages for timer-based retry |
320
- | Monopolization | Rejects keys exceeding execution time threshold |
321
- | Shutdown | Drains in-flight work on partition revocation |
322
- | Scheduler | Enforces concurrency limits and VT-based priority |
323
- | Timeout | Cancels handlers exceeding deadline |
324
- | Telemetry | Emits handler lifecycle events |
315
+ | Layer | Purpose |
316
+ | -------------- | ------------------------------------------------------ |
317
+ | Retry | Retries transient errors indefinitely |
318
+ | Defer | Stores failing messages for timer-based retry |
319
+ | Monopolization | Rejects keys exceeding execution time threshold |
320
+ | Deduplication | Filters duplicate messages via local cache + Cassandra |
321
+ | Cancellation | Skips work once shutdown or cancellation is signaled |
322
+ | Scheduler | Enforces concurrency limits and VT-based priority |
323
+ | Timeout | Cancels handlers exceeding deadline |
324
+ | Telemetry | Emits handler lifecycle events |
325
325
 
326
326
  ```javascript
327
327
  const client = await ProsodyClient.create({
@@ -439,7 +439,10 @@ Deduplication uses a two-tier approach:
439
439
  - **Global in-memory cache**: A single cache shared across all partitions within the same consumer instance. Survives
440
440
  partition reassignments within the same process. Controlled by `idempotenceCacheSize` (default 8192).
441
441
  - **Cassandra-backed persistent store**: Survives restarts and rebalances across instances. TTL controlled by
442
- `idempotenceTtlS` (default 7 days, i.e. 604800s).
442
+ `idempotenceTtlSeconds` (default 7 days, i.e. 604800s).
443
+
444
+ The producer also keeps a cache of the event IDs it sent. It skips a send that repeats a recent ID for the same topic
445
+ and key. `idempotenceCacheSize` sets the capacity of this cache too.
443
446
 
444
447
  Deduplication is always active. `idempotenceCacheSize` must be greater than `0`; a value of `0` (via either the option
445
448
  or `PROSODY_IDEMPOTENCE_CACHE_SIZE=0`) is rejected when the client is constructed.
@@ -571,6 +574,17 @@ default, all errors are considered transient.
571
574
 
572
575
  The error classes and decorators apply to `onMessage`, `onExcise`, and `onTimer`.
573
576
 
577
+ A handler can read `context.demand` to learn whether it runs for a retry. The retry count is 1 on the first retry. It is an estimate, so keep an exact attempt count in keyed state if you need one:
578
+
579
+ ```typescript
580
+ async onMessage(context, message) {
581
+ if (context.demand.kind === "failure") {
582
+ console.warn(`retry ${context.demand.retry} for ${message.key}`);
583
+ }
584
+ return null;
585
+ }
586
+ ```
587
+
574
588
  #### Using Decorators
575
589
 
576
590
  If you're using TypeScript or a JavaScript environment that supports decorators, you can use the `@permanent` decorator
@@ -579,11 +593,13 @@ to classify exceptions that should not be retried:
579
593
  ```javascript
580
594
  import { permanent, ProsodyClient } from "@prosody-events/prosody";
581
595
 
596
+ class ValidationError extends Error {}
597
+
582
598
  class MyHandler {
583
- @permanent(TypeError, AttributeError)
599
+ @permanent(TypeError, ValidationError)
584
600
  async onMessage(context, message, signal) {
585
601
  // Your message handling logic here
586
- // TypeError and AttributeError will be treated as permanent
602
+ // TypeError and ValidationError will be treated as permanent
587
603
  // All other exceptions will be treated as transient (default behavior)
588
604
  return null;
589
605
  }
@@ -606,13 +622,15 @@ If you're not using decorators, you can still classify errors as permanent by th
606
622
  ```javascript
607
623
  import { PermanentError, ProsodyClient } from "@prosody-events/prosody";
608
624
 
625
+ class ValidationError extends Error {}
626
+
609
627
  const messageHandler = {
610
628
  onMessage: async (context, message, signal) => {
611
629
  try {
612
630
  // Your message handling logic here
613
631
  } catch (error) {
614
- if (error instanceof TypeError || error instanceof AttributeError) {
615
- throw new PermanentError(error.message);
632
+ if (error instanceof TypeError || error instanceof ValidationError) {
633
+ throw new PermanentError(error.message, { cause: error });
616
634
  }
617
635
  // All other exceptions will be treated as transient (default behavior)
618
636
  throw error;
@@ -736,15 +754,60 @@ A definition sets a collection's durable name, kind, and options. Register it on
736
754
 
737
755
  Do not reuse a durable name for a different collection kind or payload type. Create handles inside the handler. Do not retain handles or iterators.
738
756
 
739
- | Collection | JSON payload | Kafka message | Main operations |
740
- | ------------------ | ------------ | ----------------- | -------------------------------------------------------------------- |
741
- | Value | `value<T>` | `messageValue<P>` | `get`, `set`, `clear` |
742
- | Ordered string map | `map<V>` | `messageMap<P>` | `get`, `getMany`, `has`, `set`, `delete`, `entries`, `keys`, `clear` |
743
- | Deque | `deque<T>` | `messageDeque<P>` | `push`, `unshift`, `pop`, `shift`, `at`, `length`, `values`, `clear` |
757
+ | Collection | JSON payload | Kafka message | Main operations |
758
+ | ------------------ | ------------ | ----------------- | ------------------------------------------------------------------------------------------ |
759
+ | Value | `value<T>` | `messageValue<P>` | `get`, `set`, `clear` |
760
+ | Ordered string map | `map<V>` | `messageMap<P>` | `get`, `getMany`, `has`, `hasMany`, `isEmpty`, `set`, `delete`, `entries`, `keys`, `clear` |
761
+ | Deque | `deque<T>` | `messageDeque<P>` | `push`, `unshift`, `pop`, `shift`, `at`, `length`, `values`, `clear` |
762
+ | Ordered string set | `set` | (none) | `add`, `has`, `hasMany`, `delete`, `isEmpty`, `keys`, `values`, `clear` |
763
+
764
+ All operations are asynchronous. Map, set, and deque scans are asynchronous iterables. A `for await` loop can stop early safely.
765
+
766
+ A set stores string members and no values. Its methods follow the JavaScript `Set`, but each returns a promise. Use a set to record which IDs a key has seen:
767
+
768
+ ```typescript
769
+ const SEEN = set("seen-orders", { ttlSeconds: 7 * 24 * 60 * 60 });
770
+
771
+ // In a handler:
772
+ const seen = context.state(SEEN);
773
+ if (await seen.has(message.payload.orderId)) return null;
774
+ await seen.add(message.payload.orderId);
775
+ ```
776
+
777
+ ### Query a collection
778
+
779
+ Map `entries`, `keys`, and `values` and set `keys` and `values` accept a direction or a `KeyQueryOptions`. Deque `values` accepts a direction or a `PositionQueryOptions`. Prosody applies each option in storage, so a query reads only the entries, members, or values it selects.
780
+
781
+ | Option | Effect |
782
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------- |
783
+ | `direction` | `"forward"` (the default) or `"backward"` |
784
+ | `from` / `after` | Start at a bound, or start after it. Set at most one. |
785
+ | `to` / `before` | Stop at a bound, or stop before it. Set at most one. |
786
+ | `range` | Keep keys or positions from `start` up to, but not including, `end`: `[start, end]`. Use `null` for an open end. |
787
+ | `prefix` | Keep keys that start with a prefix. Keys only. |
788
+ | `limit` | Return at most this many items. Use a positive integer. |
789
+
790
+ The edges `from`, `after`, `to`, and `before` are in query order, so a backward query starts at the high end. A `range` is ascending and applies in both directions, so `{ range: ["a", "m"], direction: "backward" }` yields the forward keys in the opposite order. A `null` bound leaves its end open, so `[2, null]` keeps position 2 and every later one. A range whose start is not below its end selects nothing. Bounds and `prefix` narrow the selection, so a query that sets a range and edges keeps their overlap. Deque positions count from the front and cannot be negative. To read the last N elements, use `values({ direction: "backward", limit: N })`.
744
791
 
745
- All operations are asynchronous. Map and deque scans are asynchronous iterables. A `for await` loop can stop early safely.
792
+ To page through a map or a set, pass the last key of the previous page as `after`:
746
793
 
747
- Map keys are strings. `null` and `undefined` mean absence. Do not store these values. Use `clear()` or `delete()`.
794
+ ```typescript
795
+ const ORDERS = map<Order>("orders");
796
+
797
+ async function orderPage(context: Context, after?: string) {
798
+ const page: [string, Order][] = [];
799
+ for await (const entry of context
800
+ .state(ORDERS)
801
+ .entries({ after, limit: 50 })) {
802
+ page.push(entry);
803
+ }
804
+ return page; // Pass page.at(-1)?.[0] as `after` for the next page.
805
+ }
806
+ ```
807
+
808
+ Set `direction: "backward"` to page from the highest key down.
809
+
810
+ Map keys are strings. `null` and `undefined` mean absence. Do not store these values. Prosody rejects a JSON `null` write with a `PermanentStateError`. Use `clear()` (value, deque) or `delete()` (map) to delete.
748
811
 
749
812
  ### When keyed-state changes become visible
750
813
 
@@ -756,6 +819,8 @@ This transaction applies only to keyed state. Some workflows need state changes
756
819
  - `commit()` commits the collection's pending changes before the handler ends. A later handler failure does not remove them.
757
820
  - `rollback()` discards pending changes since the last `commit()`. It cannot undo committed changes.
758
821
 
822
+ Both resolve to a `StoreOutcome`: `"applied"` when the call wrote or discarded pending changes, or `"noOp"` when the collection had no pending changes.
823
+
759
824
  ### Published state
760
825
 
761
826
  Some callers need only the current value for a key. They can accept a stale value or a race with a concurrent update.
@@ -777,7 +842,7 @@ const currentOrder = context.state(CURRENT_ORDER);
777
842
  await currentOrder.set({ sku: "book" });
778
843
  ```
779
844
 
780
- Read published state from a handler or other application code. The Prosody client does not need an active subscription.
845
+ Read published state from a handler or other application code. The Prosody client does not need an active subscription. A client that only reads published state does not need `subscribedTopics`.
781
846
 
782
847
  Use the subsystem and the same definition to open a reader:
783
848
 
@@ -786,11 +851,11 @@ const orderReader = await client.state("checkout", CURRENT_ORDER);
786
851
  const currentOrder = await orderReader.get("customer-123");
787
852
  ```
788
853
 
789
- The reader cannot see pending changes that exist only in a handler. It cannot change the collection. Each read takes an explicit key because no handler supplies one.
854
+ The reader cannot see pending changes that exist only in a handler. It cannot change the collection. Each read takes an explicit key because no handler supplies one. A failed read rejects with a `TransientStateError` or a `PermanentStateError`, like an owned read. A reader error at open, such as a zero `readCache` TTL, rejects `state()` the same way.
790
855
 
791
- Map and deque readers fetch data in chunks. They do not load the complete collection before iteration starts.
856
+ Map, set, and deque readers fetch data in chunks. They do not load the complete collection before iteration starts.
792
857
 
793
- The default cache window is five seconds. Set `readCache: { ttlMs }` to select a different window. Set `readCache: false` to bypass the cache.
858
+ The default cache window is five seconds. Set `readCache: { ttlMs }` to select a different window. Set `readCache: false` to bypass the cache. The client option `stateReadCache` sets the default for every reader and takes the same two forms.
794
859
 
795
860
  To stop publication, deploy the definition with `published: false`. Keep the definition registered during that deployment. Keep the subsystem configured during that deployment.
796
861
 
@@ -838,7 +903,7 @@ const tracer = opentelemetry.trace.getTracer("my-service-name");
838
903
  Set the following standard OpenTelemetry environment variables:
839
904
 
840
905
  ```
841
- OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
906
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
842
907
  OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
843
908
  OTEL_SERVICE_NAME=my-service-name
844
909
  ```
@@ -870,7 +935,7 @@ const messageHandler = {
870
935
  try {
871
936
  // Process the received message
872
937
  span.addEvent("message.received", {
873
- "message.payload": JSON.stringify(message),
938
+ "message.payload": JSON.stringify(message.payload),
874
939
  });
875
940
  } finally {
876
941
  span.end();
@@ -943,6 +1008,17 @@ Call `shutdown()` when the application terminates. It stops all client services
943
1008
  await client.shutdown();
944
1009
  ```
945
1010
 
1011
+ A client is `AsyncDisposable`. Declare it with `await using` to shut it down when the block ends, also when the block throws:
1012
+
1013
+ ```javascript
1014
+ {
1015
+ await using client = await ProsodyClient.create(config);
1016
+ await client.send("orders", "order-1", { type: "order.created" });
1017
+ } // The client shuts down here.
1018
+ ```
1019
+
1020
+ Concurrent and repeated shutdowns await the same operation, so an explicit `shutdown()` inside the block is safe.
1021
+
946
1022
  Handle application shutdown:
947
1023
 
948
1024
  ```javascript
@@ -1090,7 +1166,7 @@ your changes before merging to `main`.
1090
1166
  - `send<P>(topic: string, key: string, payload: P & JsonCompatible<P>, signal?: AbortSignal): Promise<void>`: Send a statically checked JSON-compatible message to a specified
1091
1167
  topic.
1092
1168
  - `excise(topic: string, key: string, signal?: AbortSignal): Promise<void>`: Send an excise record for a key.
1093
- - `request<R>(topic, key, payload: JsonValue, options): Promise<ReadonlyMap<string, Outcome<R>>>`: Return one outcome for each subsystem.
1169
+ - `request<R, P>(topic, key, payload: P, options): Promise<ReadonlyMap<string, Outcome<R>>>`: Return one outcome for each subsystem. `P` is inferred from the payload when you give no type arguments.
1094
1170
  - `requestExcise<R>(topic, key, options): Promise<ReadonlyMap<string, Outcome<R>>>`: Return one excise outcome for each subsystem.
1095
1171
  - `consumerState(): Promise<ConsumerState>`: Get the current state of the consumer.
1096
1172
  - `assignedPartitionCount(): Promise<number>`: Get the assigned partition count.
@@ -1098,15 +1174,17 @@ your changes before merging to `main`.
1098
1174
  - `sourceSystem: string`: Get the source system identifier configured for the client.
1099
1175
  - `state<T>(subsystem: string, definition: ValueDefinition<T>): Promise<PublishedValue<T>>`: Open a read-only published value.
1100
1176
  - `state<V>(subsystem: string, definition: MapDefinition<V>): Promise<PublishedMap<V>>`: Open a read-only published map.
1177
+ - `state(subsystem: string, definition: SetDefinition): Promise<PublishedSet>`: Open a read-only published set.
1101
1178
  - `state<T>(subsystem: string, definition: DequeDefinition<T>): Promise<PublishedDeque<T>>`: Open a read-only published deque.
1102
1179
  - `subscribe<P = JsonValue, R = JsonValue>(eventHandler: EventHandler<P, R>): Promise<void>`: Subscribe with typed payload and response values.
1103
1180
  - `unsubscribe(): Promise<void>`: Stop the consumer. You can subscribe again later.
1104
1181
  - `shutdown(): Promise<void>`: Stop all client services. Concurrent and repeated calls await the same operation.
1182
+ - `[Symbol.asyncDispose](): Promise<void>`: Call `shutdown()` when an `await using` block ends.
1105
1183
 
1106
1184
  ### AdminClient
1107
1185
 
1108
1186
  - `new AdminClient(bootstrapServers)`: Create an admin client for the specified Kafka servers.
1109
- - `createTopic(name, partitions, replicationFactor)`: Create a Kafka topic.
1187
+ - `createTopic(name, partitions, replicationFactor, options?)`: Create a Kafka topic. `options` takes `cleanupPolicy`, such as `"compact"`, and `retentionMs`. The cluster default applies to each setting you omit.
1110
1188
  - `deleteTopic(name)`: Delete a Kafka topic.
1111
1189
 
1112
1190
  ### EventHandler
@@ -1128,6 +1206,8 @@ Represents a Kafka message with the following properties:
1128
1206
  - `timestamp: Date`: The timestamp when the message was created or sent.
1129
1207
  - `key: string`: The message key.
1130
1208
  - `payload: P`: The statically typed message payload.
1209
+ - `sourceSystem: string | null`: The source system of the producer, or `null` when the message has no source system header.
1210
+ - `responseRequested: boolean`: Whether the sender waits for a response. The handler result becomes the response only when this is `true`.
1131
1211
 
1132
1212
  `Message` takes an optional payload type parameter, `Message<P>`, used by handlers and message-backed state collections to type `payload`. Unparameterized `Message` is `Message<JsonValue>`, preserving useful JSON safety without requiring an application-specific payload type.
1133
1213
 
@@ -1137,7 +1217,7 @@ Ordinary interfaces work with `send()`. TypeScript rejects functions, `undefined
1137
1217
 
1138
1218
  ### ExciseMessage
1139
1219
 
1140
- An `ExciseMessage` has `topic`, `partition`, `offset`, `timestamp`, and `key` properties. It has no `payload` property.
1220
+ An `ExciseMessage` has `topic`, `partition`, `offset`, `timestamp`, `key`, `sourceSystem`, and `responseRequested` properties. It has no `payload` property.
1141
1221
 
1142
1222
  ### Context
1143
1223
 
@@ -1145,6 +1225,7 @@ Represents the current event context:
1145
1225
 
1146
1226
  - `onCancel(): Promise<void>`: A method that resolves when the context is cancelled.
1147
1227
  - `shouldCancel: boolean`: A property indicating whether the context has been cancelled.
1228
+ - `demand: Demand`: Why the handler runs. `kind` is `"normal"` or `"failure"`. `retry` is the retry count: 0 for normal demand and 1 on the first retry. The count is an estimate. Keep an exact attempt count in keyed state if you need one.
1148
1229
 
1149
1230
  Timer scheduling methods:
1150
1231
 
@@ -1156,7 +1237,7 @@ Timer scheduling methods:
1156
1237
 
1157
1238
  Keyed-state binding:
1158
1239
 
1159
- - `state(definition): ValueState<T> | MapState<V> | DequeState<T>`: Bind a registered collection for the current attempt. Message definitions return handles that contain `Message<P>`. An unregistered or mismatched definition throws `PermanentStateError`. See [Keyed State](#keyed-state-2).
1240
+ - `state(definition): ValueState<T> | MapState<V> | SetState | DequeState<T>`: Bind a registered collection for the current attempt. Message definitions return handles that contain `Message<P>`. An unregistered or mismatched definition throws `PermanentStateError`. See [Keyed State](#keyed-state).
1160
1241
 
1161
1242
  ### Timer
1162
1243
 
@@ -1194,35 +1275,52 @@ Definition constructors (each returns a frozen definition object used both in `C
1194
1275
 
1195
1276
  - `value<T = JsonValue>(name: string, options?: PublishedStateDefinitionOptions): ValueDefinition<T>`
1196
1277
  - `map<V = JsonValue>(name: string, options?: MapDefinitionOptions): MapDefinition<V>`
1278
+ - `set(name: string, options?: SetDefinitionOptions): SetDefinition`
1197
1279
  - `deque<T = JsonValue>(name: string, options?: DequeDefinitionOptions): DequeDefinition<T>`
1198
1280
  - `messageValue<P = JsonValue>(name: string, options?: StateDefinitionOptions): MessageValueDefinition<P>`
1199
1281
  - `messageMap<P = JsonValue>(name: string, options?: MessageMapDefinitionOptions): MessageMapDefinition<P>`
1200
1282
  - `messageDeque<P = JsonValue>(name: string, options?: MessageDequeDefinitionOptions): MessageDequeDefinition<P>`
1201
1283
 
1202
- `StateDefinitionOptions`: `{ ttlSeconds?: number; readUncommitted?: boolean }`. `PublishedStateDefinitionOptions` adds `{ published?: boolean; readCache?: { ttlMs: number } | false }` for JSON definitions. Map and deque option types add `keysetLimit` and `capacity`, respectively; their message equivalents omit publication options.
1284
+ `StateDefinitionOptions`: `{ ttlSeconds?: number; readUncommitted?: boolean }`. `PublishedStateDefinitionOptions` adds `{ published?: boolean; readCache?: { ttlMs: number } | false }` for JSON definitions. Map and deque option types add `keysetLimit` and `capacity`, respectively; their message equivalents omit publication options. `SetDefinitionOptions` is the map option type: publication options and `keysetLimit`.
1203
1285
 
1204
1286
  `ValueState<T>`:
1205
1287
 
1206
1288
  - `get(): Promise<T | null>`
1207
1289
  - `set(value: T): Promise<void>`
1208
1290
  - `clear(): Promise<void>`
1209
- - `commit(): Promise<void>`
1210
- - `rollback(): Promise<void>`
1291
+ - `commit(): Promise<StoreOutcome>`
1292
+ - `rollback(): Promise<StoreOutcome>`
1211
1293
 
1212
1294
  `MapState<V>` (keys are `string`):
1213
1295
 
1214
1296
  - `get(key: string): Promise<V | null>`
1215
1297
  - `getMany(keys: readonly string[]): Promise<(V | null)[]>`
1216
1298
  - `has(key: string): Promise<boolean>`
1299
+ - `hasMany(keys: readonly string[]): Promise<boolean[]>`
1300
+ - `isEmpty(): Promise<boolean>`
1217
1301
  - `set(key: string, value: V): Promise<void>`
1218
1302
  - `delete(key: string): Promise<void>`
1219
1303
  - `clear(): Promise<void>`
1220
- - `entries(direction?: ScanDirection): AsyncIterableIterator<[string, V]>`
1221
- - `keys(direction?: ScanDirection): AsyncIterableIterator<string>`
1222
- - `values(direction?: ScanDirection): AsyncIterableIterator<V>`
1304
+ - `entries(options?: ScanDirection | KeyQueryOptions): AsyncIterableIterator<[string, V]>`
1305
+ - `keys(options?: ScanDirection | KeyQueryOptions): AsyncIterableIterator<string>`
1306
+ - `values(options?: ScanDirection | KeyQueryOptions): AsyncIterableIterator<V>`
1223
1307
  - `[Symbol.asyncIterator](): AsyncIterableIterator<[string, V]>`
1224
- - `commit(): Promise<void>`
1225
- - `rollback(): Promise<void>`
1308
+ - `commit(): Promise<StoreOutcome>`
1309
+ - `rollback(): Promise<StoreOutcome>`
1310
+
1311
+ `SetState` (members are `string`):
1312
+
1313
+ - `add(member: string): Promise<void>`
1314
+ - `has(member: string): Promise<boolean>`
1315
+ - `hasMany(members: readonly string[]): Promise<boolean[]>`
1316
+ - `delete(member: string): Promise<void>`
1317
+ - `clear(): Promise<void>`
1318
+ - `isEmpty(): Promise<boolean>`
1319
+ - `keys(options?: ScanDirection | KeyQueryOptions): AsyncIterableIterator<string>`
1320
+ - `values(options?: ScanDirection | KeyQueryOptions): AsyncIterableIterator<string>`
1321
+ - `[Symbol.asyncIterator](): AsyncIterableIterator<string>`
1322
+ - `commit(): Promise<StoreOutcome>`
1323
+ - `rollback(): Promise<StoreOutcome>`
1226
1324
 
1227
1325
  `DequeState<T>`:
1228
1326
 
@@ -1234,22 +1332,26 @@ Definition constructors (each returns a frozen definition object used both in `C
1234
1332
  - `isEmpty(): Promise<boolean>`
1235
1333
  - `clear(): Promise<void>`
1236
1334
  - `at(index: number): Promise<T | null>`
1237
- - `values(direction?: ScanDirection): AsyncIterableIterator<T>`
1335
+ - `values(options?: ScanDirection | PositionQueryOptions): AsyncIterableIterator<T>`
1238
1336
  - `[Symbol.asyncIterator](): AsyncIterableIterator<T>`
1239
- - `commit(): Promise<void>`
1240
- - `rollback(): Promise<void>`
1337
+ - `commit(): Promise<StoreOutcome>`
1338
+ - `rollback(): Promise<StoreOutcome>`
1241
1339
 
1242
1340
  `ScanDirection`: `"forward" | "backward"`.
1243
1341
 
1244
- Published readers take the user key as their first argument. `PublishedValue<T>` provides `get`. `PublishedMap<V>` provides `get`, `getMany`, `has`, `entries`, `keys`, and `values`. `PublishedDeque<T>` provides `at`, `length`, `isEmpty`, and `values`. The scan methods return `AsyncIterableIterator` directly.
1342
+ `StoreOutcome`: `"applied" | "noOp"`. `commit()` and `rollback()` resolve to it.
1343
+
1344
+ `KeyQueryOptions`: `{ direction?, prefix?, from? | after?, to? | before?, range?: [start, end], limit? }` with string bounds. A `null` range bound is open. `PositionQueryOptions`: the same without `prefix`, with non-negative integer positions. A `TypeError` reports a wrong option type, an unknown option or direction, both edges of a pair, or a range that is not an array of two bounds. A `RangeError` reports an invalid `limit` or position. See [Query a collection](#query-a-collection).
1345
+
1346
+ Published readers take the user key as their first argument. `PublishedValue<T>` provides `get`. `PublishedMap<V>` provides `get`, `getMany`, `has`, `hasMany`, `isEmpty`, `entries`, `keys`, and `values`. `PublishedSet` provides `has`, `hasMany`, `isEmpty`, `keys`, and `values`. `PublishedDeque<T>` provides `at`, `length`, `isEmpty`, and `values`. The scan methods return `AsyncIterableIterator` directly. They take the same query options as the handler handles.
1245
1347
 
1246
- `StateCollectionConfig` defines one `stateCollections` entry. It contains `name`, `kind`, `payload`, and the applicable collection options. Use a definition constructor to create this value.
1348
+ `StateCollectionConfig` defines one `stateCollections` entry. It contains `name`, `kind`, `payload` (except for a set), and the applicable collection options. Use a definition constructor to create this value.
1247
1349
 
1248
1350
  JSON definitions also accept `readCache`. This option applies when the definition opens published state. It is not part of `StateCollectionConfig`.
1249
1351
 
1250
- The public definition types are `ValueDefinition<T>`, `MapDefinition<V>`, `DequeDefinition<T>`, `MessageValueDefinition<P>`, `MessageMapDefinition<P>`, and `MessageDequeDefinition<P>`.
1352
+ The public definition types are `ValueDefinition<T>`, `MapDefinition<V>`, `SetDefinition`, `DequeDefinition<T>`, `MessageValueDefinition<P>`, `MessageMapDefinition<P>`, and `MessageDequeDefinition<P>`.
1251
1353
 
1252
- All definitions expose `name`, `kind`, `payload`, `ttlSeconds`, and `readUncommitted`. JSON definitions also expose `published` and `readCache`. Map definitions expose `keysetLimit`. Deque definitions expose `capacity`.
1354
+ All definitions expose `name`, `kind`, `ttlSeconds`, and `readUncommitted`. Every definition except a set exposes `payload`. JSON and set definitions also expose `published` and `readCache`. Map and set definitions expose `keysetLimit`. Deque definitions expose `capacity`.
1253
1355
 
1254
1356
  Errors:
1255
1357
 
@@ -1269,9 +1371,9 @@ Handler error types and decorators:
1269
1371
 
1270
1372
  - `Logger`: Provides `error`, `warn`, `info`, `debug`, and `trace` methods.
1271
1373
  - `initialize()`: Prepare the logging and tracing system during application startup.
1272
- - `loggerIsSet()`: Test whether the application configured a logger.
1374
+ - `loggerIsSet()`: Test whether the application set a logger. The default console logger does not count.
1273
1375
  - `setLogger(logger)`: Replaces the logger.
1274
- - `setLoggerIfUnset(logger)`: Sets the logger only when no logger exists.
1376
+ - `setLoggerIfUnset(logger)`: Sets the logger only when the application has not set one.
1275
1377
  - `getCurrentLogger()`: Returns the current JavaScript logger.
1276
1378
  - `flushTelemetry()`: Exports pending telemetry.
1277
1379
  - `shutdownTelemetry()`: Exports pending telemetry and stops its providers.