@procedures/client 0.7.0 → 0.8.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.
Files changed (234) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +115 -11
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/adapters/http/client/transport.d.ts +4 -0
  5. package/dist/adapters/http/client/transport.d.ts.map +1 -1
  6. package/dist/adapters/http/client/transport.js +11 -1
  7. package/dist/adapters/http/client/transport.js.map +1 -1
  8. package/dist/adapters/http/client/types.d.ts +19 -0
  9. package/dist/adapters/http/client/types.d.ts.map +1 -1
  10. package/dist/adapters/http/server/cors.d.ts +64 -0
  11. package/dist/adapters/http/server/cors.d.ts.map +1 -0
  12. package/dist/adapters/http/server/cors.js +113 -0
  13. package/dist/adapters/http/server/cors.js.map +1 -0
  14. package/dist/adapters/http/server/handler.d.ts +136 -0
  15. package/dist/adapters/http/server/handler.d.ts.map +1 -0
  16. package/dist/adapters/http/server/handler.js +495 -0
  17. package/dist/adapters/http/server/handler.js.map +1 -0
  18. package/dist/adapters/http/server/strategies.d.ts +14 -5
  19. package/dist/adapters/http/server/strategies.d.ts.map +1 -1
  20. package/dist/adapters/http/server/strategies.js.map +1 -1
  21. package/dist/adapters/http/server/transport.d.ts +36 -46
  22. package/dist/adapters/http/server/transport.d.ts.map +1 -1
  23. package/dist/adapters/http/server/transport.js +58 -433
  24. package/dist/adapters/http/server/transport.js.map +1 -1
  25. package/dist/adapters/http/server/types.d.ts +13 -0
  26. package/dist/adapters/http/server/types.d.ts.map +1 -1
  27. package/dist/adapters/websocket/client/transport.d.ts +7 -3
  28. package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
  29. package/dist/adapters/websocket/client/transport.js +9 -3
  30. package/dist/adapters/websocket/client/transport.js.map +1 -1
  31. package/dist/adapters/websocket/server/transport.d.ts +56 -1
  32. package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
  33. package/dist/adapters/websocket/server/transport.js +174 -17
  34. package/dist/adapters/websocket/server/transport.js.map +1 -1
  35. package/dist/adapters/websocket/server/types.d.ts +10 -3
  36. package/dist/adapters/websocket/server/types.d.ts.map +1 -1
  37. package/dist/adapters/websocket/server/upgrade-router.d.ts +28 -0
  38. package/dist/adapters/websocket/server/upgrade-router.d.ts.map +1 -0
  39. package/dist/adapters/websocket/server/upgrade-router.js +62 -0
  40. package/dist/adapters/websocket/server/upgrade-router.js.map +1 -0
  41. package/dist/adapters/websocket/server/upgrade.d.ts +68 -0
  42. package/dist/adapters/websocket/server/upgrade.d.ts.map +1 -0
  43. package/dist/adapters/websocket/server/upgrade.js +49 -0
  44. package/dist/adapters/websocket/server/upgrade.js.map +1 -0
  45. package/dist/adapters/websocket/shared/rpc.d.ts +2 -0
  46. package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
  47. package/dist/adapters/websocket/shared/rpc.js +8 -2
  48. package/dist/adapters/websocket/shared/rpc.js.map +1 -1
  49. package/dist/browser.d.ts +14 -6
  50. package/dist/browser.d.ts.map +1 -1
  51. package/dist/browser.js +12 -3
  52. package/dist/browser.js.map +1 -1
  53. package/dist/client/client.d.ts +43 -0
  54. package/dist/client/client.d.ts.map +1 -1
  55. package/dist/client/client.js +69 -1
  56. package/dist/client/client.js.map +1 -1
  57. package/dist/client/errors/registry.d.ts.map +1 -1
  58. package/dist/client/errors/registry.js +1 -0
  59. package/dist/client/errors/registry.js.map +1 -1
  60. package/dist/client/middleware/retry.d.ts +4 -0
  61. package/dist/client/middleware/retry.d.ts.map +1 -1
  62. package/dist/client/middleware/retry.js +86 -78
  63. package/dist/client/middleware/retry.js.map +1 -1
  64. package/dist/events/bus.d.ts +32 -14
  65. package/dist/events/bus.d.ts.map +1 -1
  66. package/dist/events/bus.js +119 -65
  67. package/dist/events/bus.js.map +1 -1
  68. package/dist/events/index.d.ts +3 -2
  69. package/dist/events/index.d.ts.map +1 -1
  70. package/dist/events/index.js.map +1 -1
  71. package/dist/events/publisher.d.ts +14 -0
  72. package/dist/events/publisher.d.ts.map +1 -1
  73. package/dist/events/publisher.js +62 -10
  74. package/dist/events/publisher.js.map +1 -1
  75. package/dist/events/remote.d.ts +80 -18
  76. package/dist/events/remote.d.ts.map +1 -1
  77. package/dist/events/remote.js +448 -58
  78. package/dist/events/remote.js.map +1 -1
  79. package/dist/events/rows.d.ts +31 -0
  80. package/dist/events/rows.d.ts.map +1 -0
  81. package/dist/events/rows.js +39 -0
  82. package/dist/events/rows.js.map +1 -0
  83. package/dist/events/types.d.ts +13 -2
  84. package/dist/events/types.d.ts.map +1 -1
  85. package/dist/index.d.ts +1 -1
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +1 -1
  88. package/dist/index.js.map +1 -1
  89. package/dist/node.d.ts +14 -0
  90. package/dist/node.d.ts.map +1 -1
  91. package/dist/node.js +9 -0
  92. package/dist/node.js.map +1 -1
  93. package/dist/procedures/caller.d.ts +33 -0
  94. package/dist/procedures/caller.d.ts.map +1 -0
  95. package/dist/procedures/caller.js +27 -0
  96. package/dist/procedures/caller.js.map +1 -0
  97. package/dist/procedures/capability.d.ts +6 -0
  98. package/dist/procedures/capability.d.ts.map +1 -1
  99. package/dist/procedures/capability.js +25 -0
  100. package/dist/procedures/capability.js.map +1 -1
  101. package/dist/procedures/collection/changes.d.ts +94 -0
  102. package/dist/procedures/collection/changes.d.ts.map +1 -0
  103. package/dist/procedures/collection/changes.js +182 -0
  104. package/dist/procedures/collection/changes.js.map +1 -0
  105. package/dist/procedures/collection/index.d.ts +8 -3
  106. package/dist/procedures/collection/index.d.ts.map +1 -1
  107. package/dist/procedures/collection/index.js +6 -1
  108. package/dist/procedures/collection/index.js.map +1 -1
  109. package/dist/procedures/collection/procedures.d.ts +39 -2
  110. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  111. package/dist/procedures/collection/procedures.js +131 -8
  112. package/dist/procedures/collection/procedures.js.map +1 -1
  113. package/dist/procedures/collection/query.d.ts +246 -0
  114. package/dist/procedures/collection/query.d.ts.map +1 -0
  115. package/dist/procedures/collection/query.js +854 -0
  116. package/dist/procedures/collection/query.js.map +1 -0
  117. package/dist/procedures/collection/schemas.d.ts +59 -0
  118. package/dist/procedures/collection/schemas.d.ts.map +1 -1
  119. package/dist/procedures/collection/schemas.js +172 -6
  120. package/dist/procedures/collection/schemas.js.map +1 -1
  121. package/dist/procedures/collection/storage.d.ts +64 -6
  122. package/dist/procedures/collection/storage.d.ts.map +1 -1
  123. package/dist/procedures/collection/storage.js +23 -0
  124. package/dist/procedures/collection/storage.js.map +1 -1
  125. package/dist/procedures/elicit.d.ts +190 -0
  126. package/dist/procedures/elicit.d.ts.map +1 -0
  127. package/dist/procedures/elicit.js +127 -0
  128. package/dist/procedures/elicit.js.map +1 -0
  129. package/dist/procedures/hints.d.ts +43 -0
  130. package/dist/procedures/hints.d.ts.map +1 -0
  131. package/dist/procedures/hints.js +74 -0
  132. package/dist/procedures/hints.js.map +1 -0
  133. package/dist/procedures/index.d.ts +12 -3
  134. package/dist/procedures/index.d.ts.map +1 -1
  135. package/dist/procedures/index.js +7 -1
  136. package/dist/procedures/index.js.map +1 -1
  137. package/dist/procedures/invoke.d.ts +20 -0
  138. package/dist/procedures/invoke.d.ts.map +1 -1
  139. package/dist/procedures/invoke.js +16 -0
  140. package/dist/procedures/invoke.js.map +1 -1
  141. package/dist/procedures/progress.d.ts +24 -0
  142. package/dist/procedures/progress.d.ts.map +1 -0
  143. package/dist/procedures/progress.js +11 -0
  144. package/dist/procedures/progress.js.map +1 -0
  145. package/dist/procedures/registry.d.ts.map +1 -1
  146. package/dist/procedures/registry.js +5 -0
  147. package/dist/procedures/registry.js.map +1 -1
  148. package/dist/procedures/types.d.ts +92 -0
  149. package/dist/procedures/types.d.ts.map +1 -1
  150. package/dist/procedures/types.js.map +1 -1
  151. package/dist/procedures/untrusted.d.ts +28 -0
  152. package/dist/procedures/untrusted.d.ts.map +1 -0
  153. package/dist/procedures/untrusted.js +32 -0
  154. package/dist/procedures/untrusted.js.map +1 -0
  155. package/dist/protocol.d.ts +1 -1
  156. package/dist/protocol.d.ts.map +1 -1
  157. package/dist/protocol.js +1 -0
  158. package/dist/protocol.js.map +1 -1
  159. package/dist/server/browser-guard.d.ts +12 -2
  160. package/dist/server/browser-guard.d.ts.map +1 -1
  161. package/dist/server/browser-guard.js +43 -6
  162. package/dist/server/browser-guard.js.map +1 -1
  163. package/dist/server/host-policy.d.ts +10 -1
  164. package/dist/server/host-policy.d.ts.map +1 -1
  165. package/dist/server/host-policy.js +5 -0
  166. package/dist/server/host-policy.js.map +1 -1
  167. package/dist/server/index.d.ts +1 -1
  168. package/dist/server/index.d.ts.map +1 -1
  169. package/dist/server/index.js.map +1 -1
  170. package/dist/server/procedure-server.d.ts +75 -9
  171. package/dist/server/procedure-server.d.ts.map +1 -1
  172. package/dist/server/procedure-server.js +151 -17
  173. package/dist/server/procedure-server.js.map +1 -1
  174. package/dist/server/request-auth.d.ts +51 -4
  175. package/dist/server/request-auth.d.ts.map +1 -1
  176. package/dist/server/request-auth.js +116 -5
  177. package/dist/server/request-auth.js.map +1 -1
  178. package/dist/server/types.d.ts +73 -5
  179. package/dist/server/types.d.ts.map +1 -1
  180. package/dist/server/types.js.map +1 -1
  181. package/dist/timers.d.ts +5 -0
  182. package/dist/timers.d.ts.map +1 -1
  183. package/dist/timers.js +3 -1
  184. package/dist/timers.js.map +1 -1
  185. package/package.json +5 -1
  186. package/src/adapters/http/client/transport.ts +11 -1
  187. package/src/adapters/http/client/types.ts +22 -0
  188. package/src/adapters/http/server/cors.ts +144 -0
  189. package/src/adapters/http/server/handler.ts +624 -0
  190. package/src/adapters/http/server/strategies.ts +16 -6
  191. package/src/adapters/http/server/transport.ts +66 -479
  192. package/src/adapters/http/server/types.ts +15 -0
  193. package/src/adapters/websocket/client/transport.ts +9 -3
  194. package/src/adapters/websocket/server/transport.ts +189 -18
  195. package/src/adapters/websocket/server/types.ts +11 -3
  196. package/src/adapters/websocket/server/upgrade-router.ts +77 -0
  197. package/src/adapters/websocket/server/upgrade.ts +88 -0
  198. package/src/adapters/websocket/shared/rpc.ts +9 -2
  199. package/src/browser.ts +85 -2
  200. package/src/client/client.ts +83 -1
  201. package/src/client/errors/registry.ts +1 -0
  202. package/src/client/middleware/retry.ts +89 -78
  203. package/src/events/bus.ts +141 -73
  204. package/src/events/index.ts +3 -0
  205. package/src/events/publisher.ts +71 -10
  206. package/src/events/remote.ts +575 -73
  207. package/src/events/rows.ts +48 -0
  208. package/src/events/types.ts +15 -2
  209. package/src/index.ts +1 -1
  210. package/src/node.ts +21 -0
  211. package/src/procedures/caller.ts +45 -0
  212. package/src/procedures/capability.ts +21 -0
  213. package/src/procedures/collection/changes.ts +245 -0
  214. package/src/procedures/collection/index.ts +56 -1
  215. package/src/procedures/collection/procedures.ts +185 -7
  216. package/src/procedures/collection/query.ts +1009 -0
  217. package/src/procedures/collection/schemas.ts +233 -7
  218. package/src/procedures/collection/storage.ts +84 -6
  219. package/src/procedures/elicit.ts +320 -0
  220. package/src/procedures/hints.ts +91 -0
  221. package/src/procedures/index.ts +82 -2
  222. package/src/procedures/invoke.ts +31 -0
  223. package/src/procedures/progress.ts +25 -0
  224. package/src/procedures/registry.ts +4 -0
  225. package/src/procedures/types.ts +102 -0
  226. package/src/procedures/untrusted.ts +50 -0
  227. package/src/protocol.ts +3 -1
  228. package/src/server/browser-guard.ts +48 -6
  229. package/src/server/host-policy.ts +13 -1
  230. package/src/server/index.ts +3 -0
  231. package/src/server/procedure-server.ts +185 -20
  232. package/src/server/request-auth.ts +128 -6
  233. package/src/server/types.ts +82 -5
  234. 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";
@@ -8,11 +8,17 @@
8
8
  *
9
9
  * - The topic rule (`topics`) is the expose rule of the topics. A topic outside it gives
10
10
  * `TOPIC_NOT_FOUND`.
11
+ * - `reads` gives the resources that a subscriber of a topic reads. The capability of the call
12
+ * must permit them, as for a procedure that reads them (roadmap CLI-15). By default, a topic
13
+ * `collections.<name>` reads `collection:<name>`.
11
14
  * - `allow` examines each topic with the capability of the call. A refusal gives `FORBIDDEN` with
12
15
  * the reason `topic-not-allowed`.
13
16
  * - The procedure declares that it reads the resource `events`, so the capability of the host
14
17
  * applies to it as to each procedure.
15
18
  *
19
+ * - A value with rows (`withRows()`) reaches each subscriber through the output filter of its host
20
+ * (`ctx.admit`): each row passes the filter, and the subscriber gets only the admitted rows.
21
+ *
16
22
  * The delivery is at most once. The queue of a subscription holds at most `buffer` events. When
17
23
  * it is full, the publisher drops the oldest event, and the next event tells the number of lost
18
24
  * events (`dropped`). The bus never waits for a subscriber.
@@ -21,10 +27,13 @@
21
27
  import { z } from "zod";
22
28
  import { createProcedure } from "../procedures/define.js";
23
29
  import { InvocationError } from "../procedures/invoke.js";
30
+ import { capabilityRefusal } from "../procedures/capability.js";
31
+ import { collectionTopicReads } from "../procedures/collection/changes.js";
24
32
  import type { AnyProcedure, Capability, ProcedureContext } from "../procedures/types.js";
25
33
  import type { EventBus, Unsubscribe } from "./types.js";
26
34
  import { getGlobalEventBus } from "./bus.js";
27
35
  import { EVENTS_SUBSCRIBE_PATH, type EventItem, type SubscribeInput } from "./remote.js";
36
+ import { admittedValue, rowsOf } from "./rows.js";
28
37
 
29
38
  /** The number of topics that one subscription can name. */
30
39
  export const MAX_SUBSCRIPTION_TOPICS = 64;
@@ -64,6 +73,18 @@ export interface EventPublisherOptions {
64
73
  * @default 1024
65
74
  */
66
75
  buffer?: number | undefined;
76
+ /**
77
+ * The resources that a subscriber of a topic reads (roadmap CLI-15). The publisher refuses a
78
+ * topic whose reads the capability of the call does not permit, as for a procedure that reads
79
+ * them: `FORBIDDEN` with the reason `effects-exceeded`.
80
+ * @default `collectionTopicReads`: a topic `collections.<name>` reads `collection:<name>`
81
+ */
82
+ reads?: ((topic: string) => readonly string[]) | undefined;
83
+ }
84
+
85
+ /** A procedure that reads resources, for the admission of a topic under a capability. */
86
+ function topicReader(reads: readonly string[]): AnyProcedure {
87
+ return { path: [...EVENTS_SUBSCRIBE_PATH], metadata: { effects: { reads: [...reads], writes: [] } } } as unknown as AnyProcedure;
67
88
  }
68
89
 
69
90
  /** This function gives the test of a topic rule. */
@@ -92,6 +113,14 @@ function stoppedError(): Error {
92
113
  return Object.assign(new Error("The publisher of the events stops"), { code: "UNAVAILABLE", retryable: true });
93
114
  }
94
115
 
116
+ /**
117
+ * The error that ends a subscription when `clear()` or `clearAll()` of the bus removes the
118
+ * listener of one of its topics. The subscriber does not resubscribe: the host removed the topic.
119
+ */
120
+ function clearedError(topic: string): Error {
121
+ return Object.assign(new Error(`The host cleared the topic "${topic}", so the subscription ends`), { code: "TOPIC_CLEARED", retryable: false });
122
+ }
123
+
95
124
  const topicSchema = z.string().min(1).max(MAX_TOPIC_LENGTH);
96
125
 
97
126
  const subscribeInput = z.object({
@@ -115,14 +144,17 @@ const eventItemSchema = z.discriminatedUnion("type", [
115
144
  /**
116
145
  * One subscription: the listeners on the bus and the queue of its events. It is the async
117
146
  * iterator of the stream of the call. `return()`, the abort of the signal of the call and
118
- * `fail()` remove the listeners.
147
+ * `fail()` remove the listeners. When `clear()` of the bus removes a listener, the subscription
148
+ * gives the events of its queue, and then it ends with `TOPIC_CLEARED`.
119
149
  */
120
150
  class TopicSubscription implements AsyncIterableIterator<EventItem> {
121
151
  private confirmation: EventItem | undefined;
122
152
  private readonly queue: Array<Extract<EventItem, { type: "event" }>> = [];
123
153
  private seq = 0;
124
154
  private dropped = 0;
125
- private waiter: { resolve: (result: IteratorResult<EventItem>) => void; reject: (error: unknown) => void } | undefined;
155
+ private waiter:
156
+ | { resolve: (result: IteratorResult<EventItem> | PromiseLike<IteratorResult<EventItem>>) => void; reject: (error: unknown) => void }
157
+ | undefined;
126
158
  private closed = false;
127
159
  private failure: unknown;
128
160
  private readonly unsubscribes: Unsubscribe[];
@@ -133,9 +165,14 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
133
165
  private readonly buffer: number,
134
166
  private readonly signal: AbortSignal | undefined,
135
167
  private readonly onClose: (subscription: TopicSubscription) => void,
168
+ /** The output filter of the host for this subscriber: each row of a value passes it */
169
+ private readonly admit: ((item: unknown) => Promise<boolean>) | undefined,
136
170
  ) {
137
171
  this.confirmation = { type: "subscribed", topics };
138
- this.unsubscribes = topics.map((topic) => bus.on(topic, (data) => this.push(topic, data)));
172
+ // Before, clear() removed the listeners, and the subscriber saw no end (EVENTS.md)
173
+ this.unsubscribes = topics.map((topic) =>
174
+ bus.on(topic, (data) => this.push(topic, data), { onClear: () => this.fail(clearedError(topic), { keepQueue: true }) }),
175
+ );
139
176
  signal?.addEventListener("abort", this.onAbort, { once: true });
140
177
  }
141
178
 
@@ -154,7 +191,7 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
154
191
  const waiter = this.waiter;
155
192
  if (waiter && this.queue.length === 0) {
156
193
  this.waiter = undefined;
157
- waiter.resolve({ value: this.withDropped(event), done: false });
194
+ waiter.resolve(this.deliver(event));
158
195
  return;
159
196
  }
160
197
  if (this.queue.length >= this.buffer) {
@@ -165,6 +202,16 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
165
202
  this.queue.push(event);
166
203
  }
167
204
 
205
+ /**
206
+ * The item of an event for the subscriber. The rows of its data pass the output filter of the
207
+ * host for this subscriber, and the subscriber gets only the admitted rows.
208
+ */
209
+ private deliver(event: Extract<EventItem, { type: "event" }>): Promise<IteratorResult<EventItem>> {
210
+ const item = this.withDropped(event) as Extract<EventItem, { type: "event" }>;
211
+ if (!this.admit || !rowsOf(item.data)) return Promise.resolve({ value: item, done: false });
212
+ return admittedValue(item.data, this.admit).then((data): IteratorResult<EventItem> => ({ value: { ...item, data }, done: false }));
213
+ }
214
+
168
215
  /** The event with the number of events that the publisher dropped before it. */
169
216
  private withDropped(event: Extract<EventItem, { type: "event" }>): EventItem {
170
217
  if (this.dropped === 0) return event;
@@ -180,7 +227,7 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
180
227
  return Promise.resolve({ value: confirmation, done: false });
181
228
  }
182
229
  const event = this.queue.shift();
183
- if (event) return Promise.resolve({ value: this.withDropped(event), done: false });
230
+ if (event) return this.deliver(event);
184
231
  if (this.failure !== undefined) {
185
232
  const failure = this.failure;
186
233
  this.failure = undefined;
@@ -201,14 +248,19 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
201
248
  return this;
202
249
  }
203
250
 
204
- /** This function ends the subscription with an error: the reader gets it at its next item. */
205
- fail(error: unknown): void {
251
+ /**
252
+ * This function ends the subscription with an error: the reader gets it at its next item. With
253
+ * `keepQueue`, the reader first gets the confirmation and the events of the queue.
254
+ */
255
+ fail(error: unknown, options: { keepQueue?: boolean } = {}): void {
206
256
  if (this.closed) return;
207
257
  const waiter = this.waiter;
208
258
  this.waiter = undefined;
209
259
  this.release();
210
- this.queue.length = 0;
211
- this.confirmation = undefined;
260
+ if (!options.keepQueue) {
261
+ this.queue.length = 0;
262
+ this.confirmation = undefined;
263
+ }
212
264
  if (waiter) waiter.reject(error);
213
265
  else this.failure = error;
214
266
  }
@@ -251,6 +303,7 @@ export class EventPublisher {
251
303
  readonly procedure: AnyProcedure;
252
304
  private readonly known: (topic: string) => boolean;
253
305
  private readonly allow: EventPublisherOptions["allow"];
306
+ private readonly reads: (topic: string) => readonly string[];
254
307
  private readonly buffer: number;
255
308
  private readonly open = new Set<TopicSubscription>();
256
309
 
@@ -258,6 +311,7 @@ export class EventPublisher {
258
311
  this.bus = options.bus ?? getGlobalEventBus();
259
312
  this.known = topicMatcher(options.topics);
260
313
  this.allow = options.allow;
314
+ this.reads = options.reads ?? collectionTopicReads;
261
315
  this.buffer = Math.max(1, Math.floor(options.buffer ?? 1024));
262
316
  this.procedure = createProcedure()
263
317
  .path([...EVENTS_SUBSCRIBE_PATH])
@@ -299,6 +353,13 @@ export class EventPublisher {
299
353
  for (const topic of unique) {
300
354
  if (!this.known(topic)) throw new TopicNotFoundError(`The host has no topic "${topic}"`);
301
355
  }
356
+ // A subscriber of a topic reads its resources: the capability of the call must permit them
357
+ for (const topic of unique) {
358
+ const reads = this.reads(topic);
359
+ if (reads.length === 0) continue;
360
+ const refusal = capabilityRefusal(topicReader(reads), ctx.capability);
361
+ if (refusal) throw new InvocationError("FORBIDDEN", `The topic "${topic}": ${refusal.message}`, ctx.path, refusal.reason);
362
+ }
302
363
  if (this.allow) {
303
364
  const call: TopicCall = { capability: ctx.capability, metadata: ctx.metadata };
304
365
  for (const topic of unique) {
@@ -307,7 +368,7 @@ export class EventPublisher {
307
368
  }
308
369
  }
309
370
  }
310
- const subscription = new TopicSubscription(this.bus, unique, this.buffer, ctx.signal, (closed) => this.open.delete(closed));
371
+ const subscription = new TopicSubscription(this.bus, unique, this.buffer, ctx.signal, (closed) => this.open.delete(closed), ctx.admit);
311
372
  this.open.add(subscription);
312
373
  return subscription.closeIfAborted();
313
374
  }