@procedures/client 0.2.3 → 0.4.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 (369) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +77 -44
  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 +54 -22
  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 +67 -6
  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/node.d.ts +30 -0
  38. package/dist/adapters/websocket/client/node.d.ts.map +1 -0
  39. package/dist/adapters/websocket/client/node.js +32 -0
  40. package/dist/adapters/websocket/client/node.js.map +1 -0
  41. package/dist/adapters/websocket/client/transport.d.ts +47 -7
  42. package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
  43. package/dist/adapters/websocket/client/transport.js +229 -75
  44. package/dist/adapters/websocket/client/transport.js.map +1 -1
  45. package/dist/adapters/websocket/client/types.d.ts +71 -21
  46. package/dist/adapters/websocket/client/types.d.ts.map +1 -1
  47. package/dist/adapters/websocket/client/types.js.map +1 -1
  48. package/dist/adapters/websocket/server/transport.d.ts +65 -24
  49. package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
  50. package/dist/adapters/websocket/server/transport.js +192 -263
  51. package/dist/adapters/websocket/server/transport.js.map +1 -1
  52. package/dist/adapters/websocket/server/types.d.ts +20 -1
  53. package/dist/adapters/websocket/server/types.d.ts.map +1 -1
  54. package/dist/adapters/websocket/shared/rpc.d.ts +129 -0
  55. package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -0
  56. package/dist/adapters/websocket/shared/rpc.js +425 -0
  57. package/dist/adapters/websocket/shared/rpc.js.map +1 -0
  58. package/dist/browser.d.ts +29 -7
  59. package/dist/browser.d.ts.map +1 -1
  60. package/dist/browser.js +26 -3
  61. package/dist/browser.js.map +1 -1
  62. package/dist/client/call-types.d.ts +10 -10
  63. package/dist/client/call-types.d.ts.map +1 -1
  64. package/dist/client/call-types.js +1 -6
  65. package/dist/client/call-types.js.map +1 -1
  66. package/dist/client/client.d.ts +6 -1
  67. package/dist/client/client.d.ts.map +1 -1
  68. package/dist/client/client.js +23 -9
  69. package/dist/client/client.js.map +1 -1
  70. package/dist/client/deadline.d.ts +38 -0
  71. package/dist/client/deadline.d.ts.map +1 -0
  72. package/dist/client/deadline.js +60 -0
  73. package/dist/client/deadline.js.map +1 -0
  74. package/dist/client/errors/factory.d.ts.map +1 -1
  75. package/dist/client/errors/factory.js +11 -7
  76. package/dist/client/errors/factory.js.map +1 -1
  77. package/dist/client/errors/http-errors.d.ts.map +1 -1
  78. package/dist/client/errors/http-errors.js +6 -0
  79. package/dist/client/errors/http-errors.js.map +1 -1
  80. package/dist/client/errors/index.d.ts +1 -1
  81. package/dist/client/errors/index.js +1 -1
  82. package/dist/client/errors/registry.d.ts +2 -1
  83. package/dist/client/errors/registry.d.ts.map +1 -1
  84. package/dist/client/errors/registry.js +72 -22
  85. package/dist/client/errors/registry.js.map +1 -1
  86. package/dist/client/index.d.ts +9 -2
  87. package/dist/client/index.d.ts.map +1 -1
  88. package/dist/client/index.js +8 -2
  89. package/dist/client/index.js.map +1 -1
  90. package/dist/client/middleware/cache.d.ts +19 -5
  91. package/dist/client/middleware/cache.d.ts.map +1 -1
  92. package/dist/client/middleware/cache.js +106 -18
  93. package/dist/client/middleware/cache.js.map +1 -1
  94. package/dist/client/middleware/circuit-breaker.d.ts +28 -10
  95. package/dist/client/middleware/circuit-breaker.d.ts.map +1 -1
  96. package/dist/client/middleware/circuit-breaker.js +35 -24
  97. package/dist/client/middleware/circuit-breaker.js.map +1 -1
  98. package/dist/client/middleware/contexts.d.ts +16 -10
  99. package/dist/client/middleware/contexts.d.ts.map +1 -1
  100. package/dist/client/middleware/contexts.js.map +1 -1
  101. package/dist/client/middleware/items.d.ts +31 -2
  102. package/dist/client/middleware/items.d.ts.map +1 -1
  103. package/dist/client/middleware/items.js +86 -6
  104. package/dist/client/middleware/items.js.map +1 -1
  105. package/dist/client/middleware/lru.d.ts +2 -0
  106. package/dist/client/middleware/lru.d.ts.map +1 -1
  107. package/dist/client/middleware/lru.js +11 -0
  108. package/dist/client/middleware/lru.js.map +1 -1
  109. package/dist/client/middleware/rate-limit.d.ts +7 -6
  110. package/dist/client/middleware/rate-limit.d.ts.map +1 -1
  111. package/dist/client/middleware/rate-limit.js +9 -7
  112. package/dist/client/middleware/rate-limit.js.map +1 -1
  113. package/dist/client/middleware/resilience.d.ts +61 -0
  114. package/dist/client/middleware/resilience.d.ts.map +1 -0
  115. package/dist/client/middleware/resilience.js +59 -0
  116. package/dist/client/middleware/resilience.js.map +1 -0
  117. package/dist/client/middleware/retry.d.ts +45 -11
  118. package/dist/client/middleware/retry.d.ts.map +1 -1
  119. package/dist/client/middleware/retry.js +102 -22
  120. package/dist/client/middleware/retry.js.map +1 -1
  121. package/dist/client/middleware/timeout.d.ts +15 -12
  122. package/dist/client/middleware/timeout.d.ts.map +1 -1
  123. package/dist/client/middleware/timeout.js +48 -46
  124. package/dist/client/middleware/timeout.js.map +1 -1
  125. package/dist/client/middleware/tracing.d.ts.map +1 -1
  126. package/dist/client/middleware/tracing.js +11 -3
  127. package/dist/client/middleware/tracing.js.map +1 -1
  128. package/dist/client/types.d.ts +9 -1
  129. package/dist/client/types.d.ts.map +1 -1
  130. package/dist/client/types.js.map +1 -1
  131. package/dist/clock.d.ts +19 -0
  132. package/dist/clock.d.ts.map +1 -0
  133. package/dist/clock.js +18 -0
  134. package/dist/clock.js.map +1 -0
  135. package/dist/duration.d.ts +37 -0
  136. package/dist/duration.d.ts.map +1 -0
  137. package/dist/duration.js +44 -0
  138. package/dist/duration.js.map +1 -0
  139. package/dist/index.d.ts +4 -0
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +4 -0
  142. package/dist/index.js.map +1 -1
  143. package/dist/machines.d.ts +90 -0
  144. package/dist/machines.d.ts.map +1 -0
  145. package/dist/machines.js +127 -0
  146. package/dist/machines.js.map +1 -0
  147. package/dist/node.d.ts +11 -0
  148. package/dist/node.d.ts.map +1 -0
  149. package/dist/node.js +10 -0
  150. package/dist/node.js.map +1 -0
  151. package/dist/procedures/capability.d.ts +3 -6
  152. package/dist/procedures/capability.d.ts.map +1 -1
  153. package/dist/procedures/capability.js +7 -8
  154. package/dist/procedures/capability.js.map +1 -1
  155. package/dist/procedures/collection/index.d.ts +3 -3
  156. package/dist/procedures/collection/index.d.ts.map +1 -1
  157. package/dist/procedures/collection/index.js +1 -1
  158. package/dist/procedures/collection/index.js.map +1 -1
  159. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  160. package/dist/procedures/collection/procedures.js +49 -1
  161. package/dist/procedures/collection/procedures.js.map +1 -1
  162. package/dist/procedures/collection/schemas.d.ts +33 -0
  163. package/dist/procedures/collection/schemas.d.ts.map +1 -1
  164. package/dist/procedures/collection/schemas.js +68 -0
  165. package/dist/procedures/collection/schemas.js.map +1 -1
  166. package/dist/procedures/collection/storage.d.ts +37 -0
  167. package/dist/procedures/collection/storage.d.ts.map +1 -1
  168. package/dist/procedures/core/array.d.ts +1 -1
  169. package/dist/procedures/core/array.d.ts.map +1 -1
  170. package/dist/procedures/core/array.js +81 -29
  171. package/dist/procedures/core/array.js.map +1 -1
  172. package/dist/procedures/core/index.d.ts.map +1 -1
  173. package/dist/procedures/core/index.js +17 -2
  174. package/dist/procedures/core/index.js.map +1 -1
  175. package/dist/procedures/core/logic.js +1 -1
  176. package/dist/procedures/core/logic.js.map +1 -1
  177. package/dist/procedures/core/math.d.ts.map +1 -1
  178. package/dist/procedures/core/math.js +5 -1
  179. package/dist/procedures/core/math.js.map +1 -1
  180. package/dist/procedures/core/meta.d.ts.map +1 -1
  181. package/dist/procedures/core/meta.js +43 -8
  182. package/dist/procedures/core/meta.js.map +1 -1
  183. package/dist/procedures/core/object.d.ts.map +1 -1
  184. package/dist/procedures/core/object.js +42 -13
  185. package/dist/procedures/core/object.js.map +1 -1
  186. package/dist/procedures/core/operands.d.ts.map +1 -1
  187. package/dist/procedures/core/operands.js +6 -1
  188. package/dist/procedures/core/operands.js.map +1 -1
  189. package/dist/procedures/core/string.d.ts.map +1 -1
  190. package/dist/procedures/core/string.js +4 -2
  191. package/dist/procedures/core/string.js.map +1 -1
  192. package/dist/procedures/core/type.d.ts.map +1 -1
  193. package/dist/procedures/core/type.js +4 -2
  194. package/dist/procedures/core/type.js.map +1 -1
  195. package/dist/procedures/define-procedure.d.ts +19 -7
  196. package/dist/procedures/define-procedure.d.ts.map +1 -1
  197. package/dist/procedures/define-procedure.js +57 -17
  198. package/dist/procedures/define-procedure.js.map +1 -1
  199. package/dist/procedures/discovery.d.ts +4 -6
  200. package/dist/procedures/discovery.d.ts.map +1 -1
  201. package/dist/procedures/discovery.js +4 -6
  202. package/dist/procedures/discovery.js.map +1 -1
  203. package/dist/procedures/effects.d.ts +16 -0
  204. package/dist/procedures/effects.d.ts.map +1 -1
  205. package/dist/procedures/effects.js +33 -0
  206. package/dist/procedures/effects.js.map +1 -1
  207. package/dist/procedures/idempotency.d.ts +73 -0
  208. package/dist/procedures/idempotency.d.ts.map +1 -0
  209. package/dist/procedures/idempotency.js +131 -0
  210. package/dist/procedures/idempotency.js.map +1 -0
  211. package/dist/procedures/index.d.ts +7 -3
  212. package/dist/procedures/index.d.ts.map +1 -1
  213. package/dist/procedures/index.js +6 -2
  214. package/dist/procedures/index.js.map +1 -1
  215. package/dist/procedures/interceptors.d.ts +17 -4
  216. package/dist/procedures/interceptors.d.ts.map +1 -1
  217. package/dist/procedures/interceptors.js +18 -7
  218. package/dist/procedures/interceptors.js.map +1 -1
  219. package/dist/procedures/invoke.d.ts +15 -3
  220. package/dist/procedures/invoke.d.ts.map +1 -1
  221. package/dist/procedures/invoke.js +8 -3
  222. package/dist/procedures/invoke.js.map +1 -1
  223. package/dist/procedures/manifest.d.ts +83 -0
  224. package/dist/procedures/manifest.d.ts.map +1 -0
  225. package/dist/procedures/manifest.js +90 -0
  226. package/dist/procedures/manifest.js.map +1 -0
  227. package/dist/procedures/ref.d.ts +4 -2
  228. package/dist/procedures/ref.d.ts.map +1 -1
  229. package/dist/procedures/ref.js +2 -1
  230. package/dist/procedures/ref.js.map +1 -1
  231. package/dist/procedures/runtime-programs.d.ts +19 -0
  232. package/dist/procedures/runtime-programs.d.ts.map +1 -0
  233. package/dist/procedures/runtime-programs.js +24 -0
  234. package/dist/procedures/runtime-programs.js.map +1 -0
  235. package/dist/procedures/types.d.ts +16 -0
  236. package/dist/procedures/types.d.ts.map +1 -1
  237. package/dist/procedures/types.js.map +1 -1
  238. package/dist/protocol.d.ts +47 -0
  239. package/dist/protocol.d.ts.map +1 -0
  240. package/dist/protocol.js +68 -0
  241. package/dist/protocol.js.map +1 -0
  242. package/dist/server/host-policy.d.ts +4 -3
  243. package/dist/server/host-policy.d.ts.map +1 -1
  244. package/dist/server/host-policy.js +6 -3
  245. package/dist/server/host-policy.js.map +1 -1
  246. package/dist/server/host-procedures.d.ts +31 -0
  247. package/dist/server/host-procedures.d.ts.map +1 -0
  248. package/dist/server/host-procedures.js +75 -0
  249. package/dist/server/host-procedures.js.map +1 -0
  250. package/dist/server/index.d.ts +3 -3
  251. package/dist/server/index.d.ts.map +1 -1
  252. package/dist/server/index.js +1 -1
  253. package/dist/server/index.js.map +1 -1
  254. package/dist/server/procedure-server.d.ts +112 -5
  255. package/dist/server/procedure-server.d.ts.map +1 -1
  256. package/dist/server/procedure-server.js +273 -19
  257. package/dist/server/procedure-server.js.map +1 -1
  258. package/dist/server/request-auth.d.ts +19 -0
  259. package/dist/server/request-auth.d.ts.map +1 -0
  260. package/dist/server/request-auth.js +32 -0
  261. package/dist/server/request-auth.js.map +1 -0
  262. package/dist/server/types.d.ts +32 -0
  263. package/dist/server/types.d.ts.map +1 -1
  264. package/dist/server/types.js.map +1 -1
  265. package/dist/timers.d.ts +60 -0
  266. package/dist/timers.d.ts.map +1 -0
  267. package/dist/timers.js +33 -0
  268. package/dist/timers.js.map +1 -0
  269. package/dist/tracing/traceparent.d.ts +26 -0
  270. package/dist/tracing/traceparent.d.ts.map +1 -0
  271. package/dist/tracing/traceparent.js +53 -0
  272. package/dist/tracing/traceparent.js.map +1 -0
  273. package/package.json +11 -4
  274. package/src/adapters/http/client/transport.ts +31 -3
  275. package/src/adapters/http/client/types.ts +3 -2
  276. package/src/adapters/http/server/transport.ts +61 -26
  277. package/src/adapters/http/shared/headers.ts +7 -6
  278. package/src/adapters/local/client/transport.ts +74 -7
  279. package/src/adapters/local/client/types.ts +18 -0
  280. package/src/adapters/mock/client/transport.ts +9 -6
  281. package/src/adapters/mock/client/types.ts +7 -6
  282. package/src/adapters/port/transport.ts +124 -0
  283. package/src/adapters/websocket/client/index.ts +1 -0
  284. package/src/adapters/websocket/client/node.ts +34 -0
  285. package/src/adapters/websocket/client/transport.ts +251 -81
  286. package/src/adapters/websocket/client/types.ts +81 -21
  287. package/src/adapters/websocket/server/transport.ts +199 -301
  288. package/src/adapters/websocket/server/types.ts +20 -1
  289. package/src/adapters/websocket/shared/rpc.ts +453 -0
  290. package/src/browser.ts +51 -3
  291. package/src/client/call-types.ts +10 -14
  292. package/src/client/client.ts +28 -9
  293. package/src/client/deadline.ts +64 -0
  294. package/src/client/errors/README.md +2 -0
  295. package/src/client/errors/factory.ts +12 -8
  296. package/src/client/errors/http-errors.ts +5 -0
  297. package/src/client/errors/index.ts +1 -1
  298. package/src/client/errors/registry.ts +79 -22
  299. package/src/client/index.ts +21 -3
  300. package/src/client/middleware/cache.ts +120 -23
  301. package/src/client/middleware/circuit-breaker.ts +65 -35
  302. package/src/client/middleware/contexts.ts +17 -10
  303. package/src/client/middleware/items.ts +101 -6
  304. package/src/client/middleware/lru.ts +12 -0
  305. package/src/client/middleware/rate-limit.ts +9 -9
  306. package/src/client/middleware/resilience.ts +81 -0
  307. package/src/client/middleware/retry.ts +146 -34
  308. package/src/client/middleware/timeout.ts +57 -55
  309. package/src/client/middleware/tracing.ts +13 -4
  310. package/src/client/types.ts +9 -1
  311. package/src/clock.ts +21 -0
  312. package/src/duration.ts +56 -0
  313. package/src/index.ts +4 -0
  314. package/src/machines.ts +186 -0
  315. package/src/node.ts +11 -0
  316. package/src/procedures/capability.ts +7 -8
  317. package/src/procedures/collection/index.ts +9 -1
  318. package/src/procedures/collection/procedures.ts +57 -0
  319. package/src/procedures/collection/schemas.ts +110 -0
  320. package/src/procedures/collection/storage.ts +38 -0
  321. package/src/procedures/core/array.ts +79 -31
  322. package/src/procedures/core/index.ts +17 -2
  323. package/src/procedures/core/logic.ts +1 -1
  324. package/src/procedures/core/math.ts +5 -1
  325. package/src/procedures/core/meta.ts +45 -8
  326. package/src/procedures/core/object.ts +48 -14
  327. package/src/procedures/core/operands.ts +7 -0
  328. package/src/procedures/core/string.ts +4 -2
  329. package/src/procedures/core/type.ts +4 -2
  330. package/src/procedures/define-procedure.ts +73 -20
  331. package/src/procedures/discovery.ts +4 -6
  332. package/src/procedures/effects.ts +35 -0
  333. package/src/procedures/idempotency.ts +152 -0
  334. package/src/procedures/index.ts +18 -2
  335. package/src/procedures/interceptors.ts +28 -11
  336. package/src/procedures/invoke.ts +19 -5
  337. package/src/procedures/manifest.ts +158 -0
  338. package/src/procedures/ref.ts +4 -2
  339. package/src/procedures/runtime-programs.ts +27 -0
  340. package/src/procedures/types.ts +16 -0
  341. package/src/protocol.ts +108 -0
  342. package/src/server/host-policy.ts +8 -5
  343. package/src/server/host-procedures.ts +102 -0
  344. package/src/server/index.ts +5 -2
  345. package/src/server/procedure-server.ts +328 -12
  346. package/src/server/request-auth.ts +35 -0
  347. package/src/server/types.ts +37 -0
  348. package/src/timers.ts +82 -0
  349. package/src/tracing/traceparent.ts +61 -0
  350. package/dist/procedures/cli/announce.d.ts +0 -67
  351. package/dist/procedures/cli/announce.d.ts.map +0 -1
  352. package/dist/procedures/cli/announce.js +0 -392
  353. package/dist/procedures/cli/announce.js.map +0 -1
  354. package/dist/procedures/cli/discover.d.ts +0 -66
  355. package/dist/procedures/cli/discover.d.ts.map +0 -1
  356. package/dist/procedures/cli/discover.js +0 -310
  357. package/dist/procedures/cli/discover.js.map +0 -1
  358. package/dist/procedures/cli/index.d.ts +0 -13
  359. package/dist/procedures/cli/index.d.ts.map +0 -1
  360. package/dist/procedures/cli/index.js +0 -203
  361. package/dist/procedures/cli/index.js.map +0 -1
  362. package/dist/procedures/cli/init.d.ts +0 -17
  363. package/dist/procedures/cli/init.d.ts.map +0 -1
  364. package/dist/procedures/cli/init.js +0 -336
  365. package/dist/procedures/cli/init.js.map +0 -1
  366. package/src/procedures/cli/announce.ts +0 -470
  367. package/src/procedures/cli/discover.ts +0 -390
  368. package/src/procedures/cli/index.ts +0 -231
  369. package/src/procedures/cli/init.ts +0 -370
@@ -5,26 +5,29 @@
5
5
  * Works with any transport!
6
6
  */
7
7
 
8
- import type { ClientMiddleware, ClientRunner, ClientContext, ResponseItem, TypedClientMiddleware } from "../types.js";
9
- import { ABORTED_FIRST, errorItem, nextOrAbort } from "./items.js";
8
+ import type { ClientRunner, ClientContext, ResponseItem, TypedClientMiddleware } from "../types.js";
9
+ import { ABORTED_FIRST, abortedItem, errorItem, nextOrAbort, withDelivery } from "./items.js";
10
10
  import type { TimeoutContext } from "./contexts.js";
11
+ import { deadlineOf, narrowDeadline, restoreDeadline } from "../deadline.js";
12
+ import { resilience } from "./resilience.js";
13
+ import { optionalMs, type Duration } from "../../duration.js";
11
14
 
12
15
  /**
13
16
  * Timeout middleware options.
14
17
  */
15
18
  export interface TimeoutOptions {
16
19
  /**
17
- * Timeout in milliseconds for the entire request (including retries).
20
+ * The time of the entire request (including retries), for example `seconds(10)`.
18
21
  * @default undefined (no overall timeout)
19
22
  */
20
- overall?: number;
23
+ overall?: Duration | undefined;
21
24
 
22
25
  /**
23
- * Timeout in milliseconds for each individual attempt.
26
+ * The time of each individual attempt, for example `seconds(2)`.
24
27
  * Useful with retry middleware for per-attempt timeouts.
25
28
  * @default undefined (no per-attempt timeout)
26
29
  */
27
- perAttempt?: number;
30
+ perAttempt?: Duration | undefined;
28
31
 
29
32
  /**
30
33
  * Custom timeout error message.
@@ -89,17 +92,18 @@ function composeAbortSignals(...signals: (AbortSignal | undefined)[]): {
89
92
  }
90
93
  }
91
94
 
92
- // Fallback: Manual composition for older environments
95
+ // Fallback: Manual composition for older environments. The composed signal keeps the reason of
96
+ // the signal that aborted, as with AbortSignal.any() (before, it got a new AbortError).
93
97
  const controller = new AbortController();
94
98
  const abortListeners: Array<() => void> = [];
95
99
 
96
100
  for (const signal of validSignals) {
97
101
  if (signal.aborted) {
98
- controller.abort();
102
+ controller.abort(signal.reason);
99
103
  break;
100
104
  }
101
105
 
102
- const listener = () => controller.abort();
106
+ const listener = () => controller.abort(signal.reason);
103
107
  signal.addEventListener("abort", listener, { once: true });
104
108
  abortListeners.push(() => signal.removeEventListener("abort", listener));
105
109
  }
@@ -116,7 +120,8 @@ function composeAbortSignals(...signals: (AbortSignal | undefined)[]): {
116
120
  /**
117
121
  * Run `next` with a timeout. When the time runs out, the request's signal aborts, and the
118
122
  * reader gets one TIMEOUT error item in place of the transport's ABORTED item or thrown error.
119
- * The middleware does not wait for a transport that ignores the signal.
123
+ * The middleware does not wait for a transport that ignores the signal: not after the timeout,
124
+ * and not after an abort of the caller, which gives an ABORTED item.
120
125
  *
121
126
  * Before, the middleware waited for a thrown error, but the transports yield error items: the
122
127
  * reader got "ABORTED" (not retryable), so a per-attempt timeout with retry never retried, and
@@ -131,47 +136,50 @@ async function* runWithTimeout<TReq, TRes>(
131
136
  ): AsyncGenerator<ResponseItem<TRes>, void, undefined> {
132
137
  const timeoutController = new AbortController();
133
138
  const timeoutId = setTimeout(() => timeoutController.abort(), ms);
139
+ // The overall timeout is the deadline of the call: the transport sends its remaining time to
140
+ // the server (roadmap R10). The timeout of one try is not: the server keeps a keyed write
141
+ // until the next try can get its result, and the request signal stops another call.
142
+ const deadlineBefore = retryable ? deadlineOf(context.message.metadata) : narrowDeadline(context.message.metadata, Date.now() + ms);
134
143
 
135
144
  // Compose with existing signal
136
145
  const originalSignal = context.message.signal;
137
146
  const { controller: composedController, cleanup } = composeAbortSignals(originalSignal, timeoutController.signal);
138
147
  context.message.signal = composedController.signal;
139
148
  const timedOut = (): boolean => timeoutController.signal.aborted && !originalSignal?.aborted;
140
- const timeoutItem = (): ResponseItem<TRes> => errorItem<TRes>(context.message.id, "TIMEOUT", message, retryable);
149
+ // The request left before the time ran out: the server can still run it (R1)
150
+ const timeoutItem = (): ResponseItem<TRes> =>
151
+ withDelivery(errorItem<TRes>(context.message.id, "TIMEOUT", message, retryable), "indeterminate");
141
152
 
142
153
  const iterator = next(context)[Symbol.asyncIterator]();
143
154
  // "running": a next() may be pending; "done": the iterator ended; "abandoned": the timeout won
144
155
  let phase: "running" | "done" | "abandoned" = "running";
145
156
  try {
146
157
  for (;;) {
147
- const result = await nextOrAbort(iterator, timeoutController.signal);
158
+ // The timeout and the abort of the caller both end the wait (roadmap R30: before, the wait
159
+ // ignored the abort of the caller, so a transport that ignored the signal kept the caller
160
+ // waiting until the timeout, and the caller got TIMEOUT for its own abort)
161
+ const result = await nextOrAbort(iterator, composedController.signal);
148
162
  if (result === ABORTED_FIRST) {
149
163
  phase = "abandoned";
150
- // The transport did not stop in time: end it in the background, and give the timeout
164
+ // The transport did not stop in time: end it in the background, and give the timeout or the abort
151
165
  void Promise.resolve(iterator.return?.()).catch(() => undefined);
152
- yield timeoutItem();
166
+ yield timedOut() ? timeoutItem() : abortedItem<TRes>(context.message.id);
153
167
  return;
154
168
  }
155
169
  if (result.done) {
156
170
  phase = "done";
157
171
  return;
158
172
  }
159
- if (result.value.status.type === "error" && timedOut()) {
160
- // The transport saw the aborted signal and reported ABORTED: it is a timeout
161
- yield timeoutItem();
162
- return;
163
- }
173
+ // An item or an error of the transport comes before the abort. `nextOrAbort` adds its abort
174
+ // listener before it calls `next()`, so a later abort gives ABORTED_FIRST, not an item.
164
175
  yield result.value;
165
176
  }
166
177
  } catch (error) {
167
178
  phase = "done";
168
- if (timedOut()) {
169
- yield timeoutItem();
170
- return;
171
- }
172
179
  throw error;
173
180
  } finally {
174
181
  clearTimeout(timeoutId);
182
+ restoreDeadline(context.message.metadata, deadlineBefore);
175
183
  cleanup();
176
184
  // Restore the caller's original signal; we only swapped in the composed signal for this
177
185
  // attempt. Leaving the (possibly aborted) composed signal in place poisons subsequent
@@ -200,22 +208,24 @@ async function* runWithTimeout<TReq, TRes>(
200
208
  * @example
201
209
  * ```typescript
202
210
  * // Create middleware with default timeout
203
- * client.use(createOverallTimeoutMiddleware({ overall: 5000 }));
211
+ * client.use(createOverallTimeoutMiddleware({ overall: seconds(5) }));
204
212
  *
205
213
  * // Override per-call via context
206
214
  * await client.call(method, payload, {
207
- * context: { timeout: { overall: 10000 } }
215
+ * context: { timeout: { overall: seconds(10) } }
208
216
  * });
209
217
  * ```
210
218
  */
211
219
  export function createOverallTimeoutMiddleware(options: Pick<TimeoutOptions, "overall" | "message">): TypedClientMiddleware<TimeoutContext, {}> {
212
- const { overall: defaultOverall, message = "Overall request timeout" } = options;
220
+ const { message = "Overall request timeout" } = options;
221
+ const defaultOverall = optionalMs(options.overall, "overall");
213
222
 
214
223
  return <TReq, TRes>(next: ClientRunner<TReq, TRes>): ClientRunner<TReq, TRes> => {
215
224
  return async function* (context: ClientContext<TReq>) {
216
225
  // Read from metadata first (user-provided context), then fall back to options
217
- const overall = (context.message.metadata.timeout as TimeoutContext["timeout"] | undefined)?.overall
218
- ?? defaultOverall;
226
+ const overall =
227
+ optionalMs((context.message.metadata.timeout as TimeoutContext["timeout"] | undefined)?.overall, "timeout.overall") ??
228
+ defaultOverall;
219
229
 
220
230
  if (!overall) {
221
231
  // No timeout - passthrough
@@ -243,22 +253,24 @@ export function createOverallTimeoutMiddleware(options: Pick<TimeoutOptions, "ov
243
253
  * @example
244
254
  * ```typescript
245
255
  * // Create middleware with default timeout
246
- * client.use(createTimeoutMiddleware({ perAttempt: 1000 }));
256
+ * client.use(createTimeoutMiddleware({ perAttempt: seconds(1) }));
247
257
  *
248
258
  * // Override per-call via context
249
259
  * await client.call(method, payload, {
250
- * context: { timeout: { perAttempt: 2000 } }
260
+ * context: { timeout: { perAttempt: seconds(2) } }
251
261
  * });
252
262
  * ```
253
263
  */
254
264
  export function createTimeoutMiddleware(options: Pick<TimeoutOptions, "perAttempt" | "message">): TypedClientMiddleware<TimeoutContext, {}> {
255
- const { perAttempt: defaultPerAttempt, message = "Request timeout" } = options;
265
+ const { message = "Request timeout" } = options;
266
+ const defaultPerAttempt = optionalMs(options.perAttempt, "perAttempt");
256
267
 
257
268
  return <TReq, TRes>(next: ClientRunner<TReq, TRes>): ClientRunner<TReq, TRes> => {
258
269
  return async function* (context: ClientContext<TReq>) {
259
270
  // Read from metadata first (user-provided context), then fall back to options
260
- const perAttempt = (context.message.metadata.timeout as TimeoutContext["timeout"] | undefined)?.perAttempt
261
- ?? defaultPerAttempt;
271
+ const perAttempt =
272
+ optionalMs((context.message.metadata.timeout as TimeoutContext["timeout"] | undefined)?.perAttempt, "timeout.perAttempt") ??
273
+ defaultPerAttempt;
262
274
 
263
275
  if (!perAttempt) {
264
276
  // No timeout - passthrough
@@ -266,8 +278,12 @@ export function createTimeoutMiddleware(options: Pick<TimeoutOptions, "perAttemp
266
278
  return;
267
279
  }
268
280
 
281
+ // The limit of one try is at most the remaining time of the call (roadmap R10)
282
+ const deadline = deadlineOf(context.message.metadata);
283
+ const limit = deadline === undefined ? perAttempt : Math.max(0, Math.min(perAttempt, deadline - Date.now()));
284
+
269
285
  // A retry middleware outside this one can start a new attempt
270
- yield* runWithTimeout(next, context, perAttempt, message, true);
286
+ yield* runWithTimeout(next, context, limit, message, true);
271
287
  };
272
288
  };
273
289
  }
@@ -275,34 +291,20 @@ export function createTimeoutMiddleware(options: Pick<TimeoutOptions, "perAttemp
275
291
  /**
276
292
  * Create combined timeout middleware with both overall and per-attempt timeouts.
277
293
  *
294
+ * @deprecated Use `resilience({ overall, perAttempt, retry })`. This function puts the two
295
+ * timeouts in one slot, so a retry middleware that you add with another `use()` cannot sit
296
+ * between them: the retry is outside both, or inside both. `resilience` puts the retry between
297
+ * them (roadmap R10).
298
+ *
278
299
  * @param options - Timeout configuration
279
300
  * @returns Middleware function
280
301
  *
281
302
  * @example
282
303
  * ```typescript
283
- * client.use(createCombinedTimeoutMiddleware({
284
- * overall: 5000, // 5 seconds total
285
- * perAttempt: 1000 // 1 second per attempt
286
- * }));
304
+ * client.use(resilience({ overall: seconds(5), perAttempt: seconds(1), retry: { maxRetries: 2 } }));
287
305
  * ```
288
306
  */
289
307
  export function createCombinedTimeoutMiddleware(options: TimeoutOptions): TypedClientMiddleware<TimeoutContext, {}> {
290
308
  const { overall, perAttempt, message = "Request timeout" } = options;
291
-
292
- return <TReq, TRes>(next: ClientRunner<TReq, TRes>): ClientRunner<TReq, TRes> => {
293
- // Compose both timeout middlewares
294
- let composed: ClientRunner<TReq, TRes> = next;
295
-
296
- if (perAttempt) {
297
- const perAttemptMiddleware = createTimeoutMiddleware({ perAttempt, message: `${message} (per-attempt)` }) as ClientMiddleware<TReq, TRes>;
298
- composed = perAttemptMiddleware(composed);
299
- }
300
-
301
- if (overall) {
302
- const overallMiddleware = createOverallTimeoutMiddleware({ overall, message: `${message} (overall)` }) as ClientMiddleware<TReq, TRes>;
303
- composed = overallMiddleware(composed);
304
- }
305
-
306
- return composed;
307
- };
309
+ return resilience({ overall, perAttempt, message }) as TypedClientMiddleware<TimeoutContext, {}>;
308
310
  }
@@ -14,6 +14,7 @@
14
14
 
15
15
  import type { TypedClientMiddleware } from "../types.js";
16
16
  import type { TracingContext } from "./contexts.js";
17
+ import { TRACEPARENT_METADATA_KEY, formatTraceparent, isW3cIds, newSpanId, newTraceId, parseTraceparent } from "../../tracing/traceparent.js";
17
18
 
18
19
  /**
19
20
  * Tracing Configuration
@@ -98,9 +99,10 @@ function generateId(): string {
98
99
  export function createTracingMiddleware(
99
100
  options: TracingOptions = {}
100
101
  ): TypedClientMiddleware<TracingContext, {}> {
102
+ // W3C ids by default, so the server continues the trace (roadmap R16)
101
103
  const {
102
- generateTraceId = generateId,
103
- generateSpanId = generateId,
104
+ generateTraceId = newTraceId,
105
+ generateSpanId = newSpanId,
104
106
  getCurrentTrace,
105
107
  serviceName,
106
108
  } = options;
@@ -108,8 +110,9 @@ export function createTracingMiddleware(
108
110
  return (next) => async function* (context) {
109
111
  // Check if there's already tracing metadata
110
112
  if (!context.message.metadata.tracing) {
111
- // Try to get current trace context
112
- const currentTrace = getCurrentTrace?.();
113
+ // Try to get current trace context: the function of the options, or the trace context of
114
+ // the caller (a call that a handler makes on a server carries the span of that handler)
115
+ const currentTrace = getCurrentTrace?.() ?? parseTraceparent(context.message.metadata[TRACEPARENT_METADATA_KEY]) ?? null;
113
116
 
114
117
  if (currentTrace) {
115
118
  // Continue existing trace (create child span)
@@ -132,6 +135,12 @@ export function createTracingMiddleware(
132
135
  }
133
136
  }
134
137
 
138
+ // The W3C trace context of the call: the server continues this trace (R16)
139
+ const tracing = context.message.metadata.tracing;
140
+ if (tracing && isW3cIds(tracing.traceId, tracing.spanId)) {
141
+ context.message.metadata[TRACEPARENT_METADATA_KEY] = formatTraceparent(tracing.traceId, tracing.spanId);
142
+ }
143
+
135
144
  // Continue middleware chain
136
145
  yield* next(context);
137
146
  };
@@ -26,7 +26,13 @@ export interface Method {
26
26
  /** Operation name (e.g., "get", "create", "update", "delete", "watch") */
27
27
  operation: string;
28
28
 
29
- /** Optional version (e.g., "v1", "v2") */
29
+ /**
30
+ * Optional version (e.g., "v1", "v2").
31
+ *
32
+ * @deprecated The wire protocol (v1, roadmap R25) has no version of a procedure: the server
33
+ * ignores this field. A URL strategy can put it in the path. Use a new path for a procedure
34
+ * that changes in a way that is not compatible.
35
+ */
30
36
  version?: string;
31
37
  }
32
38
 
@@ -66,6 +72,8 @@ export interface Metadata {
66
72
  retry?: {
67
73
  attempt: number;
68
74
  maxAttempts: number;
75
+ /** True: a second run of the call changes nothing more, so a retry is safe */
76
+ idempotent?: boolean;
69
77
  };
70
78
 
71
79
  /** Extensible for custom fields */
package/src/clock.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The clock of the core (roadmap R35). The retry, the circuit breaker and the WebSocket client
3
+ * read the time and start their timers only through a clock. Thus a test gives a fake clock, and
4
+ * the timing is exact and repeatable.
5
+ *
6
+ * The clock comes from `@mark1russell7/service/clock` (roadmap R37). The entry imports nothing
7
+ * else, and it has 228 bytes of gzip. The `fakeClock` of `@mark1russell7/service/testing` fits here.
8
+ * A `Cancel` of the platform also has `unref`. The circuit breaker uses it for its reset: a breaker
9
+ * must not keep a process alive for its reset time (BUGS-2026-07 M4).
10
+ */
11
+
12
+ import { platformClock, type Cancel, type Clock } from "@mark1russell7/service/clock";
13
+
14
+ export type { Cancel, Clock };
15
+
16
+ /**
17
+ * The clock of the platform: `performance.now()` and `setTimeout`. Its timers keep a Node process
18
+ * alive, as `setTimeout` does: a script that waits for a retry does not end during the wait. The
19
+ * `realClock` of the service package is different: its timers do not keep a process alive.
20
+ */
21
+ export const realClock: Clock = platformClock();
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Typed durations (roadmap R11).
3
+ *
4
+ * Each public option of this package that is a time takes a `Duration`: a quantity of
5
+ * `@mark1russell7/quantity` with a time unit. A plain number is a type error, so `timeout: 5`
6
+ * cannot mean five milliseconds in one place and five seconds in another. The wire keeps
7
+ * milliseconds (`procedure-timeout`), and the internals convert each duration one time, when an
8
+ * option is read.
9
+ *
10
+ * `@mark1russell7/quantity` has no `Duration` type yet. This alias narrows its `Quantity` to the
11
+ * time units, so the type also refuses `{ value: 5, unit: "kg" }`.
12
+ */
13
+
14
+ import { quantity, valueIn, type Quantity } from "@mark1russell7/quantity";
15
+
16
+ /** The canonical time units of `@mark1russell7/quantity` that a duration of this package uses. */
17
+ export type TimeUnit = "ns" | "us" | "ms" | "s" | "min" | "h" | "d";
18
+
19
+ /** A time: a quantity with a time unit. */
20
+ export type Duration = Quantity & { readonly unit: TimeUnit };
21
+
22
+ const TIME_UNITS: ReadonlySet<string> = new Set<TimeUnit>(["ns", "us", "ms", "s", "min", "h", "d"]);
23
+
24
+ /** This function makes a duration of milliseconds. */
25
+ export const ms = (value: number): Duration => quantity(value, "ms") as Duration;
26
+ /** This function makes a duration of seconds. */
27
+ export const seconds = (value: number): Duration => quantity(value, "s") as Duration;
28
+ /** This function makes a duration of minutes. */
29
+ export const minutes = (value: number): Duration => quantity(value, "min") as Duration;
30
+ /** This function makes a duration of hours. */
31
+ export const hours = (value: number): Duration => quantity(value, "h") as Duration;
32
+
33
+ /** True when a value is a duration: a finite number with a time unit. */
34
+ export function isDuration(value: unknown): value is Duration {
35
+ if (value === null || typeof value !== "object") return false;
36
+ const { value: amount, unit } = value as { value?: unknown; unit?: unknown };
37
+ return typeof amount === "number" && Number.isFinite(amount) && typeof unit === "string" && TIME_UNITS.has(unit);
38
+ }
39
+
40
+ /**
41
+ * The number of milliseconds of a duration. A value that is not a duration (a plain number, or
42
+ * a quantity of another kind) throws a `TypeError` that names the option.
43
+ */
44
+ export function toMs(duration: Duration, option = "duration"): number {
45
+ if (!isDuration(duration)) {
46
+ throw new TypeError(
47
+ `${option} must be a duration such as ms(500) or seconds(2), got ${JSON.stringify(duration)}. Plain numbers are not durations (roadmap R11).`,
48
+ );
49
+ }
50
+ return valueIn(duration, "ms");
51
+ }
52
+
53
+ /** The milliseconds of an optional duration, or undefined. */
54
+ export function optionalMs(duration: Duration | undefined, option?: string): number | undefined {
55
+ return duration === undefined ? undefined : toMs(duration, option);
56
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,10 @@
6
6
  *
7
7
  * This entry point is for Node. It exports the browser-safe entry point
8
8
  * ("@procedures/client/browser") and the server transports, which use `http` and `ws`.
9
+ *
10
+ * The `WebSocketTransport` of this entry uses the global `WebSocket`, because some applications
11
+ * bundle this entry for a browser. The Node entry "@procedures/client/node" has a
12
+ * `WebSocketTransport` whose sockets are of the `ws` package: a drop ends them at once.
9
13
  */
10
14
 
11
15
  export * from "./browser.js";
@@ -0,0 +1,186 @@
1
+ /**
2
+ * The state machines of the core as data (roadmap R35): the connection of the WebSocket client
3
+ * and the circuit breaker. The documents have the format `mark1russell7.machine/1` of
4
+ * `@mark1russell7/algebra`. They are the specification, and the code implements them. Their knobs
5
+ * are the default times of the core (`TIMER_DEFAULTS`), and their timers say which knob gives each
6
+ * wait.
7
+ *
8
+ * The code reports each change of its state as a `MachineTransition` (the option `onTransition`).
9
+ * `replayTransitions()` replays the reports of a run against a document, so a test finds each
10
+ * change that the document does not allow. It uses `replay` of `@mark1russell7/algebra/replay`
11
+ * (roadmap R37). That entry imports nothing else, and it has 418 bytes of gzip.
12
+ */
13
+
14
+ import { replay } from "@mark1russell7/algebra/replay";
15
+ import { TIMER_DEFAULTS } from "./timers.js";
16
+
17
+ /** A change of state that the code reports. It has the fields of a lifecycle event of `@mark1russell7/service`. */
18
+ export interface MachineTransition {
19
+ /** The id of the document. */
20
+ readonly machine: string;
21
+ /** The instance: one connection, one breaker. */
22
+ readonly instance: string;
23
+ readonly from: string;
24
+ readonly on: string;
25
+ readonly to: string;
26
+ /** The time of the clock of the instance, in milliseconds. */
27
+ readonly at: number;
28
+ }
29
+
30
+ /** A time of a timer: one knob, in a unit. */
31
+ export interface MachineDuration {
32
+ readonly knob: string;
33
+ readonly unit: string;
34
+ }
35
+
36
+ /** A timer of a document: the events that start and stop it, and the event that it gives. */
37
+ export interface MachineTimer {
38
+ readonly duration: MachineDuration;
39
+ readonly arms: readonly string[];
40
+ readonly cancels?: readonly string[] | undefined;
41
+ readonly fires: string;
42
+ /** A timer that starts again after it fires, with a longer wait each time. */
43
+ readonly backoff?: { readonly resets: readonly string[] } | undefined;
44
+ readonly note?: string | undefined;
45
+ }
46
+
47
+ /** A transition of a document. */
48
+ export interface MachineTransitionDef {
49
+ readonly id: string;
50
+ readonly from: string | readonly string[];
51
+ readonly on: string | readonly string[];
52
+ readonly to: string;
53
+ readonly timer?: string | undefined;
54
+ readonly note?: string | undefined;
55
+ }
56
+
57
+ /** A machine document: the subset of `mark1russell7.machine/1` that the core uses. */
58
+ export interface MachineDocument {
59
+ readonly format: "mark1russell7.machine/1";
60
+ readonly id: string;
61
+ readonly title: string;
62
+ readonly layer: "self";
63
+ readonly version: string;
64
+ readonly sources: ReadonlyArray<{ readonly ref: string; readonly rung: "implemented" }>;
65
+ readonly states: Readonly<Record<string, { readonly initial?: boolean; readonly final?: boolean; readonly note?: string }>>;
66
+ readonly events: Readonly<Record<string, { readonly kind: "rx" | "tx" | "app" | "timer" | "local"; readonly note?: string }>>;
67
+ readonly knobs: Readonly<Record<string, { readonly key: string; readonly scope: string; readonly unit: string | null; readonly default: number | null }>>;
68
+ readonly timers: Readonly<Record<string, MachineTimer>>;
69
+ readonly transitions: readonly MachineTransitionDef[];
70
+ }
71
+
72
+ const SOURCE = [{ ref: "packages/client/src (the core implements this document)", rung: "implemented" as const }];
73
+ const ms = (knob: string): MachineDuration => ({ knob, unit: "ms" });
74
+ const client = TIMER_DEFAULTS.webSocketClient;
75
+
76
+ /** The connection of the WebSocket client (`WebSocketTransport`). */
77
+ export const WEBSOCKET_CONNECTION_MACHINE: MachineDocument = {
78
+ format: "mark1russell7.machine/1",
79
+ id: "procedures.machine.websocket-connection",
80
+ title: "The connection of the WebSocket client",
81
+ layer: "self",
82
+ version: "0.2.3",
83
+ sources: SOURCE,
84
+ states: {
85
+ DISCONNECTED: { initial: true, note: "no socket" },
86
+ CONNECTING: { note: "the handshake, with its time limit" },
87
+ CONNECTED: { note: "the calls and the heartbeat" },
88
+ RECONNECTING: { note: "the backoff before the next try" },
89
+ DISCONNECTING: { note: "close() ends the socket and the calls" },
90
+ },
91
+ events: {
92
+ connect: { kind: "app", note: "the transport starts" },
93
+ "reconnect-due": { kind: "timer" },
94
+ open: { kind: "rx", note: "the handshake ends" },
95
+ drop: { kind: "local", note: "the socket closes, or an error ends it" },
96
+ "handshake-timeout": { kind: "timer" },
97
+ "ping-due": { kind: "timer" },
98
+ pong: { kind: "rx" },
99
+ "pong-timeout": { kind: "timer", note: "a drop after the last allowed miss" },
100
+ "give-up": { kind: "local", note: "a drop without reconnect, or without tries left" },
101
+ close: { kind: "app", note: "close() of the application" },
102
+ closed: { kind: "local" },
103
+ },
104
+ knobs: {
105
+ connection_timeout: { key: "connectionTimeout", scope: "client", unit: "ms", default: client.connectionTimeoutMs },
106
+ request_timeout: { key: "requestTimeout", scope: "client", unit: "ms", default: client.requestTimeoutMs },
107
+ heartbeat_interval: { key: "heartbeat.interval", scope: "client", unit: "ms", default: client.heartbeatIntervalMs },
108
+ heartbeat_timeout: { key: "heartbeat.timeout", scope: "client", unit: "ms", default: client.heartbeatTimeoutMs },
109
+ heartbeat_misses: { key: "heartbeat.misses", scope: "client", unit: "count", default: client.heartbeatMisses },
110
+ reconnect_initial_delay: { key: "reconnect.initialDelay", scope: "client", unit: "ms", default: client.reconnectInitialDelayMs },
111
+ reconnect_max_delay: { key: "reconnect.maxDelay", scope: "client", unit: "ms", default: client.reconnectMaxDelayMs },
112
+ },
113
+ timers: {
114
+ handshake: { duration: ms("connection_timeout"), arms: ["enter:CONNECTING"], cancels: ["open", "drop", "close"], fires: "handshake-timeout" },
115
+ reconnect: {
116
+ duration: ms("reconnect_max_delay"),
117
+ arms: ["enter:RECONNECTING"],
118
+ cancels: ["close"],
119
+ fires: "reconnect-due",
120
+ backoff: { resets: ["open"] },
121
+ note: "the longest wait: try n waits a random time up to min(maxDelay, initialDelay × 2^(n-1))",
122
+ },
123
+ ping: { duration: ms("heartbeat_interval"), arms: ["enter:CONNECTED", "ping-due"], cancels: ["drop", "close"], fires: "ping-due" },
124
+ pong: { duration: ms("heartbeat_timeout"), arms: ["ping-due"], cancels: ["pong", "drop", "close"], fires: "pong-timeout" },
125
+ },
126
+ transitions: [
127
+ { id: "connect", from: "DISCONNECTED", on: "connect", to: "CONNECTING" },
128
+ { id: "reconnect", from: "RECONNECTING", on: "reconnect-due", to: "CONNECTING", timer: "reconnect" },
129
+ { id: "open", from: "CONNECTING", on: "open", to: "CONNECTED" },
130
+ {
131
+ id: "drop",
132
+ from: ["CONNECTING", "CONNECTED", "RECONNECTING"],
133
+ on: ["drop", "handshake-timeout", "pong-timeout"],
134
+ to: "RECONNECTING",
135
+ note: "the code reports each of them as drop",
136
+ },
137
+ { id: "give-up", from: ["CONNECTING", "CONNECTED", "RECONNECTING"], on: "give-up", to: "DISCONNECTED" },
138
+ { id: "close", from: ["DISCONNECTED", "CONNECTING", "CONNECTED", "RECONNECTING"], on: "close", to: "DISCONNECTING" },
139
+ { id: "closed", from: "DISCONNECTING", on: "closed", to: "DISCONNECTED" },
140
+ ],
141
+ };
142
+
143
+ /** The circuit breaker (`createCircuitBreakerMiddleware`). */
144
+ export const CIRCUIT_BREAKER_MACHINE: MachineDocument = {
145
+ format: "mark1russell7.machine/1",
146
+ id: "procedures.machine.circuit-breaker",
147
+ title: "The circuit breaker",
148
+ layer: "self",
149
+ version: "0.2.3",
150
+ sources: SOURCE,
151
+ states: {
152
+ CLOSED: { initial: true, note: "each call goes through. The window counts the failures of availability." },
153
+ OPEN: { note: "each call fails at once, until the reset time" },
154
+ HALF_OPEN: { note: "a limited number of probes go through" },
155
+ },
156
+ events: {
157
+ "failure-threshold": { kind: "local", note: "the window has the threshold of failures" },
158
+ "reset-timeout": { kind: "timer" },
159
+ "probe-failure": { kind: "local" },
160
+ "probe-successes": { kind: "local", note: "the threshold of successes of the probes" },
161
+ },
162
+ knobs: {
163
+ failure_threshold: { key: "failureThreshold", scope: "client", unit: "count", default: 5 },
164
+ failure_window: { key: "failureWindow", scope: "client", unit: "ms", default: TIMER_DEFAULTS.circuitBreaker.failureWindowMs },
165
+ reset_timeout: { key: "resetTimeout", scope: "client", unit: "ms", default: TIMER_DEFAULTS.circuitBreaker.resetTimeoutMs },
166
+ success_threshold: { key: "successThreshold", scope: "client", unit: "count", default: 2 },
167
+ },
168
+ timers: {
169
+ reset: { duration: ms("reset_timeout"), arms: ["failure-threshold", "probe-failure"], cancels: ["probe-successes"], fires: "reset-timeout" },
170
+ },
171
+ transitions: [
172
+ { id: "trip", from: "CLOSED", on: "failure-threshold", to: "OPEN" },
173
+ { id: "reset", from: "OPEN", on: "reset-timeout", to: "HALF_OPEN", timer: "reset" },
174
+ { id: "reopen", from: "HALF_OPEN", on: "probe-failure", to: "OPEN" },
175
+ { id: "recover", from: "HALF_OPEN", on: "probe-successes", to: "CLOSED" },
176
+ ],
177
+ };
178
+
179
+ /**
180
+ * This function replays the reported transitions of one document. Each instance starts in the
181
+ * state that its first transition leaves. Each transition must leave the state where the replay
182
+ * is, and the document must allow it. The function gives each disagreement.
183
+ */
184
+ export function replayTransitions(machine: MachineDocument, events: readonly MachineTransition[]): string[] {
185
+ return replay(machine, events, { machine: machine.id });
186
+ }
package/src/node.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The Node entry of `@procedures/client`: `@procedures/client/node`.
3
+ *
4
+ * This entry exports the client transports with the defaults of Node. Its `WebSocketTransport`
5
+ * uses the `WebSocket` class of the `ws` package, so a drop ends a socket at once with
6
+ * `terminate()` (finding of roadmap R36). The other entries keep the global `WebSocket`: a browser
7
+ * bundle imports `@procedures/client/browser`, and that entry does not import `ws`.
8
+ */
9
+
10
+ export { WebSocketTransport } from "./adapters/websocket/client/node.js";
11
+ export type { WebSocketTransportOptions, WebSocketClass } from "./adapters/websocket/client/types.js";
@@ -25,16 +25,14 @@ import type {
25
25
  } from "./types.js";
26
26
  import { pathToKey } from "./types.js";
27
27
  import { isDataDriven } from "./ref.js";
28
+ import { inferredEffectsOf } from "./effects.js";
28
29
  import { callMatches } from "./policy.js";
29
30
 
30
31
  /** The standard resource names. An application adds its own names. */
31
32
  export const STANDARD_RESOURCES = ["fs", "network", "process", "registry"] as const;
32
33
 
33
- /**
34
- * The metadata key of the remaining time of a remote call, in milliseconds. A server uses it
35
- * only to make its deadline earlier.
36
- */
37
- export const TIMEOUT_METADATA_KEY = "procedure-timeout";
34
+ // The metadata key of the remaining time of a remote call, in milliseconds (client/deadline.ts)
35
+ export { TIMEOUT_METADATA_KEY } from "../client/deadline.js";
38
36
 
39
37
  /** The capability without limits: the capability of a host that sets none. */
40
38
  export const UNLIMITED_CAPABILITY: Capability = Object.freeze({});
@@ -97,11 +95,12 @@ export interface ToolHints {
97
95
  /**
98
96
  * The MCP hints of a procedure, from its declared effects. A procedure without a declaration,
99
97
  * or a data-driven one (its operands decide what it does), gets no hints: then the MCP defaults
100
- * apply (not read-only, destructive, open world).
98
+ * apply (not read-only, destructive, open world). A procedure that `procedure.define` made gets
99
+ * the hints of the effects that the host found for its body (`inferredEffects`, roadmap R22).
101
100
  */
102
101
  export function toolHints(procedure: AnyProcedure): ToolHints {
103
- const effects = effectsOf(procedure);
104
- if (!effects || isDataDriven(procedure)) return {};
102
+ const effects = isDataDriven(procedure) ? inferredEffectsOf(procedure) : effectsOf(procedure);
103
+ if (!effects) return {};
105
104
  if (isReadOnly(effects)) {
106
105
  return { readOnlyHint: true, openWorldHint: isOpenWorld(effects) };
107
106
  }