@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
@@ -10,13 +10,21 @@
10
10
  * - `remoteEvents()` gives an async iterator of the events.
11
11
  * - `subscribeRemote()` calls a handler for each event, and gives the unsubscribe function.
12
12
  *
13
+ * The subscribers of the same topics with the same options on one transport share one call: one
14
+ * wire subscription with a reference count. The last unsubscribe sends the cancel. Each subscriber
15
+ * has its own queue, its own `seq`, its own `ready` and its own end.
16
+ *
13
17
  * After a failure that a retry can repair (a closed connection, `UNAVAILABLE`, `TIMEOUT`), the
14
- * subscriber sends the call again after a backoff. The delivery is at most once: the events
15
- * between the failure and the new confirmation are lost. Each other error ends the subscription.
18
+ * subscriber sends the call again after a backoff, at most `maxAttempts` times without a
19
+ * confirmation. The delivery is at most once: the events between the failure and the new
20
+ * confirmation are lost. Each other error ends the subscription.
16
21
  *
17
- * This file imports only types and the backoff, so a page or a worker that subscribes stays small.
22
+ * This file imports only types, the backoff, the clock and the durations, so a page or a worker
23
+ * that subscribes stays small.
18
24
  */
19
25
  import { backoffDelay } from "@mark1russell7/service/backoff";
26
+ import { realClock } from "../clock.js";
27
+ import { toMs } from "../duration.js";
20
28
  /** The path of the procedure of a subscription. */
21
29
  export const EVENTS_SUBSCRIBE_PATH = Object.freeze(["events", "subscribe"]);
22
30
  /** The method of the procedure of a subscription. */
@@ -42,9 +50,22 @@ export class SubscriptionError extends Error {
42
50
  }
43
51
  /** The codes after which a resubscribe can succeed, also when the item does not say retryable. */
44
52
  const RESUBSCRIBE_CODES = new Set(["UNAVAILABLE", "TIMEOUT", "CONNECTION_CLOSED", "ABORTED"]);
45
- /** The default backoff: from 250 ms, twice as long at each try, at most 30 s, with full jitter. */
53
+ /** The default number of resubscribe tries (EVENTS.md). */
54
+ const DEFAULT_MAX_ATTEMPTS = 10;
55
+ /** The default backoff, in milliseconds: from 250 ms, twice as long at each try, at most 30 s, with full jitter. */
56
+ const DEFAULT_BACKOFF = { initialDelayMs: 250, maxDelayMs: 30_000, backoffMultiplier: 2, jitter: 1 };
57
+ /**
58
+ * The number of events that the queue of one subscriber holds when another subscriber of its
59
+ * shared subscription reads faster. When it is full, the oldest event goes, as at the publisher.
60
+ */
61
+ const SUBSCRIBER_BUFFER = 1024;
62
+ /** The default backoff of `DEFAULT_BACKOFF`. */
46
63
  function defaultBackoff(attempt) {
47
- return backoffDelay({ base: 250, factor: 2, cap: 30_000, jitter: { kind: "subtract", fraction: 1 } }, attempt);
64
+ return exponentialBackoff([DEFAULT_BACKOFF.initialDelayMs, DEFAULT_BACKOFF.maxDelayMs, DEFAULT_BACKOFF.backoffMultiplier, DEFAULT_BACKOFF.jitter], attempt);
65
+ }
66
+ /** The delay before try `attempt`: an exponential backoff of `[base, cap, factor, jitter]`. */
67
+ function exponentialBackoff([base, cap, factor, jitter], attempt) {
68
+ return backoffDelay({ base, factor, cap, jitter: { kind: "subtract", fraction: jitter } }, attempt);
48
69
  }
49
70
  let subscriptions = 0;
50
71
  /** The topics of a subscription as a list. */
@@ -69,17 +90,18 @@ function linkSignals(signals) {
69
90
  },
70
91
  };
71
92
  }
72
- /** This function waits for a time, or until the signal aborts. */
73
- function pause(ms, signal) {
93
+ /** This function waits for a time on the clock, or until the signal aborts. */
94
+ function pause(ms, signal, clock) {
74
95
  if (signal.aborted || !(ms > 0))
75
96
  return Promise.resolve();
76
97
  return new Promise((resolve) => {
98
+ let cancel;
77
99
  const done = () => {
78
- clearTimeout(timer);
100
+ cancel?.();
79
101
  signal.removeEventListener("abort", done);
80
102
  resolve();
81
103
  };
82
- const timer = setTimeout(done, ms);
104
+ cancel = clock.after(ms, done);
83
105
  signal.addEventListener("abort", done, { once: true });
84
106
  });
85
107
  }
@@ -99,11 +121,66 @@ function errorOfFailure(error) {
99
121
  const message = error instanceof Error ? error.message : String(error);
100
122
  return new SubscriptionError("CONNECTION_CLOSED", message, true);
101
123
  }
102
- /** The events of a subscription, with each resubscribe. The generator ends at the unsubscribe. */
103
- async function* eventsOf(transport, topics, options, hooks, id) {
104
- const { signal, unlink } = linkSignals([options.signal, hooks.lifetime]);
124
+ /** The error that ends a subscription when its resubscribe tries ran out. It keeps the code of the last failure. */
125
+ function exhaustedError(last, tries) {
126
+ return new SubscriptionError(last.code, `The subscription stopped after ${tries} resubscribe tries without a confirmation: ${last.message}`, last.retryable, last.reason);
127
+ }
128
+ /** The ids of the clocks of the options, for the keys of the shared subscriptions. */
129
+ const clockIds = new WeakMap();
130
+ let clocks = 0;
131
+ /** The id of the clock of the options: 0 without one. */
132
+ function clockIdOf(clock) {
133
+ if (!clock)
134
+ return 0;
135
+ let id = clockIds.get(clock);
136
+ if (id === undefined) {
137
+ id = ++clocks;
138
+ clockIds.set(clock, id);
139
+ }
140
+ return id;
141
+ }
142
+ /** The milliseconds of a backoff of the options. A plain number or a function throws a `TypeError`. */
143
+ function backoffSettings(backoff) {
144
+ if (backoff === null || typeof backoff !== "object") {
145
+ const given = typeof backoff === "function" ? "a function" : JSON.stringify(backoff);
146
+ throw new TypeError(`backoff must be an object of durations such as { initialDelay: ms(250), maxDelay: seconds(30) }, got ${given}. The backoff is no longer a function of milliseconds (EVENTS.md).`);
147
+ }
148
+ const base = backoff.initialDelay === undefined ? DEFAULT_BACKOFF.initialDelayMs : toMs(backoff.initialDelay, "backoff.initialDelay");
149
+ const cap = backoff.maxDelay === undefined ? DEFAULT_BACKOFF.maxDelayMs : toMs(backoff.maxDelay, "backoff.maxDelay");
150
+ const factor = backoff.backoffMultiplier ?? DEFAULT_BACKOFF.backoffMultiplier;
151
+ const jitter = Math.min(1, Math.max(0, backoff.jitter ?? DEFAULT_BACKOFF.jitter));
152
+ return [base, cap, factor, jitter];
153
+ }
154
+ /** The resubscribe rules of the options and the hooks. A bad option throws a `TypeError` at once. */
155
+ function policyOf(options, hooks) {
156
+ const maxAttempts = options.maxAttempts ?? hooks.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
157
+ if (typeof maxAttempts !== "number" || Number.isNaN(maxAttempts) || maxAttempts < 0) {
158
+ throw new TypeError(`maxAttempts must be a number from 0 (Infinity for no limit), got ${JSON.stringify(maxAttempts)}`);
159
+ }
160
+ let backoff;
161
+ let backoffKey;
162
+ if (options.backoff !== undefined) {
163
+ const settings = backoffSettings(options.backoff);
164
+ backoff = (attempt) => exponentialBackoff(settings, attempt);
165
+ backoffKey = settings;
166
+ }
167
+ else {
168
+ backoff = hooks.backoff ?? defaultBackoff;
169
+ backoffKey = "transport";
170
+ }
105
171
  const resubscribe = hooks.resubscribe !== false && options.resubscribe !== false;
106
- const backoff = options.backoff ?? hooks.backoff ?? defaultBackoff;
172
+ return {
173
+ resubscribe,
174
+ maxAttempts,
175
+ backoff,
176
+ clock: options.clock ?? hooks.clock ?? realClock,
177
+ key: [resubscribe, maxAttempts === Number.POSITIVE_INFINITY ? "Infinity" : maxAttempts, backoffKey, clockIdOf(options.clock)],
178
+ };
179
+ }
180
+ /** The events of a wire subscription, with each resubscribe. The generator ends when `stop` aborts. */
181
+ async function* wireEvents(transport, call, stop) {
182
+ const { policy, hooks } = call;
183
+ const { signal, unlink } = linkSignals([stop, hooks.lifetime]);
107
184
  let attempt = 0;
108
185
  let tries = 0;
109
186
  try {
@@ -116,10 +193,10 @@ async function* eventsOf(transport, topics, options, hooks, id) {
116
193
  return;
117
194
  }
118
195
  const message = {
119
- id: `${id}.${++tries}`,
196
+ id: `${call.id}.${++tries}`,
120
197
  method: SUBSCRIBE_METHOD,
121
- payload: { topics: [...topics] },
122
- metadata: { ...(options.metadata ?? {}) },
198
+ payload: { topics: [...call.topics] },
199
+ metadata: { ...call.metadata },
123
200
  signal,
124
201
  };
125
202
  let failure;
@@ -134,18 +211,10 @@ async function* eventsOf(transport, topics, options, hooks, id) {
134
211
  continue;
135
212
  if (payload.type === "subscribed") {
136
213
  attempt = 0;
137
- options.onSubscribed?.(payload.topics);
214
+ call.onSubscribed(payload.topics);
138
215
  }
139
216
  else if (payload.type === "event") {
140
- const event = {
141
- topic: payload.topic,
142
- data: payload.data,
143
- seq: payload.seq,
144
- dropped: payload.dropped ?? 0,
145
- subscription: id,
146
- };
147
- hooks.onEvent?.(event);
148
- yield event;
217
+ yield payload;
149
218
  }
150
219
  }
151
220
  }
@@ -158,48 +227,371 @@ async function* eventsOf(transport, topics, options, hooks, id) {
158
227
  // The publisher ended the subscription
159
228
  if (!failure)
160
229
  return;
161
- if (!resubscribe || !failure.error.retryable)
230
+ if (!policy.resubscribe || !failure.error.retryable || policy.maxAttempts === 0)
162
231
  throw failure.error;
232
+ // The tries ran out: before, HTTP resubscribed forever
233
+ if (attempt >= policy.maxAttempts)
234
+ throw exhaustedError(failure.error, attempt);
163
235
  attempt++;
236
+ call.onRetry();
164
237
  // After a lost connection, the transport waits for its reconnect: no second backoff
165
238
  if (!(failure.lost && hooks.ready))
166
- await pause(backoff(attempt), signal);
239
+ await pause(policy.backoff(attempt), signal, policy.clock);
167
240
  }
168
241
  }
169
242
  finally {
170
243
  unlink();
171
244
  }
172
245
  }
246
+ /**
247
+ * One subscriber of a shared subscription: its queue, and its numbers. Its `seq` counts from the
248
+ * last event that the shared subscription got before the subscriber joined.
249
+ */
250
+ class Subscriber {
251
+ id;
252
+ /** The `seq` of the wire subscription before the first event of this subscriber. */
253
+ base = 0;
254
+ /** The error that ends this subscriber, after the entries of its queue. */
255
+ failure;
256
+ /** This function tells the shared subscription that the subscriber waits. */
257
+ onWait = () => undefined;
258
+ queue = [];
259
+ events = 0;
260
+ waiter;
261
+ ended = false;
262
+ constructor(id) {
263
+ this.id = id;
264
+ }
265
+ /** True when the subscriber waits for an entry. */
266
+ get waiting() {
267
+ return this.waiter !== undefined;
268
+ }
269
+ /** This function gives the subscriber one event of the wire, with its own `seq`. */
270
+ pushEvent(event) {
271
+ this.push({ kind: "event", topic: event.topic, data: event.data, seq: event.seq - this.base, dropped: event.dropped ?? 0 });
272
+ }
273
+ /** This function gives the subscriber one entry. A full queue drops its oldest event. */
274
+ push(entry) {
275
+ if (this.ended)
276
+ return;
277
+ const waiter = this.waiter;
278
+ if (waiter) {
279
+ this.waiter = undefined;
280
+ waiter(entry);
281
+ return;
282
+ }
283
+ if (entry.kind === "event") {
284
+ if (this.events >= SUBSCRIBER_BUFFER) {
285
+ // The next event of the same wire stream tells the loss. After a confirmation, a new stream starts.
286
+ const index = this.queue.findIndex((queued) => queued.kind === "event");
287
+ const lost = this.queue.splice(index, 1)[0];
288
+ this.events--;
289
+ const next = index < this.queue.length ? this.queue[index] : entry;
290
+ if (next.kind === "event")
291
+ next.dropped += 1 + lost.dropped;
292
+ }
293
+ this.events++;
294
+ }
295
+ this.queue.push(entry);
296
+ }
297
+ /** The next entry, or undefined at the end. */
298
+ take() {
299
+ const entry = this.queue.shift();
300
+ if (entry) {
301
+ if (entry.kind === "event")
302
+ this.events--;
303
+ return Promise.resolve(entry);
304
+ }
305
+ if (this.ended)
306
+ return Promise.resolve(undefined);
307
+ return new Promise((resolve) => {
308
+ this.waiter = resolve;
309
+ this.onWait();
310
+ });
311
+ }
312
+ /**
313
+ * This function ends the subscriber. Without `discard`, it first gives the entries of its queue.
314
+ * The shared subscription ends it with its error, and an unsubscribe ends it with `discard`.
315
+ */
316
+ end(failure, discard = false) {
317
+ if (this.ended)
318
+ return;
319
+ this.ended = true;
320
+ this.failure = failure;
321
+ if (discard) {
322
+ this.queue.length = 0;
323
+ this.events = 0;
324
+ }
325
+ const waiter = this.waiter;
326
+ this.waiter = undefined;
327
+ waiter?.(undefined);
328
+ }
329
+ }
330
+ /**
331
+ * One wire subscription and its subscribers. It reads the next item of the wire only when a
332
+ * subscriber waits, so a single slow subscriber holds the credit, as before the share. It ends
333
+ * when its last subscriber leaves, or with the end of the wire subscription for each subscriber.
334
+ */
335
+ class SharedSubscription {
336
+ shares;
337
+ key;
338
+ transport;
339
+ call;
340
+ members = new Set();
341
+ stop = new AbortController();
342
+ /** The topics of the current confirmation, or undefined before it and during a resubscribe. */
343
+ confirmed;
344
+ /** The `seq` of the last event of the wire. */
345
+ lastSeq = 0;
346
+ demand;
347
+ started = false;
348
+ finished = false;
349
+ constructor(shares, key, transport, call) {
350
+ this.shares = shares;
351
+ this.key = key;
352
+ this.transport = transport;
353
+ this.call = call;
354
+ }
355
+ /** True while a new subscriber can join. */
356
+ get open() {
357
+ return !this.finished && !this.stop.signal.aborted;
358
+ }
359
+ /** This function adds a subscriber. On a confirmed subscription, it gets the confirmation at once. */
360
+ join(subscriber) {
361
+ this.members.add(subscriber);
362
+ subscriber.base = this.lastSeq;
363
+ subscriber.onWait = () => this.wake();
364
+ if (this.confirmed)
365
+ subscriber.push({ kind: "subscribed", topics: this.confirmed });
366
+ if (!this.started) {
367
+ this.started = true;
368
+ void this.run();
369
+ }
370
+ }
371
+ /** This function removes a subscriber. The last one ends the wire subscription: the transport sends the cancel. */
372
+ leave(subscriber) {
373
+ if (!this.members.delete(subscriber) || this.members.size > 0)
374
+ return;
375
+ this.forget();
376
+ this.stop.abort();
377
+ this.wake();
378
+ }
379
+ wake() {
380
+ const demand = this.demand;
381
+ this.demand = undefined;
382
+ demand?.();
383
+ }
384
+ /** This function removes the subscription from the shares of its transport. */
385
+ forget() {
386
+ if (this.shares?.get(this.key) === this)
387
+ this.shares.delete(this.key);
388
+ }
389
+ stopped() {
390
+ return this.stop.signal.aborted || this.call.hooks.lifetime?.aborted === true;
391
+ }
392
+ wanted() {
393
+ for (const member of this.members)
394
+ if (member.waiting)
395
+ return true;
396
+ return false;
397
+ }
398
+ async run() {
399
+ const events = wireEvents(this.transport, {
400
+ ...this.call,
401
+ onSubscribed: (topics) => {
402
+ this.confirmed = topics;
403
+ this.lastSeq = 0;
404
+ for (const member of this.members) {
405
+ member.base = 0;
406
+ member.push({ kind: "subscribed", topics });
407
+ }
408
+ },
409
+ onRetry: () => {
410
+ this.confirmed = undefined;
411
+ },
412
+ }, this.stop.signal);
413
+ // The end of the transport ends the wait for a subscriber too
414
+ const lifetime = this.call.hooks.lifetime;
415
+ const onLifetime = () => this.wake();
416
+ lifetime?.addEventListener("abort", onLifetime, { once: true });
417
+ let failure;
418
+ try {
419
+ for (;;) {
420
+ while (!this.stopped() && !this.wanted()) {
421
+ await new Promise((resolve) => {
422
+ this.demand = resolve;
423
+ });
424
+ }
425
+ if (this.stopped())
426
+ break;
427
+ const result = await events.next();
428
+ if (result.done)
429
+ break;
430
+ this.lastSeq = result.value.seq;
431
+ for (const member of this.members)
432
+ member.pushEvent(result.value);
433
+ }
434
+ }
435
+ catch (error) {
436
+ failure = errorOfFailure(error);
437
+ }
438
+ finally {
439
+ this.finished = true;
440
+ this.forget();
441
+ lifetime?.removeEventListener("abort", onLifetime);
442
+ await events.return(undefined).catch(() => undefined);
443
+ }
444
+ // The end of the wire subscription goes to each subscriber
445
+ for (const member of [...this.members])
446
+ member.end(failure);
447
+ this.members.clear();
448
+ }
449
+ }
450
+ /** The shared subscriptions of each transport, by key. */
451
+ const sharesOf = new WeakMap();
452
+ /** The key of a shared subscription: the set of topics, the metadata and the resubscribe rules. */
453
+ function keyOf(topics, metadata, policy) {
454
+ try {
455
+ return JSON.stringify([[...new Set(topics)].sort(), metadata, policy.key]);
456
+ }
457
+ catch {
458
+ // Metadata that is not JSON: the subscription is not shared
459
+ return undefined;
460
+ }
461
+ }
462
+ /** The shared subscription of a new subscriber: an open one with the same key, or a new one with the id of the subscriber. */
463
+ function sharedFor(transport, topics, options, hooks, policy, id) {
464
+ const metadata = { ...(options.metadata ?? {}) };
465
+ const key = keyOf(topics, metadata, policy);
466
+ let shares = sharesOf.get(transport);
467
+ if (!shares) {
468
+ shares = new Map();
469
+ sharesOf.set(transport, shares);
470
+ }
471
+ const existing = key === undefined ? undefined : shares.get(key);
472
+ if (existing?.open)
473
+ return existing;
474
+ const shared = new SharedSubscription(key === undefined ? undefined : shares, key ?? "", transport, { topics, metadata, policy, hooks, id });
475
+ if (key !== undefined)
476
+ shares.set(key, shared);
477
+ return shared;
478
+ }
479
+ /** The event that a subscriber gets from an entry of its queue. */
480
+ function remoteEventOf(entry, id) {
481
+ return { topic: entry.topic, data: entry.data, seq: entry.seq, dropped: entry.dropped, subscription: id };
482
+ }
483
+ /** This function calls `onSubscribed`. An error of it goes to `console.error`, and the subscription goes on. */
484
+ function confirmTo(options, topics) {
485
+ try {
486
+ options.onSubscribed?.(topics);
487
+ }
488
+ catch (error) {
489
+ console.error("[events] The onSubscribed handler failed:", error);
490
+ }
491
+ }
492
+ /** This function joins a new subscriber to its shared subscription, and gives the function that leaves it. */
493
+ function attach(subscriber, transport, topics, options, hooks, policy) {
494
+ if (options.signal?.aborted) {
495
+ subscriber.end(undefined, true);
496
+ return () => undefined;
497
+ }
498
+ const shared = sharedFor(transport, topics, options, hooks, policy, subscriber.id);
499
+ let left = false;
500
+ const leave = () => {
501
+ if (left)
502
+ return;
503
+ left = true;
504
+ options.signal?.removeEventListener("abort", leave);
505
+ shared.leave(subscriber);
506
+ subscriber.end(undefined, true);
507
+ };
508
+ options.signal?.addEventListener("abort", leave, { once: true });
509
+ shared.join(subscriber);
510
+ return leave;
511
+ }
173
512
  /**
174
513
  * This function gives the events of a subscription to topics of the bus of the other side. The
175
- * iteration sends the call. A `break` of the reader, or the abort of the signal, sends the cancel.
176
- * An error that ends the subscription ends the iteration with a `SubscriptionError`.
514
+ * iteration joins the subscription, so it sends the call when no subscriber shares one. A `break`
515
+ * of the reader, or the abort of the signal, leaves it: the last subscriber sends the cancel. An
516
+ * error that ends the subscription ends the iteration with a `SubscriptionError`.
177
517
  *
178
518
  * @param transport - The transport to the publisher: HTTP, WebSocket, a message port, local
179
519
  * @param topics - One topic, or a list of 1 to 64 topics
180
- * @param options - The signal, the resubscribe, the backoff and the confirmation handler
520
+ * @param options - The signal, the resubscribe rules, the clock and the confirmation handler
181
521
  * @param hooks - The parts that a transport adds (its connection, its backoff, its `onEvent`)
522
+ * @throws TypeError - When `backoff` or `maxAttempts` is not valid, for example a plain number of milliseconds
182
523
  */
183
524
  export function remoteEvents(transport, topics, options = {}, hooks = {}) {
184
525
  const list = topicList(topics);
526
+ const policy = policyOf(options, hooks);
185
527
  return {
186
- [Symbol.asyncIterator]: () => eventsOf(transport, list, options, hooks, `events-${++subscriptions}`),
528
+ [Symbol.asyncIterator]: () => {
529
+ const subscriber = new Subscriber(`events-${++subscriptions}`);
530
+ let leave;
531
+ let stopped = false;
532
+ const stop = () => {
533
+ stopped = true;
534
+ if (leave)
535
+ leave();
536
+ else
537
+ subscriber.end(undefined, true);
538
+ };
539
+ const iterator = {
540
+ async next() {
541
+ if (!leave && !stopped)
542
+ leave = attach(subscriber, transport, list, options, hooks, policy);
543
+ for (;;) {
544
+ const entry = await subscriber.take();
545
+ if (!entry) {
546
+ const failure = subscriber.failure;
547
+ subscriber.failure = undefined;
548
+ stop();
549
+ if (failure)
550
+ throw failure;
551
+ return { value: undefined, done: true };
552
+ }
553
+ if (entry.kind === "subscribed") {
554
+ confirmTo(options, entry.topics);
555
+ continue;
556
+ }
557
+ const event = remoteEventOf(entry, subscriber.id);
558
+ hooks.onEvent?.(event);
559
+ return { value: event, done: false };
560
+ }
561
+ },
562
+ async return() {
563
+ stop();
564
+ return { value: undefined, done: true };
565
+ },
566
+ async throw(error) {
567
+ stop();
568
+ throw error;
569
+ },
570
+ [Symbol.asyncIterator]() {
571
+ return iterator;
572
+ },
573
+ };
574
+ return iterator;
575
+ },
187
576
  };
188
577
  }
189
578
  /**
190
579
  * This function subscribes to topics of the bus of the other side, and calls the handler for each
191
580
  * event in order. It gives the unsubscribe function, with the `ready` promise of the first
192
581
  * confirmation. An error of the handler goes to `console.error`, and the subscription goes on.
582
+ * The subscribers of the same topics with the same options on one transport share one call.
193
583
  *
194
584
  * @param transport - The transport to the publisher
195
585
  * @param topics - One topic, or a list of 1 to 64 topics
196
586
  * @param handler - The handler of each event: it gets the data and the event
197
587
  * @param options - The options of `remoteEvents()`, and the `onError` handler of the end
198
588
  * @param hooks - The parts that a transport adds
589
+ * @throws TypeError - When `backoff` or `maxAttempts` is not valid, for example a plain number of milliseconds
199
590
  */
200
591
  export function subscribeRemote(transport, topics, handler, options = {}, hooks = {}) {
201
- const controller = new AbortController();
202
- const id = `events-${++subscriptions}`;
592
+ const list = topicList(topics);
593
+ const policy = policyOf(options, hooks);
594
+ const subscriber = new Subscriber(`events-${++subscriptions}`);
203
595
  let confirmed = false;
204
596
  let confirm = () => undefined;
205
597
  let refuse = () => undefined;
@@ -209,21 +601,23 @@ export function subscribeRemote(transport, topics, handler, options = {}, hooks
209
601
  });
210
602
  // A caller that does not wait for `ready` gets no unhandled rejection
211
603
  ready.catch(() => undefined);
212
- const { signal, unlink } = linkSignals([options.signal, controller.signal]);
213
- const settings = {
214
- ...options,
215
- signal,
216
- onSubscribed: (list) => {
217
- if (!confirmed) {
218
- confirmed = true;
219
- confirm();
220
- }
221
- options.onSubscribed?.(list);
222
- },
223
- };
604
+ const leave = attach(subscriber, transport, list, options, hooks, policy);
224
605
  void (async () => {
225
606
  try {
226
- for await (const event of { [Symbol.asyncIterator]: () => eventsOf(transport, topicList(topics), settings, hooks, id) }) {
607
+ for (;;) {
608
+ const entry = await subscriber.take();
609
+ if (!entry)
610
+ break;
611
+ if (entry.kind === "subscribed") {
612
+ if (!confirmed) {
613
+ confirmed = true;
614
+ confirm();
615
+ }
616
+ confirmTo(options, entry.topics);
617
+ continue;
618
+ }
619
+ const event = remoteEventOf(entry, subscriber.id);
620
+ hooks.onEvent?.(event);
227
621
  try {
228
622
  await handler(event.data, event);
229
623
  }
@@ -231,20 +625,16 @@ export function subscribeRemote(transport, topics, handler, options = {}, hooks
231
625
  console.error(`[events] The handler of ${event.topic} failed:`, error);
232
626
  }
233
627
  }
234
- if (!confirmed)
235
- refuse(new SubscriptionError("ABORTED", "The subscription ended before its confirmation", false));
236
- }
237
- catch (error) {
238
- const failure = errorOfFailure(error);
239
- if (!confirmed)
240
- refuse(failure);
241
- options.onError?.(failure);
242
628
  }
243
629
  finally {
244
- unlink();
630
+ leave();
245
631
  }
632
+ const failure = subscriber.failure;
633
+ if (!confirmed)
634
+ refuse(failure ?? new SubscriptionError("ABORTED", "The subscription ended before its confirmation", false));
635
+ if (failure)
636
+ options.onError?.(failure);
246
637
  })();
247
- const unsubscribe = () => controller.abort();
248
- return Object.assign(unsubscribe, { ready, id });
638
+ return Object.assign(leave, { ready, id: subscriber.id });
249
639
  }
250
640
  //# sourceMappingURL=remote.js.map