@procedures/client 0.7.0 → 0.8.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 (104) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +20 -3
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/adapters/http/server/transport.d.ts +6 -0
  5. package/dist/adapters/http/server/transport.d.ts.map +1 -1
  6. package/dist/adapters/http/server/transport.js +10 -1
  7. package/dist/adapters/http/server/transport.js.map +1 -1
  8. package/dist/adapters/websocket/client/transport.d.ts +7 -3
  9. package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
  10. package/dist/adapters/websocket/client/transport.js +9 -3
  11. package/dist/adapters/websocket/client/transport.js.map +1 -1
  12. package/dist/adapters/websocket/server/transport.d.ts +10 -0
  13. package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
  14. package/dist/adapters/websocket/server/transport.js +22 -2
  15. package/dist/adapters/websocket/server/transport.js.map +1 -1
  16. package/dist/browser.d.ts +4 -4
  17. package/dist/browser.d.ts.map +1 -1
  18. package/dist/browser.js +1 -1
  19. package/dist/browser.js.map +1 -1
  20. package/dist/client/errors/registry.d.ts.map +1 -1
  21. package/dist/client/errors/registry.js +1 -0
  22. package/dist/client/errors/registry.js.map +1 -1
  23. package/dist/client/middleware/retry.d.ts +4 -0
  24. package/dist/client/middleware/retry.d.ts.map +1 -1
  25. package/dist/client/middleware/retry.js +86 -78
  26. package/dist/client/middleware/retry.js.map +1 -1
  27. package/dist/events/bus.d.ts +32 -14
  28. package/dist/events/bus.d.ts.map +1 -1
  29. package/dist/events/bus.js +119 -65
  30. package/dist/events/bus.js.map +1 -1
  31. package/dist/events/index.d.ts +3 -2
  32. package/dist/events/index.d.ts.map +1 -1
  33. package/dist/events/index.js.map +1 -1
  34. package/dist/events/publisher.d.ts.map +1 -1
  35. package/dist/events/publisher.js +20 -6
  36. package/dist/events/publisher.js.map +1 -1
  37. package/dist/events/remote.d.ts +80 -18
  38. package/dist/events/remote.d.ts.map +1 -1
  39. package/dist/events/remote.js +448 -58
  40. package/dist/events/remote.js.map +1 -1
  41. package/dist/events/types.d.ts +13 -2
  42. package/dist/events/types.d.ts.map +1 -1
  43. package/dist/procedures/caller.d.ts +33 -0
  44. package/dist/procedures/caller.d.ts.map +1 -0
  45. package/dist/procedures/caller.js +27 -0
  46. package/dist/procedures/caller.js.map +1 -0
  47. package/dist/procedures/collection/index.d.ts +2 -2
  48. package/dist/procedures/collection/index.d.ts.map +1 -1
  49. package/dist/procedures/collection/index.js +1 -1
  50. package/dist/procedures/collection/index.js.map +1 -1
  51. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  52. package/dist/procedures/collection/procedures.js +21 -1
  53. package/dist/procedures/collection/procedures.js.map +1 -1
  54. package/dist/procedures/collection/schemas.d.ts +9 -0
  55. package/dist/procedures/collection/schemas.d.ts.map +1 -1
  56. package/dist/procedures/collection/schemas.js +28 -0
  57. package/dist/procedures/collection/schemas.js.map +1 -1
  58. package/dist/procedures/collection/storage.d.ts +6 -0
  59. package/dist/procedures/collection/storage.d.ts.map +1 -1
  60. package/dist/procedures/index.d.ts +3 -1
  61. package/dist/procedures/index.d.ts.map +1 -1
  62. package/dist/procedures/index.js +2 -0
  63. package/dist/procedures/index.js.map +1 -1
  64. package/dist/procedures/invoke.d.ts.map +1 -1
  65. package/dist/procedures/invoke.js +3 -0
  66. package/dist/procedures/invoke.js.map +1 -1
  67. package/dist/procedures/types.d.ts +50 -0
  68. package/dist/procedures/types.d.ts.map +1 -1
  69. package/dist/procedures/types.js.map +1 -1
  70. package/dist/protocol.d.ts +1 -1
  71. package/dist/protocol.d.ts.map +1 -1
  72. package/dist/protocol.js +1 -0
  73. package/dist/protocol.js.map +1 -1
  74. package/dist/server/procedure-server.d.ts +37 -3
  75. package/dist/server/procedure-server.d.ts.map +1 -1
  76. package/dist/server/procedure-server.js +86 -4
  77. package/dist/server/procedure-server.js.map +1 -1
  78. package/dist/timers.d.ts +5 -0
  79. package/dist/timers.d.ts.map +1 -1
  80. package/dist/timers.js +3 -1
  81. package/dist/timers.js.map +1 -1
  82. package/package.json +1 -1
  83. package/src/adapters/http/server/transport.ts +11 -1
  84. package/src/adapters/websocket/client/transport.ts +9 -3
  85. package/src/adapters/websocket/server/transport.ts +23 -2
  86. package/src/browser.ts +6 -0
  87. package/src/client/errors/registry.ts +1 -0
  88. package/src/client/middleware/retry.ts +89 -78
  89. package/src/events/bus.ts +141 -73
  90. package/src/events/index.ts +3 -0
  91. package/src/events/publisher.ts +23 -6
  92. package/src/events/remote.ts +575 -73
  93. package/src/events/types.ts +15 -2
  94. package/src/procedures/caller.ts +45 -0
  95. package/src/procedures/collection/index.ts +3 -0
  96. package/src/procedures/collection/procedures.ts +22 -0
  97. package/src/procedures/collection/schemas.ts +37 -0
  98. package/src/procedures/collection/storage.ts +6 -0
  99. package/src/procedures/index.ts +4 -0
  100. package/src/procedures/invoke.ts +2 -0
  101. package/src/procedures/types.ts +53 -0
  102. package/src/protocol.ts +3 -1
  103. package/src/server/procedure-server.ts +103 -6
  104. package/src/timers.ts +8 -1
package/src/events/bus.ts CHANGED
@@ -11,8 +11,11 @@ import type {
11
11
  EventHandler,
12
12
  Unsubscribe,
13
13
  EventBusOptions,
14
+ EventListenOptions,
14
15
  EventWaitOptions,
15
16
  } from "./types.js";
17
+ import { realClock, type Clock } from "../clock.js";
18
+ import { toMs, type Duration } from "../duration.js";
16
19
 
17
20
  /** The error of a wait that its signal ended. */
18
21
  function abortedError(channel: string): Error {
@@ -21,6 +24,16 @@ function abortedError(channel: string): Error {
21
24
  return error;
22
25
  }
23
26
 
27
+ /**
28
+ * The error of a wait that `clear()` ended. Its name is `AbortError`, so code that ignores a
29
+ * cancelled wait ignores this one too.
30
+ */
31
+ function clearedError(channel: string): Error {
32
+ const error = new Error(`The wait on channel "${channel}" ended, because clear() removed the channel`);
33
+ error.name = "AbortError";
34
+ return error;
35
+ }
36
+
24
37
  // =============================================================================
25
38
  // Event Bus Implementation
26
39
  // =============================================================================
@@ -32,8 +45,11 @@ function abortedError(channel: string): Error {
32
45
  export class DefaultEventBus implements EventBus {
33
46
  private readonly subscribers = new Map<string, Set<EventHandler<unknown>>>();
34
47
  private readonly buffer = new Map<string, unknown[]>();
35
- /** For each channel, a function per open stream that ends it (for `clear()`). */
36
- private readonly streamEnders = new Map<string, Set<() => void>>();
48
+ /**
49
+ * For each channel, the functions that `clear()` runs: the `onClear` of each handler of `on()`
50
+ * that has one. Each open stream has one too, so `clear()` ends it.
51
+ */
52
+ private readonly clearHandlers = new Map<string, Set<() => void>>();
37
53
  private readonly options: Required<EventBusOptions>;
38
54
 
39
55
  constructor(options: EventBusOptions = {}) {
@@ -85,9 +101,10 @@ export class DefaultEventBus implements EventBus {
85
101
  }
86
102
 
87
103
  /**
88
- * Subscribe to events on a channel.
104
+ * Subscribe to events on a channel. The `onClear` of the options runs one time when `clear()`
105
+ * or `clearAll()` removes the handler, not at an unsubscribe.
89
106
  */
90
- on<T>(channel: string, handler: EventHandler<T>): Unsubscribe {
107
+ on<T>(channel: string, handler: EventHandler<T>, options: EventListenOptions = {}): Unsubscribe {
91
108
  if (this.options.debug) {
92
109
  console.log(`[EventBus] subscribe "${channel}"`);
93
110
  }
@@ -100,6 +117,30 @@ export class DefaultEventBus implements EventBus {
100
117
 
101
118
  channelSubs.add(handler as EventHandler<unknown>);
102
119
 
120
+ // The unsubscribe function works once, and removes only the set it added to. (Before, a
121
+ // repeated call of an old unsubscribe removed the set of a newer subscriber: deep dive CORE-15.)
122
+ // A clear() also ends the handler: then its unsubscribe does nothing.
123
+ let subscribed = true;
124
+ const { onClear } = options;
125
+ let cleared: (() => void) | undefined;
126
+ if (onClear) {
127
+ cleared = () => {
128
+ if (!subscribed) return;
129
+ subscribed = false;
130
+ try {
131
+ onClear();
132
+ } catch (error) {
133
+ this.options.onError(error instanceof Error ? error : new Error(String(error)), channel);
134
+ }
135
+ };
136
+ let handlers = this.clearHandlers.get(channel);
137
+ if (!handlers) {
138
+ handlers = new Set();
139
+ this.clearHandlers.set(channel, handlers);
140
+ }
141
+ handlers.add(cleared);
142
+ }
143
+
103
144
  // Replay buffered events if any
104
145
  const buffered = this.buffer.get(channel);
105
146
  if (buffered && buffered.length > 0) {
@@ -115,9 +156,6 @@ export class DefaultEventBus implements EventBus {
115
156
  }
116
157
  }
117
158
 
118
- // The unsubscribe function works once, and removes only the set it added to. (Before, a
119
- // repeated call of an old unsubscribe removed the set of a newer subscriber: deep dive CORE-15.)
120
- let subscribed = true;
121
159
  return () => {
122
160
  if (!subscribed) return;
123
161
  subscribed = false;
@@ -130,9 +168,23 @@ export class DefaultEventBus implements EventBus {
130
168
  if (channelSubs.size === 0 && this.subscribers.get(channel) === channelSubs) {
131
169
  this.subscribers.delete(channel);
132
170
  }
171
+ if (cleared) {
172
+ const handlers = this.clearHandlers.get(channel);
173
+ if (handlers?.delete(cleared) && handlers.size === 0) this.clearHandlers.delete(channel);
174
+ }
133
175
  };
134
176
  }
135
177
 
178
+ /** This function runs the clear handlers of the channels, after it removed them from the bus. */
179
+ private runClearHandlers(channels: readonly string[]): void {
180
+ const handlers: Array<() => void> = [];
181
+ for (const channel of channels) {
182
+ handlers.push(...(this.clearHandlers.get(channel) ?? []));
183
+ this.clearHandlers.delete(channel);
184
+ }
185
+ for (const cleared of handlers) cleared();
186
+ }
187
+
136
188
  /**
137
189
  * Subscribe to a single event on a channel. On a buffered channel, the first buffered event
138
190
  * resolves the promise. When the signal aborts first, the promise rejects with an `AbortError`.
@@ -153,14 +205,26 @@ export class DefaultEventBus implements EventBus {
153
205
  reject(abortedError(channel));
154
206
  };
155
207
  // on() can replay a buffered event before it returns: the handler must not need
156
- // `unsubscribe` yet (before, it threw a ReferenceError, and the promise never settled)
157
- unsubscribe = this.on<T>(channel, (data) => {
158
- if (settled) return;
159
- settled = true;
160
- unsubscribe?.();
161
- signal?.removeEventListener("abort", onAbort);
162
- resolve(data);
163
- });
208
+ // `unsubscribe` yet (before, it threw a ReferenceError, and the promise never settled).
209
+ // clear() rejects the wait: before, the promise never settled.
210
+ unsubscribe = this.on<T>(
211
+ channel,
212
+ (data) => {
213
+ if (settled) return;
214
+ settled = true;
215
+ unsubscribe?.();
216
+ signal?.removeEventListener("abort", onAbort);
217
+ resolve(data);
218
+ },
219
+ {
220
+ onClear: () => {
221
+ if (settled) return;
222
+ settled = true;
223
+ signal?.removeEventListener("abort", onAbort);
224
+ reject(clearedError(channel));
225
+ },
226
+ },
227
+ );
164
228
  if (settled) {
165
229
  unsubscribe();
166
230
  return;
@@ -182,23 +246,7 @@ export class DefaultEventBus implements EventBus {
182
246
  const queue: T[] = [];
183
247
  let resolve: ((value: IteratorResult<T>) => void) | null = null;
184
248
  let done = signal?.aborted ?? false;
185
-
186
- // Subscribe to channel
187
- const unsubscribe = done
188
- ? () => {}
189
- : self.on<T>(channel, (data) => {
190
- if (done) return;
191
-
192
- if (resolve) {
193
- // Someone is waiting, resolve immediately
194
- const r = resolve;
195
- resolve = null;
196
- r({ value: data, done: false });
197
- } else {
198
- // Queue for later
199
- queue.push(data);
200
- }
201
- });
249
+ let unsubscribe: Unsubscribe = () => {};
202
250
 
203
251
  // End the stream: no more events, and a waiting reader gets `done`
204
252
  const end = (): void => {
@@ -206,19 +254,31 @@ export class DefaultEventBus implements EventBus {
206
254
  done = true;
207
255
  unsubscribe();
208
256
  signal?.removeEventListener("abort", end);
209
- self.streamEnders.get(channel)?.delete(end);
210
257
  const r = resolve;
211
258
  resolve = null;
212
259
  r?.({ value: undefined, done: true });
213
260
  };
261
+
262
+ // Subscribe to channel. A clear() of the channel ends the stream.
214
263
  if (!done) {
264
+ unsubscribe = self.on<T>(
265
+ channel,
266
+ (data) => {
267
+ if (done) return;
268
+
269
+ if (resolve) {
270
+ // Someone is waiting, resolve immediately
271
+ const r = resolve;
272
+ resolve = null;
273
+ r({ value: data, done: false });
274
+ } else {
275
+ // Queue for later
276
+ queue.push(data);
277
+ }
278
+ },
279
+ { onClear: end },
280
+ );
215
281
  signal?.addEventListener("abort", end, { once: true });
216
- let enders = self.streamEnders.get(channel);
217
- if (!enders) {
218
- enders = new Set();
219
- self.streamEnders.set(channel, enders);
220
- }
221
- enders.add(end);
222
282
  }
223
283
 
224
284
  return {
@@ -276,7 +336,8 @@ export class DefaultEventBus implements EventBus {
276
336
  }
277
337
 
278
338
  /**
279
- * Remove all subscribers from a channel.
339
+ * Remove all subscribers from a channel. The `onClear` of each removed handler runs, and each
340
+ * stream of the channel ends.
280
341
  */
281
342
  clear(channel: string): void {
282
343
  if (this.options.debug) {
@@ -284,13 +345,14 @@ export class DefaultEventBus implements EventBus {
284
345
  }
285
346
  this.subscribers.delete(channel);
286
347
  this.buffer.delete(channel);
287
- // The streams of the channel end: before, their readers waited forever (deep dive CORE-15)
288
- for (const end of [...(this.streamEnders.get(channel) ?? [])]) end();
289
- this.streamEnders.delete(channel);
348
+ // The streams of the channel end: before, their readers waited forever (deep dive CORE-15).
349
+ // The remote subscriptions end with notice too (EVENTS.md).
350
+ this.runClearHandlers([channel]);
290
351
  }
291
352
 
292
353
  /**
293
- * Remove all subscribers from all channels.
354
+ * Remove all subscribers from all channels. The `onClear` of each removed handler runs, and
355
+ * each stream ends.
294
356
  */
295
357
  clearAll(): void {
296
358
  if (this.options.debug) {
@@ -298,10 +360,7 @@ export class DefaultEventBus implements EventBus {
298
360
  }
299
361
  this.subscribers.clear();
300
362
  this.buffer.clear();
301
- for (const enders of [...this.streamEnders.values()]) {
302
- for (const end of [...enders]) end();
303
- }
304
- this.streamEnders.clear();
363
+ this.runClearHandlers([...this.clearHandlers.keys()]);
305
364
  }
306
365
  }
307
366
 
@@ -398,7 +457,7 @@ export function withEventBusSignal(bus: EventBus, signal: AbortSignal): EventBus
398
457
  const waitOptions = (options?: EventWaitOptions): EventWaitOptions => ({ signal: options?.signal ?? signal });
399
458
  return {
400
459
  emit: (channel, data) => bus.emit(channel, data),
401
- on: (channel, handler) => bus.on(channel, handler),
460
+ on: (channel, handler, options) => bus.on(channel, handler, options),
402
461
  once: (channel, options) => bus.once(channel, waitOptions(options)),
403
462
  stream: (channel, options) => bus.stream(channel, waitOptions(options)),
404
463
  subscriberCount: (channel) => bus.subscriberCount(channel),
@@ -435,8 +494,8 @@ export function createChannelPrefix(bus: EventBus, prefix: string) {
435
494
  bus.emit(prefixed(channel), data);
436
495
  },
437
496
 
438
- on<T>(channel: string, handler: EventHandler<T>): Unsubscribe {
439
- return bus.on(prefixed(channel), handler);
497
+ on<T>(channel: string, handler: EventHandler<T>, options?: EventListenOptions): Unsubscribe {
498
+ return bus.on(prefixed(channel), handler, options);
440
499
  },
441
500
 
442
501
  once<T>(channel: string, options?: EventWaitOptions): Promise<T> {
@@ -525,68 +584,77 @@ export function mapChannel<TIn, TOut>(
525
584
  });
526
585
  }
527
586
 
587
+ /** Options of `debounceChannel()` and `throttleChannel()`. */
588
+ export interface ChannelTimingOptions {
589
+ /** The clock of the timers. The default is the clock of the platform. A test gives a fake clock. */
590
+ clock?: Clock;
591
+ }
592
+
528
593
  /**
529
- * Debounce events on a channel.
594
+ * This function sends the last event of `source` to `target` after `delay` without a new event
595
+ * (a debounce). The delay is a `Duration`, such as `ms(300)`. A plain number throws a `TypeError`.
530
596
  *
531
597
  * @param bus - Event bus
532
598
  * @param source - Source channel
533
599
  * @param target - Target channel
534
- * @param delay - Debounce delay in milliseconds
600
+ * @param delay - The time without an event before the send
535
601
  * @returns Unsubscribe function
536
602
  */
537
603
  export function debounceChannel<T>(
538
604
  bus: EventBus,
539
605
  source: string,
540
606
  target: string,
541
- delay: number
607
+ delay: Duration,
608
+ options: ChannelTimingOptions = {},
542
609
  ): Unsubscribe {
543
- let timer: ReturnType<typeof setTimeout> | null = null;
610
+ const delayMs = toMs(delay, "debounceChannel delay");
611
+ const clock = options.clock ?? realClock;
612
+ let cancel: (() => void) | null = null;
544
613
  let lastData: T | undefined;
545
614
 
546
615
  const unsub = bus.on<T>(source, (data) => {
547
616
  lastData = data;
548
-
549
- if (timer) {
550
- clearTimeout(timer);
551
- }
552
-
553
- timer = setTimeout(() => {
617
+ cancel?.();
618
+ cancel = clock.after(delayMs, () => {
619
+ cancel = null;
554
620
  if (lastData !== undefined) {
555
621
  bus.emit(target, lastData);
556
622
  }
557
- timer = null;
558
- }, delay);
623
+ });
559
624
  });
560
625
 
561
626
  return () => {
562
627
  unsub();
563
- if (timer) {
564
- clearTimeout(timer);
565
- }
628
+ cancel?.();
629
+ cancel = null;
566
630
  };
567
631
  }
568
632
 
569
633
  /**
570
- * Throttle events on a channel.
634
+ * This function sends an event of `source` to `target` at most one time in each `interval` (a
635
+ * throttle). It drops the events between. The interval is a `Duration`, such as `seconds(1)`. A
636
+ * plain number throws a `TypeError`.
571
637
  *
572
638
  * @param bus - Event bus
573
639
  * @param source - Source channel
574
640
  * @param target - Target channel
575
- * @param interval - Throttle interval in milliseconds
641
+ * @param interval - The shortest time between two sends
576
642
  * @returns Unsubscribe function
577
643
  */
578
644
  export function throttleChannel<T>(
579
645
  bus: EventBus,
580
646
  source: string,
581
647
  target: string,
582
- interval: number
648
+ interval: Duration,
649
+ options: ChannelTimingOptions = {},
583
650
  ): Unsubscribe {
584
- let lastEmit = 0;
651
+ const intervalMs = toMs(interval, "throttleChannel interval");
652
+ const clock = options.clock ?? realClock;
653
+ let lastEmit: number | undefined;
585
654
 
586
655
  return bus.on<T>(source, (data) => {
587
- const now = Date.now();
588
-
589
- if (now - lastEmit >= interval) {
656
+ const now = clock.now();
657
+ if (lastEmit === undefined || now - lastEmit >= intervalMs) {
590
658
  lastEmit = now;
591
659
  bus.emit(target, data);
592
660
  }
@@ -40,6 +40,7 @@ export type {
40
40
  ProcedureChannels,
41
41
  DefaultChannels,
42
42
  EventBusOptions,
43
+ EventListenOptions,
43
44
  EventWaitOptions,
44
45
  } from "./types.js";
45
46
 
@@ -65,6 +66,7 @@ export type {
65
66
  RemoteEvent,
66
67
  RemoteEventHandler,
67
68
  RemoteEventsOptions,
69
+ ResubscribeBackoff,
68
70
  SubscribeInput,
69
71
  SubscribeOptions,
70
72
  Subscription,
@@ -81,3 +83,4 @@ export {
81
83
  debounceChannel,
82
84
  throttleChannel,
83
85
  } from "./bus.js";
86
+ export type { ChannelTimingOptions } from "./bus.js";
@@ -92,6 +92,14 @@ function stoppedError(): Error {
92
92
  return Object.assign(new Error("The publisher of the events stops"), { code: "UNAVAILABLE", retryable: true });
93
93
  }
94
94
 
95
+ /**
96
+ * The error that ends a subscription when `clear()` or `clearAll()` of the bus removes the
97
+ * listener of one of its topics. The subscriber does not resubscribe: the host removed the topic.
98
+ */
99
+ function clearedError(topic: string): Error {
100
+ return Object.assign(new Error(`The host cleared the topic "${topic}", so the subscription ends`), { code: "TOPIC_CLEARED", retryable: false });
101
+ }
102
+
95
103
  const topicSchema = z.string().min(1).max(MAX_TOPIC_LENGTH);
96
104
 
97
105
  const subscribeInput = z.object({
@@ -115,7 +123,8 @@ const eventItemSchema = z.discriminatedUnion("type", [
115
123
  /**
116
124
  * One subscription: the listeners on the bus and the queue of its events. It is the async
117
125
  * iterator of the stream of the call. `return()`, the abort of the signal of the call and
118
- * `fail()` remove the listeners.
126
+ * `fail()` remove the listeners. When `clear()` of the bus removes a listener, the subscription
127
+ * gives the events of its queue, and then it ends with `TOPIC_CLEARED`.
119
128
  */
120
129
  class TopicSubscription implements AsyncIterableIterator<EventItem> {
121
130
  private confirmation: EventItem | undefined;
@@ -135,7 +144,10 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
135
144
  private readonly onClose: (subscription: TopicSubscription) => void,
136
145
  ) {
137
146
  this.confirmation = { type: "subscribed", topics };
138
- this.unsubscribes = topics.map((topic) => bus.on(topic, (data) => this.push(topic, data)));
147
+ // Before, clear() removed the listeners, and the subscriber saw no end (EVENTS.md)
148
+ this.unsubscribes = topics.map((topic) =>
149
+ bus.on(topic, (data) => this.push(topic, data), { onClear: () => this.fail(clearedError(topic), { keepQueue: true }) }),
150
+ );
139
151
  signal?.addEventListener("abort", this.onAbort, { once: true });
140
152
  }
141
153
 
@@ -201,14 +213,19 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
201
213
  return this;
202
214
  }
203
215
 
204
- /** This function ends the subscription with an error: the reader gets it at its next item. */
205
- fail(error: unknown): void {
216
+ /**
217
+ * This function ends the subscription with an error: the reader gets it at its next item. With
218
+ * `keepQueue`, the reader first gets the confirmation and the events of the queue.
219
+ */
220
+ fail(error: unknown, options: { keepQueue?: boolean } = {}): void {
206
221
  if (this.closed) return;
207
222
  const waiter = this.waiter;
208
223
  this.waiter = undefined;
209
224
  this.release();
210
- this.queue.length = 0;
211
- this.confirmation = undefined;
225
+ if (!options.keepQueue) {
226
+ this.queue.length = 0;
227
+ this.confirmation = undefined;
228
+ }
212
229
  if (waiter) waiter.reject(error);
213
230
  else this.failure = error;
214
231
  }