@prosody-events/prosody 0.5.1 → 0.6.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 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({
@@ -431,15 +431,20 @@ This prevents endless loops where a service consumes its own produced messages.
431
431
 
432
432
  ### Message Deduplication
433
433
 
434
- Prosody automatically deduplicates messages using the `id` field in their JSON payload. Consecutive messages with the
435
- same ID and key are processed only once.
434
+ Prosody uses the JSON payload's `id` field to detect duplicate messages. For a consumer group and idempotence version,
435
+ messages with the same ID, key, topic, and partition share a deduplication record. Other messages can arrive between duplicates.
436
+ Prosody skips a duplicate while its record remains in the cache or persistent store. Deduplication is best-effort;
437
+ handlers must remain idempotent.
436
438
 
437
439
  Deduplication uses a two-tier approach:
438
440
 
439
441
  - **Global in-memory cache**: A single cache shared across all partitions within the same consumer instance. Survives
440
442
  partition reassignments within the same process. Controlled by `idempotenceCacheSize` (default 8192).
441
443
  - **Cassandra-backed persistent store**: Survives restarts and rebalances across instances. TTL controlled by
442
- `idempotenceTtlS` (default 7 days, i.e. 604800s).
444
+ `idempotenceTtlSeconds` (default 7 days, i.e. 604800s).
445
+
446
+ 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
447
+ and key. `idempotenceCacheSize` sets the capacity of this cache too.
443
448
 
444
449
  Deduplication is always active. `idempotenceCacheSize` must be greater than `0`; a value of `0` (via either the option
445
450
  or `PROSODY_IDEMPOTENCE_CACHE_SIZE=0`) is rejected when the client is constructed.
@@ -571,6 +576,17 @@ default, all errors are considered transient.
571
576
 
572
577
  The error classes and decorators apply to `onMessage`, `onExcise`, and `onTimer`.
573
578
 
579
+ 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:
580
+
581
+ ```typescript
582
+ async onMessage(context, message) {
583
+ if (context.demand.kind === "failure") {
584
+ console.warn(`retry ${context.demand.retry} for ${message.key}`);
585
+ }
586
+ return null;
587
+ }
588
+ ```
589
+
574
590
  #### Using Decorators
575
591
 
576
592
  If you're using TypeScript or a JavaScript environment that supports decorators, you can use the `@permanent` decorator
@@ -579,11 +595,13 @@ to classify exceptions that should not be retried:
579
595
  ```javascript
580
596
  import { permanent, ProsodyClient } from "@prosody-events/prosody";
581
597
 
598
+ class ValidationError extends Error {}
599
+
582
600
  class MyHandler {
583
- @permanent(TypeError, AttributeError)
601
+ @permanent(TypeError, ValidationError)
584
602
  async onMessage(context, message, signal) {
585
603
  // Your message handling logic here
586
- // TypeError and AttributeError will be treated as permanent
604
+ // TypeError and ValidationError will be treated as permanent
587
605
  // All other exceptions will be treated as transient (default behavior)
588
606
  return null;
589
607
  }
@@ -606,13 +624,15 @@ If you're not using decorators, you can still classify errors as permanent by th
606
624
  ```javascript
607
625
  import { PermanentError, ProsodyClient } from "@prosody-events/prosody";
608
626
 
627
+ class ValidationError extends Error {}
628
+
609
629
  const messageHandler = {
610
630
  onMessage: async (context, message, signal) => {
611
631
  try {
612
632
  // Your message handling logic here
613
633
  } catch (error) {
614
- if (error instanceof TypeError || error instanceof AttributeError) {
615
- throw new PermanentError(error.message);
634
+ if (error instanceof TypeError || error instanceof ValidationError) {
635
+ throw new PermanentError(error.message, { cause: error });
616
636
  }
617
637
  // All other exceptions will be treated as transient (default behavior)
618
638
  throw error;
@@ -736,15 +756,60 @@ A definition sets a collection's durable name, kind, and options. Register it on
736
756
 
737
757
  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
758
 
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` |
759
+ | Collection | JSON payload | Kafka message | Main operations |
760
+ | ------------------ | ------------ | ----------------- | ------------------------------------------------------------------------------------------ |
761
+ | Value | `value<T>` | `messageValue<P>` | `get`, `set`, `clear` |
762
+ | Ordered string map | `map<V>` | `messageMap<P>` | `get`, `getMany`, `has`, `hasMany`, `isEmpty`, `set`, `delete`, `entries`, `keys`, `clear` |
763
+ | Deque | `deque<T>` | `messageDeque<P>` | `push`, `unshift`, `pop`, `shift`, `at`, `length`, `values`, `clear` |
764
+ | Ordered string set | `set` | (none) | `add`, `has`, `hasMany`, `delete`, `isEmpty`, `keys`, `values`, `clear` |
765
+
766
+ All operations are asynchronous. Map, set, and deque scans are asynchronous iterables. A `for await` loop can stop early safely.
767
+
768
+ 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:
769
+
770
+ ```typescript
771
+ const SEEN = set("seen-orders", { ttlSeconds: 7 * 24 * 60 * 60 });
772
+
773
+ // In a handler:
774
+ const seen = context.state(SEEN);
775
+ if (await seen.has(message.payload.orderId)) return null;
776
+ await seen.add(message.payload.orderId);
777
+ ```
778
+
779
+ ### Query a collection
780
+
781
+ 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.
782
+
783
+ | Option | Effect |
784
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------- |
785
+ | `direction` | `"forward"` (the default) or `"backward"` |
786
+ | `from` / `after` | Start at a bound, or start after it. Set at most one. |
787
+ | `to` / `before` | Stop at a bound, or stop before it. Set at most one. |
788
+ | `range` | Keep keys or positions from `start` up to, but not including, `end`: `[start, end]`. Use `null` for an open end. |
789
+ | `prefix` | Keep keys that start with a prefix. Keys only. |
790
+ | `limit` | Return at most this many items. Use a positive integer. |
791
+
792
+ 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
793
 
745
- All operations are asynchronous. Map and deque scans are asynchronous iterables. A `for await` loop can stop early safely.
794
+ To page through a map or a set, pass the last key of the previous page as `after`:
746
795
 
747
- Map keys are strings. `null` and `undefined` mean absence. Do not store these values. Use `clear()` or `delete()`.
796
+ ```typescript
797
+ const ORDERS = map<Order>("orders");
798
+
799
+ async function orderPage(context: Context, after?: string) {
800
+ const page: [string, Order][] = [];
801
+ for await (const entry of context
802
+ .state(ORDERS)
803
+ .entries({ after, limit: 50 })) {
804
+ page.push(entry);
805
+ }
806
+ return page; // Pass page.at(-1)?.[0] as `after` for the next page.
807
+ }
808
+ ```
809
+
810
+ Set `direction: "backward"` to page from the highest key down.
811
+
812
+ 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
813
 
749
814
  ### When keyed-state changes become visible
750
815
 
@@ -756,6 +821,8 @@ This transaction applies only to keyed state. Some workflows need state changes
756
821
  - `commit()` commits the collection's pending changes before the handler ends. A later handler failure does not remove them.
757
822
  - `rollback()` discards pending changes since the last `commit()`. It cannot undo committed changes.
758
823
 
824
+ Both resolve to a `StoreOutcome`: `"applied"` when the call wrote or discarded pending changes, or `"noOp"` when the collection had no pending changes.
825
+
759
826
  ### Published state
760
827
 
761
828
  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 +844,7 @@ const currentOrder = context.state(CURRENT_ORDER);
777
844
  await currentOrder.set({ sku: "book" });
778
845
  ```
779
846
 
780
- Read published state from a handler or other application code. The Prosody client does not need an active subscription.
847
+ 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
848
 
782
849
  Use the subsystem and the same definition to open a reader:
783
850
 
@@ -786,11 +853,11 @@ const orderReader = await client.state("checkout", CURRENT_ORDER);
786
853
  const currentOrder = await orderReader.get("customer-123");
787
854
  ```
788
855
 
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.
856
+ 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
857
 
791
- Map and deque readers fetch data in chunks. They do not load the complete collection before iteration starts.
858
+ Map, set, and deque readers fetch data in chunks. They do not load the complete collection before iteration starts.
792
859
 
793
- The default cache window is five seconds. Set `readCache: { ttlMs }` to select a different window. Set `readCache: false` to bypass the cache.
860
+ 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
861
 
795
862
  To stop publication, deploy the definition with `published: false`. Keep the definition registered during that deployment. Keep the subsystem configured during that deployment.
796
863
 
@@ -838,7 +905,7 @@ const tracer = opentelemetry.trace.getTracer("my-service-name");
838
905
  Set the following standard OpenTelemetry environment variables:
839
906
 
840
907
  ```
841
- OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
908
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
842
909
  OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
843
910
  OTEL_SERVICE_NAME=my-service-name
844
911
  ```
@@ -870,7 +937,7 @@ const messageHandler = {
870
937
  try {
871
938
  // Process the received message
872
939
  span.addEvent("message.received", {
873
- "message.payload": JSON.stringify(message),
940
+ "message.payload": JSON.stringify(message.payload),
874
941
  });
875
942
  } finally {
876
943
  span.end();
@@ -943,6 +1010,17 @@ Call `shutdown()` when the application terminates. It stops all client services
943
1010
  await client.shutdown();
944
1011
  ```
945
1012
 
1013
+ A client is `AsyncDisposable`. Declare it with `await using` to shut it down when the block ends, also when the block throws:
1014
+
1015
+ ```javascript
1016
+ {
1017
+ await using client = await ProsodyClient.create(config);
1018
+ await client.send("orders", "order-1", { type: "order.created" });
1019
+ } // The client shuts down here.
1020
+ ```
1021
+
1022
+ Concurrent and repeated shutdowns await the same operation, so an explicit `shutdown()` inside the block is safe.
1023
+
946
1024
  Handle application shutdown:
947
1025
 
948
1026
  ```javascript
@@ -1090,7 +1168,7 @@ your changes before merging to `main`.
1090
1168
  - `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
1169
  topic.
1092
1170
  - `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.
1171
+ - `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
1172
  - `requestExcise<R>(topic, key, options): Promise<ReadonlyMap<string, Outcome<R>>>`: Return one excise outcome for each subsystem.
1095
1173
  - `consumerState(): Promise<ConsumerState>`: Get the current state of the consumer.
1096
1174
  - `assignedPartitionCount(): Promise<number>`: Get the assigned partition count.
@@ -1098,15 +1176,17 @@ your changes before merging to `main`.
1098
1176
  - `sourceSystem: string`: Get the source system identifier configured for the client.
1099
1177
  - `state<T>(subsystem: string, definition: ValueDefinition<T>): Promise<PublishedValue<T>>`: Open a read-only published value.
1100
1178
  - `state<V>(subsystem: string, definition: MapDefinition<V>): Promise<PublishedMap<V>>`: Open a read-only published map.
1179
+ - `state(subsystem: string, definition: SetDefinition): Promise<PublishedSet>`: Open a read-only published set.
1101
1180
  - `state<T>(subsystem: string, definition: DequeDefinition<T>): Promise<PublishedDeque<T>>`: Open a read-only published deque.
1102
1181
  - `subscribe<P = JsonValue, R = JsonValue>(eventHandler: EventHandler<P, R>): Promise<void>`: Subscribe with typed payload and response values.
1103
1182
  - `unsubscribe(): Promise<void>`: Stop the consumer. You can subscribe again later.
1104
1183
  - `shutdown(): Promise<void>`: Stop all client services. Concurrent and repeated calls await the same operation.
1184
+ - `[Symbol.asyncDispose](): Promise<void>`: Call `shutdown()` when an `await using` block ends.
1105
1185
 
1106
1186
  ### AdminClient
1107
1187
 
1108
1188
  - `new AdminClient(bootstrapServers)`: Create an admin client for the specified Kafka servers.
1109
- - `createTopic(name, partitions, replicationFactor)`: Create a Kafka topic.
1189
+ - `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
1190
  - `deleteTopic(name)`: Delete a Kafka topic.
1111
1191
 
1112
1192
  ### EventHandler
@@ -1128,6 +1208,8 @@ Represents a Kafka message with the following properties:
1128
1208
  - `timestamp: Date`: The timestamp when the message was created or sent.
1129
1209
  - `key: string`: The message key.
1130
1210
  - `payload: P`: The statically typed message payload.
1211
+ - `sourceSystem: string | null`: The source system of the producer, or `null` when the message has no source system header.
1212
+ - `responseRequested: boolean`: Whether the sender waits for a response. The handler result becomes the response only when this is `true`.
1131
1213
 
1132
1214
  `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
1215
 
@@ -1137,7 +1219,7 @@ Ordinary interfaces work with `send()`. TypeScript rejects functions, `undefined
1137
1219
 
1138
1220
  ### ExciseMessage
1139
1221
 
1140
- An `ExciseMessage` has `topic`, `partition`, `offset`, `timestamp`, and `key` properties. It has no `payload` property.
1222
+ An `ExciseMessage` has `topic`, `partition`, `offset`, `timestamp`, `key`, `sourceSystem`, and `responseRequested` properties. It has no `payload` property.
1141
1223
 
1142
1224
  ### Context
1143
1225
 
@@ -1145,6 +1227,7 @@ Represents the current event context:
1145
1227
 
1146
1228
  - `onCancel(): Promise<void>`: A method that resolves when the context is cancelled.
1147
1229
  - `shouldCancel: boolean`: A property indicating whether the context has been cancelled.
1230
+ - `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
1231
 
1149
1232
  Timer scheduling methods:
1150
1233
 
@@ -1156,7 +1239,7 @@ Timer scheduling methods:
1156
1239
 
1157
1240
  Keyed-state binding:
1158
1241
 
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).
1242
+ - `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
1243
 
1161
1244
  ### Timer
1162
1245
 
@@ -1194,35 +1277,52 @@ Definition constructors (each returns a frozen definition object used both in `C
1194
1277
 
1195
1278
  - `value<T = JsonValue>(name: string, options?: PublishedStateDefinitionOptions): ValueDefinition<T>`
1196
1279
  - `map<V = JsonValue>(name: string, options?: MapDefinitionOptions): MapDefinition<V>`
1280
+ - `set(name: string, options?: SetDefinitionOptions): SetDefinition`
1197
1281
  - `deque<T = JsonValue>(name: string, options?: DequeDefinitionOptions): DequeDefinition<T>`
1198
1282
  - `messageValue<P = JsonValue>(name: string, options?: StateDefinitionOptions): MessageValueDefinition<P>`
1199
1283
  - `messageMap<P = JsonValue>(name: string, options?: MessageMapDefinitionOptions): MessageMapDefinition<P>`
1200
1284
  - `messageDeque<P = JsonValue>(name: string, options?: MessageDequeDefinitionOptions): MessageDequeDefinition<P>`
1201
1285
 
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.
1286
+ `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
1287
 
1204
1288
  `ValueState<T>`:
1205
1289
 
1206
1290
  - `get(): Promise<T | null>`
1207
1291
  - `set(value: T): Promise<void>`
1208
1292
  - `clear(): Promise<void>`
1209
- - `commit(): Promise<void>`
1210
- - `rollback(): Promise<void>`
1293
+ - `commit(): Promise<StoreOutcome>`
1294
+ - `rollback(): Promise<StoreOutcome>`
1211
1295
 
1212
1296
  `MapState<V>` (keys are `string`):
1213
1297
 
1214
1298
  - `get(key: string): Promise<V | null>`
1215
1299
  - `getMany(keys: readonly string[]): Promise<(V | null)[]>`
1216
1300
  - `has(key: string): Promise<boolean>`
1301
+ - `hasMany(keys: readonly string[]): Promise<boolean[]>`
1302
+ - `isEmpty(): Promise<boolean>`
1217
1303
  - `set(key: string, value: V): Promise<void>`
1218
1304
  - `delete(key: string): Promise<void>`
1219
1305
  - `clear(): Promise<void>`
1220
- - `entries(direction?: ScanDirection): AsyncIterableIterator<[string, V]>`
1221
- - `keys(direction?: ScanDirection): AsyncIterableIterator<string>`
1222
- - `values(direction?: ScanDirection): AsyncIterableIterator<V>`
1306
+ - `entries(options?: ScanDirection | KeyQueryOptions): AsyncIterableIterator<[string, V]>`
1307
+ - `keys(options?: ScanDirection | KeyQueryOptions): AsyncIterableIterator<string>`
1308
+ - `values(options?: ScanDirection | KeyQueryOptions): AsyncIterableIterator<V>`
1223
1309
  - `[Symbol.asyncIterator](): AsyncIterableIterator<[string, V]>`
1224
- - `commit(): Promise<void>`
1225
- - `rollback(): Promise<void>`
1310
+ - `commit(): Promise<StoreOutcome>`
1311
+ - `rollback(): Promise<StoreOutcome>`
1312
+
1313
+ `SetState` (members are `string`):
1314
+
1315
+ - `add(member: string): Promise<void>`
1316
+ - `has(member: string): Promise<boolean>`
1317
+ - `hasMany(members: readonly string[]): Promise<boolean[]>`
1318
+ - `delete(member: string): Promise<void>`
1319
+ - `clear(): Promise<void>`
1320
+ - `isEmpty(): Promise<boolean>`
1321
+ - `keys(options?: ScanDirection | KeyQueryOptions): AsyncIterableIterator<string>`
1322
+ - `values(options?: ScanDirection | KeyQueryOptions): AsyncIterableIterator<string>`
1323
+ - `[Symbol.asyncIterator](): AsyncIterableIterator<string>`
1324
+ - `commit(): Promise<StoreOutcome>`
1325
+ - `rollback(): Promise<StoreOutcome>`
1226
1326
 
1227
1327
  `DequeState<T>`:
1228
1328
 
@@ -1234,22 +1334,26 @@ Definition constructors (each returns a frozen definition object used both in `C
1234
1334
  - `isEmpty(): Promise<boolean>`
1235
1335
  - `clear(): Promise<void>`
1236
1336
  - `at(index: number): Promise<T | null>`
1237
- - `values(direction?: ScanDirection): AsyncIterableIterator<T>`
1337
+ - `values(options?: ScanDirection | PositionQueryOptions): AsyncIterableIterator<T>`
1238
1338
  - `[Symbol.asyncIterator](): AsyncIterableIterator<T>`
1239
- - `commit(): Promise<void>`
1240
- - `rollback(): Promise<void>`
1339
+ - `commit(): Promise<StoreOutcome>`
1340
+ - `rollback(): Promise<StoreOutcome>`
1241
1341
 
1242
1342
  `ScanDirection`: `"forward" | "backward"`.
1243
1343
 
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.
1344
+ `StoreOutcome`: `"applied" | "noOp"`. `commit()` and `rollback()` resolve to it.
1345
+
1346
+ `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).
1347
+
1348
+ 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
1349
 
1246
- `StateCollectionConfig` defines one `stateCollections` entry. It contains `name`, `kind`, `payload`, and the applicable collection options. Use a definition constructor to create this value.
1350
+ `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
1351
 
1248
1352
  JSON definitions also accept `readCache`. This option applies when the definition opens published state. It is not part of `StateCollectionConfig`.
1249
1353
 
1250
- The public definition types are `ValueDefinition<T>`, `MapDefinition<V>`, `DequeDefinition<T>`, `MessageValueDefinition<P>`, `MessageMapDefinition<P>`, and `MessageDequeDefinition<P>`.
1354
+ The public definition types are `ValueDefinition<T>`, `MapDefinition<V>`, `SetDefinition`, `DequeDefinition<T>`, `MessageValueDefinition<P>`, `MessageMapDefinition<P>`, and `MessageDequeDefinition<P>`.
1251
1355
 
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`.
1356
+ 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
1357
 
1254
1358
  Errors:
1255
1359
 
@@ -1269,9 +1373,9 @@ Handler error types and decorators:
1269
1373
 
1270
1374
  - `Logger`: Provides `error`, `warn`, `info`, `debug`, and `trace` methods.
1271
1375
  - `initialize()`: Prepare the logging and tracing system during application startup.
1272
- - `loggerIsSet()`: Test whether the application configured a logger.
1376
+ - `loggerIsSet()`: Test whether the application set a logger. The default console logger does not count.
1273
1377
  - `setLogger(logger)`: Replaces the logger.
1274
- - `setLoggerIfUnset(logger)`: Sets the logger only when no logger exists.
1378
+ - `setLoggerIfUnset(logger)`: Sets the logger only when the application has not set one.
1275
1379
  - `getCurrentLogger()`: Returns the current JavaScript logger.
1276
1380
  - `flushTelemetry()`: Exports pending telemetry.
1277
1381
  - `shutdownTelemetry()`: Exports pending telemetry and stops its providers.