@procedures/client 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +61 -38
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/adapters/http/client/transport.d.ts.map +1 -1
  5. package/dist/adapters/http/client/transport.js +35 -3
  6. package/dist/adapters/http/client/transport.js.map +1 -1
  7. package/dist/adapters/http/client/types.d.ts +3 -2
  8. package/dist/adapters/http/client/types.d.ts.map +1 -1
  9. package/dist/adapters/http/server/transport.d.ts +2 -2
  10. package/dist/adapters/http/server/transport.d.ts.map +1 -1
  11. package/dist/adapters/http/server/transport.js +53 -20
  12. package/dist/adapters/http/server/transport.js.map +1 -1
  13. package/dist/adapters/http/shared/headers.d.ts +1 -1
  14. package/dist/adapters/http/shared/headers.d.ts.map +1 -1
  15. package/dist/adapters/http/shared/headers.js +7 -6
  16. package/dist/adapters/http/shared/headers.js.map +1 -1
  17. package/dist/adapters/local/client/transport.d.ts +3 -0
  18. package/dist/adapters/local/client/transport.d.ts.map +1 -1
  19. package/dist/adapters/local/client/transport.js +63 -4
  20. package/dist/adapters/local/client/transport.js.map +1 -1
  21. package/dist/adapters/local/client/types.d.ts +15 -0
  22. package/dist/adapters/local/client/types.d.ts.map +1 -1
  23. package/dist/adapters/local/client/types.js.map +1 -1
  24. package/dist/adapters/mock/client/transport.d.ts +3 -2
  25. package/dist/adapters/mock/client/transport.d.ts.map +1 -1
  26. package/dist/adapters/mock/client/transport.js +8 -5
  27. package/dist/adapters/mock/client/transport.js.map +1 -1
  28. package/dist/adapters/mock/client/types.d.ts +7 -6
  29. package/dist/adapters/mock/client/types.d.ts.map +1 -1
  30. package/dist/adapters/port/transport.d.ts +62 -0
  31. package/dist/adapters/port/transport.d.ts.map +1 -0
  32. package/dist/adapters/port/transport.js +94 -0
  33. package/dist/adapters/port/transport.js.map +1 -0
  34. package/dist/adapters/websocket/client/index.d.ts +1 -1
  35. package/dist/adapters/websocket/client/index.d.ts.map +1 -1
  36. package/dist/adapters/websocket/client/index.js.map +1 -1
  37. package/dist/adapters/websocket/client/transport.d.ts +47 -7
  38. package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
  39. package/dist/adapters/websocket/client/transport.js +213 -67
  40. package/dist/adapters/websocket/client/transport.js.map +1 -1
  41. package/dist/adapters/websocket/client/types.d.ts +70 -21
  42. package/dist/adapters/websocket/client/types.d.ts.map +1 -1
  43. package/dist/adapters/websocket/client/types.js.map +1 -1
  44. package/dist/adapters/websocket/server/transport.d.ts +65 -24
  45. package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
  46. package/dist/adapters/websocket/server/transport.js +192 -263
  47. package/dist/adapters/websocket/server/transport.js.map +1 -1
  48. package/dist/adapters/websocket/server/types.d.ts +20 -1
  49. package/dist/adapters/websocket/server/types.d.ts.map +1 -1
  50. package/dist/adapters/websocket/shared/rpc.d.ts +129 -0
  51. package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -0
  52. package/dist/adapters/websocket/shared/rpc.js +425 -0
  53. package/dist/adapters/websocket/shared/rpc.js.map +1 -0
  54. package/dist/browser.d.ts +29 -7
  55. package/dist/browser.d.ts.map +1 -1
  56. package/dist/browser.js +26 -3
  57. package/dist/browser.js.map +1 -1
  58. package/dist/client/call-types.d.ts +10 -10
  59. package/dist/client/call-types.d.ts.map +1 -1
  60. package/dist/client/call-types.js +1 -6
  61. package/dist/client/call-types.js.map +1 -1
  62. package/dist/client/client.d.ts +6 -1
  63. package/dist/client/client.d.ts.map +1 -1
  64. package/dist/client/client.js +23 -9
  65. package/dist/client/client.js.map +1 -1
  66. package/dist/client/deadline.d.ts +38 -0
  67. package/dist/client/deadline.d.ts.map +1 -0
  68. package/dist/client/deadline.js +60 -0
  69. package/dist/client/deadline.js.map +1 -0
  70. package/dist/client/errors/factory.d.ts.map +1 -1
  71. package/dist/client/errors/factory.js +11 -7
  72. package/dist/client/errors/factory.js.map +1 -1
  73. package/dist/client/index.d.ts +9 -2
  74. package/dist/client/index.d.ts.map +1 -1
  75. package/dist/client/index.js +8 -2
  76. package/dist/client/index.js.map +1 -1
  77. package/dist/client/middleware/cache.d.ts +19 -5
  78. package/dist/client/middleware/cache.d.ts.map +1 -1
  79. package/dist/client/middleware/cache.js +106 -18
  80. package/dist/client/middleware/cache.js.map +1 -1
  81. package/dist/client/middleware/circuit-breaker.d.ts +28 -10
  82. package/dist/client/middleware/circuit-breaker.d.ts.map +1 -1
  83. package/dist/client/middleware/circuit-breaker.js +35 -24
  84. package/dist/client/middleware/circuit-breaker.js.map +1 -1
  85. package/dist/client/middleware/contexts.d.ts +16 -10
  86. package/dist/client/middleware/contexts.d.ts.map +1 -1
  87. package/dist/client/middleware/contexts.js.map +1 -1
  88. package/dist/client/middleware/items.d.ts +31 -2
  89. package/dist/client/middleware/items.d.ts.map +1 -1
  90. package/dist/client/middleware/items.js +86 -6
  91. package/dist/client/middleware/items.js.map +1 -1
  92. package/dist/client/middleware/lru.d.ts +2 -0
  93. package/dist/client/middleware/lru.d.ts.map +1 -1
  94. package/dist/client/middleware/lru.js +11 -0
  95. package/dist/client/middleware/lru.js.map +1 -1
  96. package/dist/client/middleware/rate-limit.d.ts +7 -6
  97. package/dist/client/middleware/rate-limit.d.ts.map +1 -1
  98. package/dist/client/middleware/rate-limit.js +9 -6
  99. package/dist/client/middleware/rate-limit.js.map +1 -1
  100. package/dist/client/middleware/resilience.d.ts +61 -0
  101. package/dist/client/middleware/resilience.d.ts.map +1 -0
  102. package/dist/client/middleware/resilience.js +59 -0
  103. package/dist/client/middleware/resilience.js.map +1 -0
  104. package/dist/client/middleware/retry.d.ts +45 -11
  105. package/dist/client/middleware/retry.d.ts.map +1 -1
  106. package/dist/client/middleware/retry.js +102 -22
  107. package/dist/client/middleware/retry.js.map +1 -1
  108. package/dist/client/middleware/timeout.d.ts +15 -12
  109. package/dist/client/middleware/timeout.d.ts.map +1 -1
  110. package/dist/client/middleware/timeout.js +46 -37
  111. package/dist/client/middleware/timeout.js.map +1 -1
  112. package/dist/client/middleware/tracing.d.ts.map +1 -1
  113. package/dist/client/middleware/tracing.js +11 -3
  114. package/dist/client/middleware/tracing.js.map +1 -1
  115. package/dist/client/types.d.ts +9 -1
  116. package/dist/client/types.d.ts.map +1 -1
  117. package/dist/client/types.js.map +1 -1
  118. package/dist/clock.d.ts +19 -0
  119. package/dist/clock.d.ts.map +1 -0
  120. package/dist/clock.js +18 -0
  121. package/dist/clock.js.map +1 -0
  122. package/dist/duration.d.ts +37 -0
  123. package/dist/duration.d.ts.map +1 -0
  124. package/dist/duration.js +44 -0
  125. package/dist/duration.js.map +1 -0
  126. package/dist/machines.d.ts +90 -0
  127. package/dist/machines.d.ts.map +1 -0
  128. package/dist/machines.js +127 -0
  129. package/dist/machines.js.map +1 -0
  130. package/dist/procedures/capability.d.ts +3 -6
  131. package/dist/procedures/capability.d.ts.map +1 -1
  132. package/dist/procedures/capability.js +11 -9
  133. package/dist/procedures/capability.js.map +1 -1
  134. package/dist/procedures/collection/index.d.ts +3 -3
  135. package/dist/procedures/collection/index.d.ts.map +1 -1
  136. package/dist/procedures/collection/index.js +1 -1
  137. package/dist/procedures/collection/index.js.map +1 -1
  138. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  139. package/dist/procedures/collection/procedures.js +49 -1
  140. package/dist/procedures/collection/procedures.js.map +1 -1
  141. package/dist/procedures/collection/schemas.d.ts +33 -0
  142. package/dist/procedures/collection/schemas.d.ts.map +1 -1
  143. package/dist/procedures/collection/schemas.js +68 -0
  144. package/dist/procedures/collection/schemas.js.map +1 -1
  145. package/dist/procedures/collection/storage.d.ts +37 -0
  146. package/dist/procedures/collection/storage.d.ts.map +1 -1
  147. package/dist/procedures/core/array.d.ts.map +1 -1
  148. package/dist/procedures/core/array.js +26 -15
  149. package/dist/procedures/core/array.js.map +1 -1
  150. package/dist/procedures/core/index.d.ts.map +1 -1
  151. package/dist/procedures/core/index.js +8 -0
  152. package/dist/procedures/core/index.js.map +1 -1
  153. package/dist/procedures/core/meta.d.ts.map +1 -1
  154. package/dist/procedures/core/meta.js +2 -1
  155. package/dist/procedures/core/meta.js.map +1 -1
  156. package/dist/procedures/core/object.d.ts.map +1 -1
  157. package/dist/procedures/core/object.js +42 -13
  158. package/dist/procedures/core/object.js.map +1 -1
  159. package/dist/procedures/core/operands.d.ts.map +1 -1
  160. package/dist/procedures/core/operands.js +6 -1
  161. package/dist/procedures/core/operands.js.map +1 -1
  162. package/dist/procedures/core/string.d.ts.map +1 -1
  163. package/dist/procedures/core/string.js +4 -2
  164. package/dist/procedures/core/string.js.map +1 -1
  165. package/dist/procedures/define-procedure.d.ts +19 -7
  166. package/dist/procedures/define-procedure.d.ts.map +1 -1
  167. package/dist/procedures/define-procedure.js +66 -21
  168. package/dist/procedures/define-procedure.js.map +1 -1
  169. package/dist/procedures/effects.d.ts +16 -0
  170. package/dist/procedures/effects.d.ts.map +1 -1
  171. package/dist/procedures/effects.js +33 -0
  172. package/dist/procedures/effects.js.map +1 -1
  173. package/dist/procedures/idempotency.d.ts +73 -0
  174. package/dist/procedures/idempotency.d.ts.map +1 -0
  175. package/dist/procedures/idempotency.js +131 -0
  176. package/dist/procedures/idempotency.js.map +1 -0
  177. package/dist/procedures/index.d.ts +7 -3
  178. package/dist/procedures/index.d.ts.map +1 -1
  179. package/dist/procedures/index.js +6 -2
  180. package/dist/procedures/index.js.map +1 -1
  181. package/dist/procedures/interceptors.d.ts +17 -4
  182. package/dist/procedures/interceptors.d.ts.map +1 -1
  183. package/dist/procedures/interceptors.js +18 -7
  184. package/dist/procedures/interceptors.js.map +1 -1
  185. package/dist/procedures/invoke.d.ts +15 -3
  186. package/dist/procedures/invoke.d.ts.map +1 -1
  187. package/dist/procedures/invoke.js +8 -3
  188. package/dist/procedures/invoke.js.map +1 -1
  189. package/dist/procedures/manifest.d.ts +83 -0
  190. package/dist/procedures/manifest.d.ts.map +1 -0
  191. package/dist/procedures/manifest.js +90 -0
  192. package/dist/procedures/manifest.js.map +1 -0
  193. package/dist/procedures/runtime-programs.d.ts +19 -0
  194. package/dist/procedures/runtime-programs.d.ts.map +1 -0
  195. package/dist/procedures/runtime-programs.js +24 -0
  196. package/dist/procedures/runtime-programs.js.map +1 -0
  197. package/dist/procedures/types.d.ts +20 -1
  198. package/dist/procedures/types.d.ts.map +1 -1
  199. package/dist/procedures/types.js.map +1 -1
  200. package/dist/protocol.d.ts +40 -0
  201. package/dist/protocol.d.ts.map +1 -0
  202. package/dist/protocol.js +58 -0
  203. package/dist/protocol.js.map +1 -0
  204. package/dist/server/host-policy.d.ts +4 -3
  205. package/dist/server/host-policy.d.ts.map +1 -1
  206. package/dist/server/host-policy.js +6 -3
  207. package/dist/server/host-policy.js.map +1 -1
  208. package/dist/server/host-procedures.d.ts +31 -0
  209. package/dist/server/host-procedures.d.ts.map +1 -0
  210. package/dist/server/host-procedures.js +75 -0
  211. package/dist/server/host-procedures.js.map +1 -0
  212. package/dist/server/index.d.ts +3 -3
  213. package/dist/server/index.d.ts.map +1 -1
  214. package/dist/server/index.js +1 -1
  215. package/dist/server/index.js.map +1 -1
  216. package/dist/server/procedure-server.d.ts +110 -4
  217. package/dist/server/procedure-server.d.ts.map +1 -1
  218. package/dist/server/procedure-server.js +269 -17
  219. package/dist/server/procedure-server.js.map +1 -1
  220. package/dist/server/request-auth.d.ts +19 -0
  221. package/dist/server/request-auth.d.ts.map +1 -0
  222. package/dist/server/request-auth.js +32 -0
  223. package/dist/server/request-auth.js.map +1 -0
  224. package/dist/server/types.d.ts +32 -0
  225. package/dist/server/types.d.ts.map +1 -1
  226. package/dist/server/types.js.map +1 -1
  227. package/dist/timers.d.ts +60 -0
  228. package/dist/timers.d.ts.map +1 -0
  229. package/dist/timers.js +33 -0
  230. package/dist/timers.js.map +1 -0
  231. package/dist/tracing/traceparent.d.ts +26 -0
  232. package/dist/tracing/traceparent.d.ts.map +1 -0
  233. package/dist/tracing/traceparent.js +53 -0
  234. package/dist/tracing/traceparent.js.map +1 -0
  235. package/package.json +7 -1
  236. package/src/adapters/http/client/transport.ts +31 -3
  237. package/src/adapters/http/client/types.ts +3 -2
  238. package/src/adapters/http/server/transport.ts +60 -24
  239. package/src/adapters/http/shared/headers.ts +7 -6
  240. package/src/adapters/local/client/transport.ts +70 -5
  241. package/src/adapters/local/client/types.ts +18 -0
  242. package/src/adapters/mock/client/transport.ts +9 -6
  243. package/src/adapters/mock/client/types.ts +7 -6
  244. package/src/adapters/port/transport.ts +124 -0
  245. package/src/adapters/websocket/client/index.ts +1 -0
  246. package/src/adapters/websocket/client/transport.ts +232 -73
  247. package/src/adapters/websocket/client/types.ts +80 -21
  248. package/src/adapters/websocket/server/transport.ts +199 -301
  249. package/src/adapters/websocket/server/types.ts +20 -1
  250. package/src/adapters/websocket/shared/rpc.ts +453 -0
  251. package/src/browser.ts +51 -3
  252. package/src/client/call-types.ts +10 -14
  253. package/src/client/client.ts +28 -9
  254. package/src/client/deadline.ts +64 -0
  255. package/src/client/errors/factory.ts +12 -8
  256. package/src/client/index.ts +21 -3
  257. package/src/client/middleware/cache.ts +120 -23
  258. package/src/client/middleware/circuit-breaker.ts +65 -35
  259. package/src/client/middleware/contexts.ts +17 -10
  260. package/src/client/middleware/items.ts +101 -6
  261. package/src/client/middleware/lru.ts +12 -0
  262. package/src/client/middleware/rate-limit.ts +9 -7
  263. package/src/client/middleware/resilience.ts +81 -0
  264. package/src/client/middleware/retry.ts +146 -34
  265. package/src/client/middleware/timeout.ts +55 -46
  266. package/src/client/middleware/tracing.ts +13 -4
  267. package/src/client/types.ts +9 -1
  268. package/src/clock.ts +21 -0
  269. package/src/duration.ts +56 -0
  270. package/src/machines.ts +186 -0
  271. package/src/procedures/capability.ts +11 -9
  272. package/src/procedures/collection/index.ts +9 -1
  273. package/src/procedures/collection/procedures.ts +57 -0
  274. package/src/procedures/collection/schemas.ts +110 -0
  275. package/src/procedures/collection/storage.ts +38 -0
  276. package/src/procedures/core/array.ts +27 -16
  277. package/src/procedures/core/index.ts +8 -0
  278. package/src/procedures/core/meta.ts +2 -1
  279. package/src/procedures/core/object.ts +48 -14
  280. package/src/procedures/core/operands.ts +7 -0
  281. package/src/procedures/core/string.ts +4 -2
  282. package/src/procedures/define-procedure.ts +82 -24
  283. package/src/procedures/effects.ts +35 -0
  284. package/src/procedures/idempotency.ts +152 -0
  285. package/src/procedures/index.ts +18 -2
  286. package/src/procedures/interceptors.ts +28 -11
  287. package/src/procedures/invoke.ts +19 -5
  288. package/src/procedures/manifest.ts +158 -0
  289. package/src/procedures/runtime-programs.ts +27 -0
  290. package/src/procedures/types.ts +20 -1
  291. package/src/protocol.ts +97 -0
  292. package/src/server/host-policy.ts +8 -5
  293. package/src/server/host-procedures.ts +102 -0
  294. package/src/server/index.ts +5 -2
  295. package/src/server/procedure-server.ts +324 -10
  296. package/src/server/request-auth.ts +35 -0
  297. package/src/server/types.ts +37 -0
  298. package/src/timers.ts +82 -0
  299. package/src/tracing/traceparent.ts +61 -0
@@ -6,9 +6,20 @@
6
6
  */
7
7
 
8
8
  import type { Transport, Message, ResponseItem } from "../../../client/types.js";
9
- import type { WebSocketTransportOptions, WebSocketMessage, ServerRequestHandler, EventHandler } from "./types.js";
9
+ import type { WebSocketTransportOptions, WebSocketMessage, ServerRequestHandler, EventHandler, WebSocketClass } from "./types.js";
10
10
  import { WebSocketState } from "./types.js";
11
11
  import { withoutInternalKeys } from "../../metadata.js";
12
+ import { IncomingCalls } from "../shared/rpc.js";
13
+ import type { RequestHandler, ServerResponse } from "../../../server/types.js";
14
+ import { methodToPath } from "../../../server/method.js";
15
+ import { optionalMs } from "../../../duration.js";
16
+ import { PROTOCOL_VERSION, protocolVersionOf } from "../../../protocol.js";
17
+ import { TIMER_DEFAULTS } from "../../../timers.js";
18
+ import { realClock, type Cancel, type Clock } from "../../../clock.js";
19
+ import { heartbeat, type Heartbeat } from "@mark1russell7/service/heartbeat";
20
+ import { backoffDelay } from "@mark1russell7/service/backoff";
21
+ import type { MachineTransition } from "../../../machines.js";
22
+ import { WEBSOCKET_CONNECTION_MACHINE } from "../../../machines.js";
12
23
 
13
24
  /**
14
25
  * Pending request waiting for response.
@@ -64,7 +75,7 @@ interface PendingRequest {
64
75
  /** The response items of the request, in order */
65
76
  queue: ItemQueue<ResponseItem<unknown>>;
66
77
  /** The request timeout: it runs until the first frame arrives */
67
- timeout?: ReturnType<typeof setTimeout> | undefined;
78
+ timeout?: Cancel | undefined;
68
79
  }
69
80
 
70
81
  /**
@@ -93,6 +104,59 @@ function abortedItem<TRes>(id: string): ResponseItem<TRes> {
93
104
  return errorItem<TRes>(id, "ABORTED", "Request was aborted", false);
94
105
  }
95
106
 
107
+ /**
108
+ * The handler of the calls of the server without a `serve` option: the older `onServerRequest`
109
+ * function (it gets the path and the input), or else a `NOT_FOUND` answer.
110
+ */
111
+ function legacyHandler(onServerRequest: ServerRequestHandler | undefined): RequestHandler {
112
+ return {
113
+ async handle(request): Promise<ServerResponse> {
114
+ if (!onServerRequest) {
115
+ return {
116
+ id: request.id,
117
+ status: { type: "error", code: "NOT_FOUND", message: "This client serves no procedures", retryable: false },
118
+ metadata: {},
119
+ };
120
+ }
121
+ try {
122
+ const payload = await onServerRequest(methodToPath(request.method), request.payload);
123
+ return { id: request.id, status: { type: "success", code: 200 }, payload, metadata: {} };
124
+ } catch (error) {
125
+ return {
126
+ id: request.id,
127
+ status: { type: "error", code: "HANDLER_ERROR", message: error instanceof Error ? error.message : String(error), retryable: false },
128
+ metadata: {},
129
+ };
130
+ }
131
+ },
132
+ };
133
+ }
134
+
135
+ /**
136
+ * The delay before a reconnect try (from 1): an exponential backoff up to `maxDelay`, with a
137
+ * random part. With `jitter: 1` (full jitter, the default), the delay is a random time from 0 to
138
+ * the backoff, so the clients of a server that restarted do not come back at the same moments
139
+ * (roadmap R3). The backoff is `backoffDelay` of `@mark1russell7/service/backoff` (roadmap R37).
140
+ */
141
+ export function reconnectDelay(
142
+ attempt: number,
143
+ options: { initialDelay: number; backoffMultiplier: number; maxDelay: number; jitter: number },
144
+ random: () => number = Math.random,
145
+ ): number {
146
+ return Math.round(
147
+ backoffDelay(
148
+ {
149
+ base: options.initialDelay,
150
+ factor: options.backoffMultiplier,
151
+ cap: options.maxDelay,
152
+ jitter: { kind: "subtract", fraction: options.jitter },
153
+ },
154
+ attempt,
155
+ random,
156
+ ),
157
+ );
158
+ }
159
+
96
160
  export class WebSocketTransport implements Transport {
97
161
  readonly name = "websocket";
98
162
 
@@ -106,6 +170,7 @@ export class WebSocketTransport implements Transport {
106
170
  initialDelay: number;
107
171
  maxDelay: number;
108
172
  backoffMultiplier: number;
173
+ jitter: number;
109
174
  };
110
175
  connectionTimeout: number;
111
176
  requestTimeout: number;
@@ -114,12 +179,14 @@ export class WebSocketTransport implements Transport {
114
179
  enabled: boolean;
115
180
  interval: number;
116
181
  timeout: number;
182
+ misses: number;
117
183
  };
118
184
  onConnect: (() => void) | undefined;
119
185
  onDisconnect: ((reason?: string) => void) | undefined;
120
186
  onReconnecting: ((attempt: number) => void) | undefined;
121
187
  onError: ((error: Error) => void) | undefined;
122
188
  onServerRequest: ServerRequestHandler | undefined;
189
+ serve: RequestHandler | undefined;
123
190
  onEvent: EventHandler | undefined;
124
191
  };
125
192
  /** The requests in progress, by wire id */
@@ -135,9 +202,21 @@ export class WebSocketTransport implements Transport {
135
202
  /** Set by close(): the connection is being closed on purpose, so it must not reconnect */
136
203
  private closing = false;
137
204
  private reconnectAttempts = 0;
138
- private reconnectTimer: ReturnType<typeof setTimeout> | undefined = undefined;
139
- private heartbeatTimer: ReturnType<typeof setTimeout> | undefined = undefined;
140
- private heartbeatTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
205
+ private reconnectTimer: Cancel | undefined = undefined;
206
+ /** The heartbeat of the connection: `heartbeat` of `@mark1russell7/service/heartbeat` (roadmap R37) */
207
+ private heartbeat: Heartbeat | undefined = undefined;
208
+ /** The clock of each timer of the transport (roadmap R35) */
209
+ private readonly clock: Clock;
210
+ /** The class of the sockets, or undefined for the global `WebSocket` (finding of roadmap R36) */
211
+ private readonly webSocketClass: WebSocketClass | undefined;
212
+ /** The observer of the changes of the state (roadmap R35) */
213
+ private readonly transitionHandler: ((transition: MachineTransition) => void) | undefined;
214
+ /** The protocol version of the server, from its hello, or undefined (an older server) (R25) */
215
+ private serverProtocol: number | undefined;
216
+ /** The calls that the server sends on the current socket, served by `serve` (roadmap R24) */
217
+ private incoming: IncomingCalls | undefined;
218
+ /** The time limit of the handshake of the socket that opens now */
219
+ private handshakeTimer: Cancel | undefined = undefined;
141
220
 
142
221
  constructor(options: WebSocketTransportOptions) {
143
222
  this.options = {
@@ -145,40 +224,54 @@ export class WebSocketTransport implements Transport {
145
224
  reconnect: {
146
225
  enabled: options.reconnect?.enabled ?? true,
147
226
  maxAttempts: options.reconnect?.maxAttempts ?? Infinity,
148
- initialDelay: options.reconnect?.initialDelay ?? 1000,
149
- maxDelay: options.reconnect?.maxDelay ?? 30000,
227
+ initialDelay: optionalMs(options.reconnect?.initialDelay, "reconnect.initialDelay") ?? TIMER_DEFAULTS.webSocketClient.reconnectInitialDelayMs,
228
+ maxDelay: optionalMs(options.reconnect?.maxDelay, "reconnect.maxDelay") ?? TIMER_DEFAULTS.webSocketClient.reconnectMaxDelayMs,
150
229
  backoffMultiplier: options.reconnect?.backoffMultiplier ?? 1.5,
230
+ jitter: Math.min(1, Math.max(0, options.reconnect?.jitter ?? 1)),
151
231
  },
152
- connectionTimeout: options.connectionTimeout ?? 10000,
153
- requestTimeout: options.requestTimeout ?? 30000,
232
+ connectionTimeout: optionalMs(options.connectionTimeout, "connectionTimeout") ?? TIMER_DEFAULTS.webSocketClient.connectionTimeoutMs,
233
+ requestTimeout: optionalMs(options.requestTimeout, "requestTimeout") ?? TIMER_DEFAULTS.webSocketClient.requestTimeoutMs,
154
234
  streamWindow: Math.max(2, options.streamWindow ?? 64),
155
235
  heartbeat: {
156
236
  enabled: options.heartbeat?.enabled ?? true,
157
- interval: options.heartbeat?.interval ?? 30000,
158
- timeout: options.heartbeat?.timeout ?? 5000,
237
+ interval: optionalMs(options.heartbeat?.interval, "heartbeat.interval") ?? TIMER_DEFAULTS.webSocketClient.heartbeatIntervalMs,
238
+ timeout: optionalMs(options.heartbeat?.timeout, "heartbeat.timeout") ?? TIMER_DEFAULTS.webSocketClient.heartbeatTimeoutMs,
239
+ misses: Math.max(1, options.heartbeat?.misses ?? TIMER_DEFAULTS.webSocketClient.heartbeatMisses),
159
240
  },
160
241
  onConnect: options.onConnect,
161
242
  onDisconnect: options.onDisconnect,
162
243
  onReconnecting: options.onReconnecting,
163
244
  onError: options.onError,
164
245
  onServerRequest: options.onServerRequest,
246
+ serve: options.serve,
165
247
  onEvent: options.onEvent,
166
248
  };
249
+ this.clock = options.clock ?? realClock;
250
+ this.webSocketClass = options.webSocket;
251
+ this.transitionHandler = options.onTransition;
167
252
 
168
253
  // Auto-connect
169
254
  this.connect();
170
255
  }
171
256
 
172
- /** Change the state, and tell the callers that wait for a connection. */
173
- private setState(state: WebSocketState): void {
257
+ /**
258
+ * Change the state, and tell the callers that wait for a connection. `on` is the event of
259
+ * `WEBSOCKET_CONNECTION_MACHINE`: a change goes to `onTransition` (roadmap R35).
260
+ */
261
+ private setState(state: WebSocketState, on: string): void {
262
+ const from = this.state;
174
263
  this.state = state;
264
+ if (from !== state) {
265
+ this.transitionHandler?.({ machine: WEBSOCKET_CONNECTION_MACHINE.id, instance: this.name, from, on, to: state, at: this.clock.now() });
266
+ }
175
267
  for (const listener of [...this.stateListeners]) listener();
176
268
  }
177
269
 
178
270
  /**
179
271
  * Connect to WebSocket server.
180
272
  */
181
- private connect(): void {
273
+ /** `on` is the event of the machine: the first connect, or the end of a backoff. */
274
+ private connect(on: "connect" | "reconnect-due" = "connect"): void {
182
275
  if (this.closing) {
183
276
  return;
184
277
  }
@@ -186,17 +279,36 @@ export class WebSocketTransport implements Transport {
186
279
  return;
187
280
  }
188
281
 
189
- this.setState(WebSocketState.CONNECTING);
282
+ this.setState(WebSocketState.CONNECTING, on);
190
283
 
191
284
  try {
192
- const ws = new WebSocket(this.options.url);
285
+ const WebSocketClass = (this.webSocketClass ?? WebSocket) as typeof WebSocket;
286
+ const ws = new WebSocketClass(this.options.url);
193
287
  this.ws = ws;
288
+ // The calls of the server on this socket (roadmap R24)
289
+ this.incoming = this.servedCallsOf(ws);
290
+
291
+ // The handshake has a time limit: an address that never answers does not keep the socket
292
+ // in CONNECTING (roadmap R3)
293
+ this.clearHandshakeTimer();
294
+ this.handshakeTimer = this.clock.after(this.options.connectionTimeout, () => {
295
+ this.handshakeTimer = undefined;
296
+ if (this.ws === ws && this.state === WebSocketState.CONNECTING) {
297
+ console.error(`[${this.name}] Handshake timeout after ${this.options.connectionTimeout}ms`);
298
+ this.dropConnection("Handshake timeout");
299
+ }
300
+ });
194
301
 
195
302
  // Connection opened
196
303
  ws.onopen = () => {
197
304
  if (this.ws !== ws) return;
305
+ this.clearHandshakeTimer();
198
306
  this.reconnectAttempts = 0;
199
- this.setState(WebSocketState.CONNECTED);
307
+ // The protocol version of this client (roadmap R25). An older server answers with an
308
+ // error frame, which the client ignores.
309
+ this.serverProtocol = undefined;
310
+ ws.send(JSON.stringify({ id: "hello", type: "hello", protocol: PROTOCOL_VERSION }));
311
+ this.setState(WebSocketState.CONNECTED, "open");
200
312
  console.error(`[${this.name}] Connected to ${this.options.url}`);
201
313
 
202
314
  if (this.options.onConnect) {
@@ -235,6 +347,10 @@ export class WebSocketTransport implements Transport {
235
347
  if (this.options.onError) {
236
348
  this.options.onError(error);
237
349
  }
350
+ // A failed handshake: the WebSocket of Node 22 gives no close event after this error, and
351
+ // stays in CONNECTING. The transport closes it now, so the reconnect starts at once
352
+ // (roadmap R30: before, each try waited for the time limit of the handshake).
353
+ if (this.state === WebSocketState.CONNECTING) this.dropConnection("Connection failed");
238
354
  };
239
355
  } catch (error) {
240
356
  console.error(`[${this.name}] Failed to create WebSocket:`, error);
@@ -257,7 +373,11 @@ export class WebSocketTransport implements Transport {
257
373
  ws.onclose = null;
258
374
  ws.onerror = null;
259
375
  try {
260
- ws.close();
376
+ // A socket of the ws package ends at once. close() waits for the answer of a peer that can
377
+ // be dead (finding of roadmap R36).
378
+ const terminate = (ws as { terminate?: () => void }).terminate;
379
+ if (typeof terminate === "function") terminate.call(ws);
380
+ else ws.close();
261
381
  } catch {
262
382
  // The socket is unusable: nothing more to do
263
383
  }
@@ -269,9 +389,7 @@ export class WebSocketTransport implements Transport {
269
389
  private failPending(reason: string): void {
270
390
  for (const pending of this.pendingRequests.values()) {
271
391
  pending.queue.fail(new Error(reason));
272
- if (pending.timeout) {
273
- clearTimeout(pending.timeout);
274
- }
392
+ pending.timeout?.();
275
393
  }
276
394
  this.pendingRequests.clear();
277
395
  }
@@ -281,11 +399,14 @@ export class WebSocketTransport implements Transport {
281
399
  */
282
400
  private handleClose(reason?: string): void {
283
401
  this.stopHeartbeat();
402
+ this.incoming?.abortAll();
403
+ this.incoming = undefined;
404
+ this.clearHandshakeTimer();
284
405
 
285
406
  // An intentional close() must not reconnect (BUGS-2026-07 H6)
286
407
  const reconnect = this.options.reconnect;
287
408
  const willReconnect = !this.closing && reconnect.enabled && this.reconnectAttempts < reconnect.maxAttempts;
288
- this.setState(willReconnect ? WebSocketState.RECONNECTING : WebSocketState.DISCONNECTED);
409
+ this.setState(willReconnect ? WebSocketState.RECONNECTING : WebSocketState.DISCONNECTED, willReconnect ? "drop" : "give-up");
289
410
 
290
411
  console.error(`[${this.name}] Disconnected${reason ? `: ${reason}` : ""}`);
291
412
 
@@ -305,17 +426,13 @@ export class WebSocketTransport implements Transport {
305
426
  */
306
427
  private scheduleReconnect(): void {
307
428
  if (this.reconnectTimer) {
308
- clearTimeout(this.reconnectTimer);
429
+ this.reconnectTimer();
309
430
  }
310
431
 
311
432
  this.reconnectAttempts++;
312
- this.setState(WebSocketState.RECONNECTING);
433
+ this.setState(WebSocketState.RECONNECTING, "drop");
313
434
 
314
- const reconnect = this.options.reconnect;
315
- const delay = Math.min(
316
- reconnect.initialDelay * Math.pow(reconnect.backoffMultiplier, this.reconnectAttempts - 1),
317
- reconnect.maxDelay
318
- );
435
+ const delay = reconnectDelay(this.reconnectAttempts, this.options.reconnect);
319
436
 
320
437
  console.error(`[${this.name}] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
321
438
 
@@ -323,64 +440,95 @@ export class WebSocketTransport implements Transport {
323
440
  this.options.onReconnecting(this.reconnectAttempts);
324
441
  }
325
442
 
326
- this.reconnectTimer = setTimeout(() => {
443
+ this.reconnectTimer = this.clock.after(delay, () => {
327
444
  this.reconnectTimer = undefined;
328
445
  // connect() returns at once in RECONNECTING only for CONNECTING/CONNECTED
329
- this.connect();
330
- }, delay);
446
+ this.connect("reconnect-due");
447
+ });
331
448
  }
332
449
 
333
450
  /**
334
- * Start heartbeat to keep connection alive.
451
+ * Start the heartbeat of the connection: a ping each interval on the clock, one ping at a time.
452
+ * A ping without a pong at its timeout is a miss. The connection drops after `misses` misses in
453
+ * sequence. A ping whose send throws is a miss too.
335
454
  */
336
455
  private startHeartbeat(): void {
337
456
  this.stopHeartbeat();
338
-
339
- this.heartbeatTimer = setInterval(() => {
340
- // One ping at a time: a new ping must not replace the timeout of an unanswered one
341
- if (this.state === WebSocketState.CONNECTED && this.ws && !this.heartbeatTimeout) {
342
- // Send ping
457
+ this.heartbeat = heartbeat({
458
+ interval: this.options.heartbeat.interval,
459
+ timeout: this.options.heartbeat.timeout,
460
+ misses: this.options.heartbeat.misses,
461
+ clock: this.clock,
462
+ due: () => this.state === WebSocketState.CONNECTED && this.ws !== null,
463
+ ping: () => {
343
464
  const ping: WebSocketMessage = {
344
465
  id: `ping-${Date.now()}`,
345
466
  type: "ping",
346
467
  };
347
- this.ws.send(JSON.stringify(ping));
348
-
349
- // Set timeout for pong
350
- this.heartbeatTimeout = setTimeout(() => {
351
- this.heartbeatTimeout = undefined;
352
- console.warn(`[${this.name}] Heartbeat timeout - dropping connection`);
353
- this.dropConnection("Heartbeat timeout");
354
- }, this.options.heartbeat.timeout);
355
- }
356
- }, this.options.heartbeat.interval);
468
+ this.ws?.send(JSON.stringify(ping));
469
+ },
470
+ onDead: () => {
471
+ console.warn(`[${this.name}] Heartbeat timeout - dropping connection`);
472
+ this.dropConnection("Heartbeat timeout");
473
+ },
474
+ });
357
475
  }
358
476
 
359
- /**
360
- * Stop heartbeat.
361
- */
362
- private stopHeartbeat(): void {
363
- if (this.heartbeatTimer) {
364
- clearInterval(this.heartbeatTimer);
365
- this.heartbeatTimer = undefined;
366
- }
367
- if (this.heartbeatTimeout) {
368
- clearTimeout(this.heartbeatTimeout);
369
- this.heartbeatTimeout = undefined;
477
+ /** Stop the time limit of the handshake. */
478
+ private clearHandshakeTimer(): void {
479
+ if (this.handshakeTimer) {
480
+ this.handshakeTimer();
481
+ this.handshakeTimer = undefined;
370
482
  }
371
483
  }
372
484
 
485
+ /** Stop the heartbeat: its ticks, its timeout and its count of misses. */
486
+ private stopHeartbeat(): void {
487
+ this.heartbeat?.stop();
488
+ this.heartbeat = undefined;
489
+ }
490
+
373
491
  /**
374
492
  * Handle incoming message.
375
493
  */
494
+ /**
495
+ * The calls of the server on a socket, served by the `serve` option (or by `onServerRequest`, or
496
+ * with `NOT_FOUND`), with the same frames as the calls of the client (roadmap R24).
497
+ */
498
+ private servedCallsOf(ws: WebSocket): IncomingCalls {
499
+ const handler = this.options.serve ?? legacyHandler(this.options.onServerRequest);
500
+ return new IncomingCalls({
501
+ handler,
502
+ send: (frame) => ws.send(JSON.stringify(frame)),
503
+ isOpen: () => this.ws === ws && ws.readyState === WebSocket.OPEN,
504
+ // A call of the server that this client opened: the client trusts it, so it has no
505
+ // credentials (a ProcedureServer applies no token to it)
506
+ requestOf: (frame, signal) => ({
507
+ id: frame.id,
508
+ method: frame.method!,
509
+ payload: frame.payload === undefined ? {} : frame.payload,
510
+ metadata: withoutInternalKeys(frame.metadata),
511
+ signal,
512
+ }),
513
+ });
514
+ }
515
+
376
516
  private handleMessage(message: WebSocketMessage): void {
517
+ // The hello of the server, or the error of an older server that does not know it (R25)
518
+ if (message.id === "hello" && (message.type === "hello" || message.type === "error")) {
519
+ if (message.type === "hello") this.serverProtocol = protocolVersionOf(message.protocol);
520
+ else if (message.error?.code === "PROTOCOL_VERSION") this.options.onError?.(new Error(message.error.message));
521
+ return;
522
+ }
523
+ // A call of the server, its cancel or its credit (roadmap R24)
524
+ if ((message.type === "request" || message.type === "cancel" || message.type === "credit") && this.incoming?.receive(message)) {
525
+ return;
526
+ }
527
+
377
528
 
378
529
  // Handle pong (heartbeat response)
379
530
  if (message.type === "pong") {
380
- if (this.heartbeatTimeout) {
381
- clearTimeout(this.heartbeatTimeout);
382
- this.heartbeatTimeout = undefined;
383
- }
531
+ this.heartbeat?.answer();
384
532
  return;
385
533
  }
386
534
 
@@ -421,7 +569,7 @@ export class WebSocketTransport implements Transport {
421
569
 
422
570
  // The first frame arrived: the request timeout ends. A stream can stay open longer.
423
571
  if (pending.timeout) {
424
- clearTimeout(pending.timeout);
572
+ pending.timeout();
425
573
  pending.timeout = undefined;
426
574
  }
427
575
 
@@ -539,7 +687,7 @@ export class WebSocketTransport implements Transport {
539
687
 
540
688
  const queue = new ItemQueue<ResponseItem<unknown>>();
541
689
  const pending: PendingRequest = { messageId: message.id, queue };
542
- pending.timeout = setTimeout(() => {
690
+ pending.timeout = this.clock.after(this.options.requestTimeout, () => {
543
691
  if (this.pendingRequests.get(wireId) !== pending) return;
544
692
  this.pendingRequests.delete(wireId);
545
693
  this.sendCancel(wireId);
@@ -548,13 +696,13 @@ export class WebSocketTransport implements Transport {
548
696
  // so a retry middleware must not repeat it (deep dive TRN-8)
549
697
  queue.push(errorItem(message.id, "TIMEOUT", `Request timeout after ${this.options.requestTimeout}ms`, false));
550
698
  queue.end();
551
- }, this.options.requestTimeout);
699
+ });
552
700
  this.pendingRequests.set(wireId, pending);
553
701
 
554
702
  const onAbort = (): void => {
555
703
  if (this.pendingRequests.get(wireId) !== pending) return;
556
704
  this.pendingRequests.delete(wireId);
557
- if (pending.timeout) clearTimeout(pending.timeout);
705
+ pending.timeout?.();
558
706
  this.sendCancel(wireId);
559
707
  queue.push(abortedItem(message.id));
560
708
  queue.end();
@@ -579,7 +727,7 @@ export class WebSocketTransport implements Transport {
579
727
  }
580
728
  } finally {
581
729
  message.signal?.removeEventListener("abort", onAbort);
582
- if (pending.timeout) clearTimeout(pending.timeout);
730
+ pending.timeout?.();
583
731
  if (this.pendingRequests.get(wireId) === pending) {
584
732
  // The reader stopped before the end: the server stops the stream
585
733
  this.pendingRequests.delete(wireId);
@@ -619,7 +767,7 @@ export class WebSocketTransport implements Transport {
619
767
 
620
768
  return new Promise((resolve, reject) => {
621
769
  const finish = (error?: Error): void => {
622
- clearTimeout(timeout);
770
+ timeout();
623
771
  this.stateListeners.delete(check);
624
772
  signal?.removeEventListener("abort", onAbort);
625
773
  if (error) reject(error);
@@ -631,7 +779,7 @@ export class WebSocketTransport implements Transport {
631
779
  };
632
780
  // The caller aborted: send() then gives the ABORTED item
633
781
  const onAbort = (): void => finish();
634
- const timeout = setTimeout(() => finish(new Error("Connection timeout")), this.options.connectionTimeout);
782
+ const timeout = this.clock.after(this.options.connectionTimeout, () => finish(new Error("Connection timeout")));
635
783
  this.stateListeners.add(check);
636
784
  signal?.addEventListener("abort", onAbort, { once: true });
637
785
  });
@@ -691,11 +839,14 @@ export class WebSocketTransport implements Transport {
691
839
  */
692
840
  async close(): Promise<void> {
693
841
  this.closing = true;
694
- this.setState(WebSocketState.DISCONNECTING);
842
+ this.setState(WebSocketState.DISCONNECTING, "close");
695
843
  this.stopHeartbeat();
844
+ this.clearHandshakeTimer();
845
+ this.incoming?.abortAll();
846
+ this.incoming = undefined;
696
847
 
697
848
  if (this.reconnectTimer) {
698
- clearTimeout(this.reconnectTimer);
849
+ this.reconnectTimer();
699
850
  this.reconnectTimer = undefined;
700
851
  }
701
852
 
@@ -711,12 +862,20 @@ export class WebSocketTransport implements Transport {
711
862
  }
712
863
 
713
864
  this.failPending("WebSocket connection closed");
714
- this.setState(WebSocketState.DISCONNECTED);
865
+ this.setState(WebSocketState.DISCONNECTED, "closed");
715
866
  if (hadSocket && this.options.onDisconnect) {
716
867
  this.options.onDisconnect("closed");
717
868
  }
718
869
  }
719
870
 
871
+ /**
872
+ * The protocol version of the server: from its hello, or undefined before the hello or for a
873
+ * server older than protocol v1 (roadmap R25).
874
+ */
875
+ getServerProtocol(): number | undefined {
876
+ return this.serverProtocol;
877
+ }
878
+
720
879
  /**
721
880
  * Get current connection state.
722
881
  */