@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/browser.ts CHANGED
@@ -76,8 +76,9 @@ export {
76
76
  export { Server } from "./server/server.js";
77
77
  export type { ServerOptions } from "./server/server.js";
78
78
  export { ProcedureServer, createProcedureServer, STREAM_METADATA_KEY, isUnboundedStream, tokenMatches } from "./server/procedure-server.js";
79
- // The credentials of a network request (roadmap R18)
80
- export { credentialsOf, bearerOf, withoutSecretHeaders } from "./server/request-auth.js";
79
+ // The credentials of a network request (roadmap R18), and the parts that a request hook reads (CLI-2a)
80
+ export { credentialsOf, bearerOf, withoutSecretHeaders, cookiesOf, requestCredentials } from "./server/request-auth.js";
81
+ export type { RequestCredentialsOptions } from "./server/request-auth.js";
81
82
  // The procedures of a host: host.health, host.info, host.metrics (roadmap R19)
82
83
  export { hostProcedures } from "./server/host-procedures.js";
83
84
  export type { HostSource } from "./server/host-procedures.js";
@@ -93,6 +94,9 @@ export type {
93
94
  RequestHandler,
94
95
  Credentials,
95
96
  Principal,
97
+ PrincipalOf,
98
+ RequestIdentity,
99
+ AmbientCredentials,
96
100
  ServerHandler,
97
101
  ServerMiddleware,
98
102
  ServerContext,
@@ -117,6 +121,24 @@ export {
117
121
  pathToKey,
118
122
  keyToPath,
119
123
  createCollectionProcedures,
124
+ // The change events of the collection writes (roadmap CLI-15)
125
+ collectionChangeTopic,
126
+ collectionTopicReads,
127
+ // The query language of the collection storages (roadmap CLI-14, CLI-34)
128
+ DEFAULT_FIND_LIMIT,
129
+ DEFAULT_QUERY_OPERATORS,
130
+ DEFAULT_QUERY_POLICY,
131
+ QUERY_OPERATORS,
132
+ applyPatch,
133
+ compareStorageValues,
134
+ compileSelector,
135
+ isPatchOperations,
136
+ matchesSelector,
137
+ mergePatch,
138
+ patchProblems,
139
+ queryEntries,
140
+ queryOptionsProblems,
141
+ selectorProblems,
120
142
  // Manual registration helpers
121
143
  registerModule,
122
144
  registerProcedures,
@@ -128,6 +150,7 @@ export {
128
150
  registerModules,
129
151
  createRegistry,
130
152
  registerClientProcedures,
153
+ callerOf,
131
154
  // Procedure reference system (procedure-as-data)
132
155
  PROCEDURE_SYMBOL,
133
156
  PROCEDURE_JSON_KEY,
@@ -213,6 +236,7 @@ export {
213
236
 
214
237
  export type {
215
238
  Effects,
239
+ FsRootRules,
216
240
  PermittedEffects,
217
241
  Register,
218
242
  ClearanceOf,
@@ -233,8 +257,29 @@ export type {
233
257
  CollectionStorage,
234
258
  StorageChanges,
235
259
  StorageMetadata,
260
+ StorageWriteOptions,
261
+ StorageSetChange,
262
+ StorageDeleteChange,
263
+ StorageClearChange,
264
+ StorageChangeEvent,
265
+ StorageChangeBatch,
266
+ StorageObserver,
267
+ StorageFieldOperators,
268
+ StoragePatch,
269
+ StorageQueryOptions,
270
+ StorageSelector,
271
+ StorageSort,
272
+ StorageSortDirection,
273
+ StoragePatchOperations,
274
+ StorageQueryOperator,
275
+ StorageQueryPolicy,
276
+ CollectionProceduresOptions,
277
+ CollectionChangeEventsOptions,
278
+ CollectionChangeEvent,
236
279
  ProcedureModule,
237
280
  ModuleInput,
281
+ Caller,
282
+ CallerOptions,
238
283
  RegistrationOptions,
239
284
  ProcedureRegistryLike,
240
285
  ProcedureDefinition,
@@ -331,6 +376,42 @@ export { TIMEOUT_METADATA_KEY, DEADLINE_METADATA_KEY } from "./client/deadline.j
331
376
  // Typed durations: each public option that is a time takes a Duration (roadmap R11)
332
377
  export { ms, seconds, minutes, hours, toMs, optionalMs, isDuration } from "./duration.js";
333
378
  export type { Duration, TimeUnit } from "./duration.js";
379
+ // What the face of a call offers the procedure: the questions to the person at the caller, the
380
+ // progress, and the text from outside the host (CLI-10, CLI-12)
381
+ export {
382
+ createElicit,
383
+ elicitWithPolicy,
384
+ operatorAnswer,
385
+ elicitInput,
386
+ elicitConfirmation,
387
+ NO_ELICITATION_REASON,
388
+ } from "./procedures/elicit.js";
389
+ export type {
390
+ Elicit,
391
+ ElicitRequest,
392
+ ElicitFormRequest,
393
+ ElicitUrlRequest,
394
+ ElicitMode,
395
+ ElicitAction,
396
+ ElicitResult,
397
+ ElicitOptions,
398
+ ElicitField,
399
+ ElicitFieldBase,
400
+ ElicitTextField,
401
+ ElicitNumberField,
402
+ ElicitBooleanField,
403
+ ElicitChoiceField,
404
+ ElicitChoicesField,
405
+ ElicitValue,
406
+ ElicitationPolicy,
407
+ ConfirmationOptions,
408
+ ConfirmationResult,
409
+ } from "./procedures/elicit.js";
410
+ export type { ProgressUpdate, ReportProgress } from "./procedures/progress.js";
411
+ export { toolHintsOf, toolHintOverrideOf, toolHintOverrideRefusal } from "./procedures/hints.js";
412
+ export type { ToolHintOverride } from "./procedures/hints.js";
413
+ export { untrustedText, untrustedFieldsOf, untrustedTextsOf } from "./procedures/untrusted.js";
414
+ export type { UntrustedText, UntrustedSource, UntrustedValue } from "./procedures/untrusted.js";
334
415
  // One manifest type (roadmap R17)
335
416
  export { MANIFEST_FORMAT, describeProcedure as describeProcedureEntry, describeRegistry, exposedProcedures, hashOf } from "./procedures/manifest.js";
336
417
  export type { ProcedureEntry, ProcedureManifest, CliDescription, DescribeOptions, DescribeRegistryOptions } from "./procedures/manifest.js";
@@ -411,6 +492,7 @@ export type {
411
492
  TypedEventBus,
412
493
  EventHandler,
413
494
  EventBusOptions,
495
+ EventListenOptions,
414
496
  EventWaitOptions,
415
497
  ChannelMap,
416
498
  } from "./events/index.js";
@@ -424,6 +506,7 @@ export type {
424
506
  RemoteEvent,
425
507
  RemoteEventHandler,
426
508
  RemoteEventsOptions,
509
+ ResubscribeBackoff,
427
510
  SubscribeOptions,
428
511
  Subscription,
429
512
  } from "./events/index.js";
@@ -30,6 +30,7 @@ import type {
30
30
  Method,
31
31
  Metadata,
32
32
  Message,
33
+ ResponseItem,
33
34
  ClientMiddleware,
34
35
  ClientContext,
35
36
  ClientRunner,
@@ -56,8 +57,19 @@ import { RouteResolver, type ResolvedRoute, type RouteResolutionResult } from ".
56
57
  import { BatchExecutor, type ExecutionContext } from "./batch-executor.js";
57
58
  import type { ProcedureRegistry } from "../procedures/registry.js";
58
59
  import type { EventBus } from "../events/types.js";
60
+ import {
61
+ remoteEvents,
62
+ subscribeRemote,
63
+ type EventTransport,
64
+ type RemoteEvent,
65
+ type RemoteEventHandler,
66
+ type RemoteEventsOptions,
67
+ type SubscribeOptions,
68
+ type Subscription,
69
+ } from "../events/remote.js";
59
70
  import { narrowCapability } from "../procedures/capability.js";
60
- import { TIMEOUT_METADATA_KEY, absorbTimeout, writeRemainingTime } from "./deadline.js";
71
+ import { DEADLINE_METADATA_KEY, TIMEOUT_METADATA_KEY, absorbTimeout, writeRemainingTime } from "./deadline.js";
72
+ import { ms } from "../duration.js";
61
73
  import type { Capability, ClearanceOrder, NestedCallOptions } from "../procedures/types.js";
62
74
  import { PROCEDURE_REGISTRY } from "../procedures/registry.js";
63
75
  import { validateSchema } from "../procedures/schema.js";
@@ -169,6 +181,12 @@ function callerOf(options: ExecOptions): CallerContext {
169
181
  return caller;
170
182
  }
171
183
 
184
+ /**
185
+ * The timeouts of a subscription: none. The timeout middleware takes 0 as "no timeout", so the
186
+ * timeouts of the client do not end a subscription, which has no end (roadmap CLI-15).
187
+ */
188
+ const NO_SUBSCRIPTION_TIMEOUT = Object.freeze({ overall: ms(0), perAttempt: ms(0) });
189
+
172
190
  /**
173
191
  * Generate a unique message ID.
174
192
  */
@@ -221,6 +239,8 @@ export class Client<TContext = {}> {
221
239
  private readonly throwOnError: boolean;
222
240
  /** The bus of the local procedures. Undefined: the global bus. */
223
241
  private bus: EventBus | undefined;
242
+ /** The transport of the subscriptions through the middleware, made at the first subscription */
243
+ private subscriptionTransport: EventTransport | undefined;
224
244
  /** The interceptors of the local invocations (roadmap 1.2) */
225
245
  private interceptors: readonly Interceptor[] = [];
226
246
  /** The capability of the local invocations (CAPABILITIES-2026-10). Undefined: no limits. */
@@ -779,6 +799,68 @@ export class Client<TContext = {}> {
779
799
  }
780
800
  }
781
801
 
802
+ /**
803
+ * This method subscribes to topics of the event bus of the server (`documentation/EVENTS.md`).
804
+ * The call of `events.subscribe` goes through the middleware and the context of this client, as
805
+ * `stream()` does. Thus a token of the auth middleware goes with it. The subscriber resubscribes
806
+ * after a failure that a retry can repair, and the subscribers of the same topics share one call.
807
+ *
808
+ * A subscription has no deadline: the timeouts of the client (its timeout middleware, the
809
+ * `timeout` of its context and a `procedure-timeout` of its metadata) do not apply to it. For a
810
+ * deadline, give it in the metadata of the options, for example
811
+ * `{ metadata: { timeout: { overall: minutes(5) } } }`.
812
+ *
813
+ * @param topics - One topic, or a list of 1 to 64 topics
814
+ * @param handler - The handler of each event. A promise that it gives holds the next event.
815
+ * @param options - `onSubscribed`, `onError`, a signal, the metadata of the call, the resubscribe rules
816
+ * @returns The unsubscribe function, with the `ready` promise of the first confirmation
817
+ *
818
+ * @example
819
+ * ```typescript
820
+ * const unsubscribe = client.subscribe("collections.users", (event) => console.log(event));
821
+ * await unsubscribe.ready;
822
+ * unsubscribe();
823
+ * ```
824
+ */
825
+ subscribe<T = unknown>(topics: string | readonly string[], handler: RemoteEventHandler<T>, options: SubscribeOptions = {}): Subscription {
826
+ return subscribeRemote<T>(this.eventTransport(), topics, handler, options);
827
+ }
828
+
829
+ /**
830
+ * This method gives the events of topics of the event bus of the server as an async iterator,
831
+ * through the middleware of this client. A `break` or the signal ends the subscription. As for
832
+ * `subscribe()`, the timeouts of the client do not apply: the metadata of the options can give one.
833
+ *
834
+ * @param topics - One topic, or a list of 1 to 64 topics
835
+ * @param options - A signal, `onSubscribed`, the metadata of the call, the resubscribe rules
836
+ */
837
+ events<T = unknown>(topics: string | readonly string[], options: RemoteEventsOptions = {}): AsyncIterable<RemoteEvent<T>> {
838
+ return remoteEvents<T>(this.eventTransport(), topics, options);
839
+ }
840
+
841
+ /**
842
+ * The transport of the subscriptions of this client: each call goes through the middleware, with
843
+ * the context of the client in its metadata. The subscriptions of one client share it, so they
844
+ * share their calls. The timeouts and the deadline of the client stay out: only the metadata of
845
+ * the subscription can give them.
846
+ */
847
+ private eventTransport(): EventTransport {
848
+ if (!this.subscriptionTransport) {
849
+ const send = <TReq, TRes>(message: Message<TReq>): AsyncIterable<ResponseItem<TRes>> => {
850
+ const metadata: Metadata = { ...this.defaultMetadata, ...(this.getEffectiveContext() as object) };
851
+ delete metadata["timeout"];
852
+ delete metadata[TIMEOUT_METADATA_KEY];
853
+ delete metadata[DEADLINE_METADATA_KEY];
854
+ metadata["timeout"] = NO_SUBSCRIPTION_TIMEOUT as unknown as NonNullable<Metadata["timeout"]>;
855
+ Object.assign(metadata, message.metadata);
856
+ absorbTimeout(metadata);
857
+ return this.composeMiddleware<TReq, TRes>()({ message: { ...message, metadata } });
858
+ };
859
+ this.subscriptionTransport = { send };
860
+ }
861
+ return this.subscriptionTransport;
862
+ }
863
+
782
864
  /**
783
865
  * Compose middleware chain using universal middleware composition.
784
866
  *
@@ -84,6 +84,7 @@ const PROTOCOL_CATEGORIES: Readonly<Record<ProtocolErrorCode, ErrorCategory>> =
84
84
  CONNECTION_CLOSED: ErrorCategory.NETWORK,
85
85
  CONNECTION_NOT_FOUND: ErrorCategory.CLIENT,
86
86
  TOPIC_NOT_FOUND: ErrorCategory.CLIENT,
87
+ TOPIC_CLEARED: ErrorCategory.CANCELLED,
87
88
  };
88
89
 
89
90
  /**
@@ -108,6 +108,10 @@ export interface RetryOptions {
108
108
  /**
109
109
  * Hook called after each retry completes (success or failure).
110
110
  *
111
+ * The middleware calls it when the call ends: after the last item, or when the reader stops
112
+ * reading. A `Client` with `throwOnError` stops at the error item. Before, the hook then did
113
+ * not run, so it did not hear of a failure. A thrown error and an abort do not call it.
114
+ *
111
115
  * @param success - Whether the retry succeeded
112
116
  * @param attempt - Attempt number that just completed
113
117
  */
@@ -249,99 +253,106 @@ export function createRetryMiddleware(options: RetryOptions = {}): TypedClientMi
249
253
  const idempotencyKey = idempotencyKeyOf(context.message.metadata) ?? newIdempotencyKey();
250
254
  context.message.metadata[IDEMPOTENCY_KEY_METADATA] = idempotencyKey;
251
255
 
252
- let attempt = 0;
253
- for (;;) {
254
- // Update retry metadata with current state
255
- context.message.metadata.retry = {
256
- ...(typeof given === "object" && given !== null ? (given as object) : {}),
257
- attempt,
258
- maxAttempts: maxRetries + 1,
259
- idempotent,
260
- };
261
- // Each attempt gets its own request id. The id correlates the frames of one attempt
262
- // on the wire: a cancelled attempt can still answer under its id (deep dive TRN-8/9).
263
- context.message.id = attempt === 0 ? baseId : `${baseId}.retry${attempt}`;
256
+ // The outcome of the call for onAfterRetry. The hook runs in `finally`: a reader that stops
257
+ // at an error item (a Client with throwOnError) closes this generator at its yield. Before,
258
+ // the hook came after the yield, so it did not hear of a failure.
259
+ let outcome: { success: boolean; attempt: number } | undefined;
260
+ try {
261
+ let attempt = 0;
262
+ for (;;) {
263
+ // Update retry metadata with current state
264
+ context.message.metadata.retry = {
265
+ ...(typeof given === "object" && given !== null ? (given as object) : {}),
266
+ attempt,
267
+ maxAttempts: maxRetries + 1,
268
+ idempotent,
269
+ };
270
+ // Each attempt gets its own request id. The id correlates the frames of one attempt
271
+ // on the wire: a cancelled attempt can still answer under its id (deep dive TRN-8/9).
272
+ context.message.id = attempt === 0 ? baseId : `${baseId}.retry${attempt}`;
264
273
 
265
- // Check if cancelled before attempt
266
- if (signal?.aborted) {
267
- yield abortedItem<TRes>(context.message.id);
268
- return;
269
- }
274
+ // Check if cancelled before attempt
275
+ if (signal?.aborted) {
276
+ yield abortedItem<TRes>(context.message.id);
277
+ return;
278
+ }
270
279
 
271
- // The items pass through as they arrive (BUGS-2026-07 M2: before, the middleware
272
- // collected the whole stream first). A retry is possible only while no item has
273
- // reached the reader: the first item is a retryable error, or the attempt throws
274
- // before its first item. After that, a retry would repeat items.
275
- let started = false;
276
- let lastItem: ResponseItem<TRes> | undefined;
277
- let retryItem: ResponseItem<TRes> | undefined;
278
- try {
279
- for await (const item of next(context)) {
280
- if (!started && attempt < maxRetries && shouldRetryItem(item, attempt)) {
281
- retryItem = item;
282
- break;
280
+ // The items pass through as they arrive (BUGS-2026-07 M2: before, the middleware
281
+ // collected the whole stream first). A retry is possible only while no item has
282
+ // reached the reader: the first item is a retryable error, or the attempt throws
283
+ // before its first item. After that, a retry would repeat items.
284
+ let started = false;
285
+ let lastItem: ResponseItem<TRes> | undefined;
286
+ let retryItem: ResponseItem<TRes> | undefined;
287
+ try {
288
+ for await (const item of next(context)) {
289
+ if (!started && attempt < maxRetries && shouldRetryItem(item, attempt)) {
290
+ retryItem = item;
291
+ break;
292
+ }
293
+ started = true;
294
+ lastItem = item;
295
+ outcome = { success: item.status.type === "success", attempt };
296
+ yield item;
283
297
  }
284
- started = true;
285
- lastItem = item;
286
- yield item;
287
- }
288
- } catch (error) {
289
- const isAborted = signal?.aborted || (error instanceof Error && error.name === "AbortError");
290
- if (started || isAborted || attempt >= maxRetries) {
291
- throw error;
298
+ } catch (error) {
299
+ // A thrown error does not call onAfterRetry
300
+ outcome = undefined;
301
+ const isAborted = signal?.aborted || (error instanceof Error && error.name === "AbortError");
302
+ if (started || isAborted || attempt >= maxRetries) {
303
+ throw error;
304
+ }
305
+ // A thrown error is retried only when it is a network failure, or when shouldRetry
306
+ // says so. Before, every thrown error was retried: a validation error, an open
307
+ // circuit breaker, a rate limit (deep dive TRN-8).
308
+ const item = thrownItem<TRes>(context.message.id, error);
309
+ if (!shouldRetryItem(item, attempt)) {
310
+ throw error;
311
+ }
312
+ retryItem = item;
292
313
  }
293
- // A thrown error is retried only when it is a network failure, or when shouldRetry
294
- // says so. Before, every thrown error was retried: a validation error, an open
295
- // circuit breaker, a rate limit (deep dive TRN-8).
296
- const item = thrownItem<TRes>(context.message.id, error);
297
- if (!shouldRetryItem(item, attempt)) {
298
- throw error;
314
+
315
+ if (retryItem === undefined) {
316
+ // Success, a non-retryable error, or the last attempt
317
+ outcome = { success: lastItem?.status.type === "success", attempt };
318
+ return;
299
319
  }
300
- retryItem = item;
301
- }
302
320
 
303
- if (retryItem === undefined) {
304
- // Success, a non-retryable error, or the last attempt
305
- if (onAfterRetry) {
306
- await onAfterRetry(lastItem?.status.type === "success", attempt);
321
+ // The budget is empty: the error goes to the caller (R12)
322
+ if (!takeRetry()) {
323
+ outcome = { success: false, attempt };
324
+ yield retryItem;
325
+ return;
307
326
  }
308
- return;
309
- }
310
327
 
311
- // The budget is empty: the error goes to the caller (R12)
312
- if (!takeRetry()) {
313
- yield retryItem;
314
- if (onAfterRetry) await onAfterRetry(false, attempt);
315
- return;
316
- }
328
+ // A server that asks for a wait (Retry-After) gets at least that wait (R12)
329
+ let delay = Math.max(retryDelayOf(attempt, overrides), retryAfterOf(retryItem) ?? 0);
317
330
 
318
- // A server that asks for a wait (Retry-After) gets at least that wait (R12)
319
- let delay = Math.max(retryDelayOf(attempt, overrides), retryAfterOf(retryItem) ?? 0);
331
+ // Before retry hook
332
+ if (onBeforeRetry) {
333
+ const hookResult = await onBeforeRetry(retryItem, attempt);
334
+ if (!hookResult.shouldRetry) {
335
+ // Hook decided not to retry - yield the error and return
336
+ outcome = { success: false, attempt };
337
+ yield retryItem;
338
+ return;
339
+ }
320
340
 
321
- // Before retry hook
322
- if (onBeforeRetry) {
323
- const hookResult = await onBeforeRetry(retryItem, attempt);
324
- if (!hookResult.shouldRetry) {
325
- // Hook decided not to retry - yield the error and return
326
- yield retryItem;
327
- if (onAfterRetry) {
328
- await onAfterRetry(false, attempt);
341
+ // Use custom delay if provided by hook
342
+ if (hookResult.delayMs !== undefined) {
343
+ delay = hookResult.delayMs;
329
344
  }
330
- return;
331
345
  }
332
346
 
333
- // Use custom delay if provided by hook
334
- if (hookResult.delayMs !== undefined) {
335
- delay = hookResult.delayMs;
347
+ // The backoff stops at once when the caller aborts (deep dive TRN-8)
348
+ if (!(await sleep(delay, signal, clock))) {
349
+ yield abortedItem<TRes>(context.message.id);
350
+ return;
336
351
  }
352
+ attempt++;
337
353
  }
338
-
339
- // The backoff stops at once when the caller aborts (deep dive TRN-8)
340
- if (!(await sleep(delay, signal, clock))) {
341
- yield abortedItem<TRes>(context.message.id);
342
- return;
343
- }
344
- attempt++;
354
+ } finally {
355
+ if (outcome && onAfterRetry) await onAfterRetry(outcome.success, outcome.attempt);
345
356
  }
346
357
  };
347
358
  };