@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
@@ -63,4 +63,16 @@ export class LruMap<K, V> {
63
63
  clear(): void {
64
64
  this.entries.clear();
65
65
  }
66
+
67
+ /** Delete each key whose value matches. This does not count as a use. The number deleted. */
68
+ deleteWhere(matches: (value: V, key: K) => boolean): number {
69
+ let deleted = 0;
70
+ for (const [key, value] of this.entries) {
71
+ if (matches(value, key)) {
72
+ this.entries.delete(key);
73
+ deleted++;
74
+ }
75
+ }
76
+ return deleted;
77
+ }
66
78
  }
@@ -10,6 +10,8 @@
10
10
  import type { ClientMiddleware, ClientRunner, ClientContext, TypedClientMiddleware } from "../types.js";
11
11
  import type { RateLimitContext } from "./contexts.js";
12
12
  import { abortedItem } from "./items.js";
13
+ import { optionalMs, type Duration } from "../../duration.js";
14
+ import { TIMER_DEFAULTS } from "../../timers.js";
13
15
 
14
16
  /**
15
17
  * Rate limiting options.
@@ -25,7 +27,7 @@ export interface RateLimitOptions {
25
27
  * Time window in milliseconds.
26
28
  * @default 60000 (1 minute)
27
29
  */
28
- window?: number;
30
+ window?: Duration;
29
31
 
30
32
  /**
31
33
  * Behavior when rate limit is exceeded.
@@ -81,7 +83,6 @@ export class RateLimitError extends Error {
81
83
  */
82
84
  interface QueuedRequest {
83
85
  resolve: () => void;
84
- reject: (error: Error) => void;
85
86
  timestamp: number;
86
87
  }
87
88
 
@@ -96,14 +97,14 @@ interface QueuedRequest {
96
97
  * // Reject strategy (default)
97
98
  * client.use(createRateLimitMiddleware({
98
99
  * maxRequests: 100, // 100 requests
99
- * window: 60000, // per minute
100
+ * window: minutes(1),
100
101
  * strategy: "reject" // Throw error when exceeded
101
102
  * }));
102
103
  *
103
104
  * // Queue strategy
104
105
  * client.use(createRateLimitMiddleware({
105
106
  * maxRequests: 10,
106
- * window: 1000,
107
+ * window: seconds(1),
107
108
  * strategy: "queue", // Queue requests when exceeded
108
109
  * maxQueueSize: 50
109
110
  * }));
@@ -114,12 +115,12 @@ export function createRateLimitMiddleware(
114
115
  ): TypedClientMiddleware<RateLimitContext, {}> {
115
116
  const {
116
117
  maxRequests = 100,
117
- window = 60000,
118
118
  strategy = "reject",
119
119
  maxQueueSize = 100,
120
120
  message = "Rate limit exceeded",
121
121
  onRateLimitExceeded,
122
122
  } = options;
123
+ const window = optionalMs(options.window, "window") ?? TIMER_DEFAULTS.rateLimit.windowMs;
123
124
 
124
125
  // Token bucket state
125
126
  let tokens = maxRequests;
@@ -206,7 +207,6 @@ export function createRateLimitMiddleware(
206
207
  signal?.removeEventListener("abort", onAbort);
207
208
  resolve(true);
208
209
  },
209
- reject: () => resolve(false),
210
210
  timestamp: Date.now(),
211
211
  };
212
212
  // A queued request whose caller aborts leaves the queue (before, it kept its place)
@@ -273,9 +273,9 @@ export function createRateLimitMiddleware(
273
273
  * @example
274
274
  * ```typescript
275
275
  * client.use(createPerServiceRateLimiter({
276
- * users: { maxRequests: 100, window: 60000 },
277
- * orders: { maxRequests: 50, window: 60000 },
278
- * default: { maxRequests: 200, window: 60000 }
276
+ * users: { maxRequests: 100, window: minutes(1) },
277
+ * orders: { maxRequests: 50, window: minutes(1) },
278
+ * default: { maxRequests: 200, window: minutes(1) }
279
279
  * }));
280
280
  * ```
281
281
  */
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The resilience preset (roadmap R10): the timeouts, the cache, the retry, the circuit breaker and
3
+ * the rate limit of a client, in the one order that works.
4
+ *
5
+ * The first layer is the outermost one:
6
+ *
7
+ * 1. The overall timeout: it covers the whole call, retries included.
8
+ * 2. The cache: a hit makes no request, so no try and no slot of the rate limit.
9
+ * 3. The retry: it starts each try.
10
+ * 4. The circuit breaker: it counts each try, and an open breaker stops a retry loop.
11
+ * 5. The rate limit: each try takes a slot.
12
+ * 6. The timeout of one try: inside the retry, so the retry can start the next try.
13
+ *
14
+ * Each order pitfall of a hand-made stack is impossible here: an overall timeout inside the
15
+ * retry, a per-try timeout outside it, or a rate limit outside the retry. The deadline of the
16
+ * call goes to the server as `procedure-timeout` with each try (`client/deadline.ts`).
17
+ */
18
+
19
+ import type { ClientMiddleware, ClientRunner, TypedClientMiddleware } from "../types.js";
20
+ import type { CacheContext, CircuitBreakerContext, RateLimitContext, RetryContext, TimeoutContext } from "./contexts.js";
21
+ import { createCacheMiddleware, type CacheOptions } from "./cache.js";
22
+ import { createCircuitBreakerMiddleware, type CircuitBreakerOptions } from "./circuit-breaker.js";
23
+ import { createRateLimitMiddleware, type RateLimitOptions } from "./rate-limit.js";
24
+ import { createRetryMiddleware, type RetryOptions } from "./retry.js";
25
+ import { createOverallTimeoutMiddleware, createTimeoutMiddleware } from "./timeout.js";
26
+ import type { Duration } from "../../duration.js";
27
+
28
+ /** The parts of the preset. An absent part is not in the stack. */
29
+ export interface ResilienceOptions {
30
+ /** The time of the whole call, retries included. */
31
+ overall?: Duration | undefined;
32
+ /** The time of one try. It is at most the remaining time of the call. */
33
+ perAttempt?: Duration | undefined;
34
+ /** The retry. Its `idempotent` option decides which calls go again after an indeterminate try. */
35
+ retry?: RetryOptions | undefined;
36
+ /** The circuit breaker. */
37
+ breaker?: CircuitBreakerOptions | undefined;
38
+ /** The cache of the reads that opt in. */
39
+ cache?: CacheOptions | undefined;
40
+ /** The rate limit of the tries. */
41
+ rateLimit?: RateLimitOptions | undefined;
42
+ /** The message of a timeout error. */
43
+ message?: string | undefined;
44
+ }
45
+
46
+ /** The context that the preset gives: the context of each part that it can have. */
47
+ export type ResilienceContext = TimeoutContext & Partial<RetryContext & CacheContext & CircuitBreakerContext & RateLimitContext>;
48
+
49
+ /**
50
+ * This function gives one middleware with the parts of `options` in the correct order (see the
51
+ * file comment). Add it with one `use()`.
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * const client = new Client(transport).use(
56
+ * resilience({
57
+ * overall: seconds(10),
58
+ * perAttempt: seconds(3),
59
+ * retry: { maxRetries: 2, idempotent: idempotentIn(registry) },
60
+ * breaker: { failureThreshold: 5 },
61
+ * }),
62
+ * );
63
+ * ```
64
+ */
65
+ export function resilience(options: ResilienceOptions): TypedClientMiddleware<ResilienceContext, {}> {
66
+ const layers: ClientMiddleware[] = [];
67
+ const message = options.message;
68
+ if (options.overall) {
69
+ layers.push(createOverallTimeoutMiddleware({ overall: options.overall, ...(message ? { message: `${message} (overall)` } : {}) }) as ClientMiddleware);
70
+ }
71
+ if (options.cache) layers.push(createCacheMiddleware(options.cache) as ClientMiddleware);
72
+ if (options.retry) layers.push(createRetryMiddleware(options.retry) as ClientMiddleware);
73
+ if (options.breaker) layers.push(createCircuitBreakerMiddleware(options.breaker) as ClientMiddleware);
74
+ if (options.rateLimit) layers.push(createRateLimitMiddleware(options.rateLimit) as ClientMiddleware);
75
+ if (options.perAttempt) {
76
+ layers.push(createTimeoutMiddleware({ perAttempt: options.perAttempt, ...(message ? { message: `${message} (per-attempt)` } : {}) }) as ClientMiddleware);
77
+ }
78
+ const composed = <TReq, TRes>(next: ClientRunner<TReq, TRes>): ClientRunner<TReq, TRes> =>
79
+ layers.reduceRight<ClientRunner<TReq, TRes>>((inner, layer) => (layer as ClientMiddleware<TReq, TRes>)(inner), next);
80
+ return composed as TypedClientMiddleware<ResilienceContext, {}>;
81
+ }
@@ -5,9 +5,14 @@
5
5
  * Works with any transport!
6
6
  */
7
7
 
8
- import type { ClientRunner, ClientContext, ResponseItem, TypedClientMiddleware } from "../types.js";
8
+ import type { ClientRunner, ClientContext, Method, ResponseItem, TypedClientMiddleware } from "../types.js";
9
9
  import type { RetryContext } from "./contexts.js";
10
- import { abortedItem, sleep, thrownItem } from "./items.js";
10
+ import { abortedItem, deliveryOf, retryAfterOf, sleep, thrownItem } from "./items.js";
11
+ import { realClock, type Clock } from "../../clock.js";
12
+ import { backoffDelay } from "@mark1russell7/service/backoff";
13
+ import { IDEMPOTENCY_KEY_METADATA, idempotencyKeyOf, newIdempotencyKey } from "../../procedures/idempotency.js";
14
+ import { isDuration, optionalMs, type Duration } from "../../duration.js";
15
+ import { TIMER_DEFAULTS } from "../../timers.js";
11
16
 
12
17
  /**
13
18
  * Retry middleware options.
@@ -20,11 +25,17 @@ export interface RetryOptions {
20
25
  maxRetries?: number;
21
26
 
22
27
  /**
23
- * Base delay in milliseconds for exponential backoff.
28
+ * The clock of the backoff and of the budget (roadmap R35). The default is the clock of the
29
+ * platform. A test gives a fake clock.
30
+ */
31
+ clock?: Clock;
32
+
33
+ /**
34
+ * Base delay for exponential backoff, for example `ms(500)`.
24
35
  * Actual delay will be: baseDelay * 2^attempt + jitter
25
- * @default 1000
36
+ * @default 1 second
26
37
  */
27
- retryDelay?: number;
38
+ retryDelay?: Duration;
28
39
 
29
40
  /**
30
41
  * The factor of the delay from one attempt to the next.
@@ -33,21 +44,47 @@ export interface RetryOptions {
33
44
  backoffMultiplier?: number;
34
45
 
35
46
  /**
36
- * The longest delay between two attempts, in milliseconds.
47
+ * The longest delay between two attempts.
37
48
  * @default no limit
38
49
  */
39
- maxDelay?: number;
50
+ maxDelay?: Duration;
40
51
 
41
52
  /**
42
- * Maximum jitter as a fraction of the delay (0-1).
43
- * Adds randomness to prevent thundering herd.
44
- * @default 0.1 (10% jitter)
53
+ * The random part of each delay, from 0 to 1. With 1 (full jitter), the delay is a random time
54
+ * from 0 to the backoff, so the clients that failed together do not retry together. 0 gives the
55
+ * backoff exactly.
56
+ * @default 1 (full jitter; roadmap R12: before, 0.1 gave a delay within 10 percent of the backoff)
45
57
  */
46
58
  jitter?: number;
47
59
 
60
+ /**
61
+ * The retry budget of this middleware: each call adds `ratio` of a retry to the budget, and each
62
+ * retry takes one. When the budget is empty, an error goes to the caller without a retry, so a
63
+ * failing service gets at most `ratio` more requests, not `maxRetries` times more. Besides the
64
+ * budget, `minPerSecond` retries a second are always allowed. `false` turns the budget off.
65
+ * @default { ratio: 0.2, minPerSecond: 10 }
66
+ */
67
+ budget?: { ratio?: number; minPerSecond?: number } | false;
68
+
69
+ /**
70
+ * The calls that the middleware can send again after a try with an indeterminate outcome: the
71
+ * request left, and no answer came (a reset, a closed connection, the timeout of one try). The
72
+ * server can have run such a call, so only an idempotent call goes again (roadmap R1).
73
+ *
74
+ * - a function: true for an idempotent method. `idempotentIn(registry)` reads the effects of
75
+ * the procedures that the client knows.
76
+ * - `true`: every call. Use it only with a server that removes duplicate calls by their
77
+ * idempotency key (`ProcedureServer` does).
78
+ * - absent: only a call with `retry: { idempotent: true }` in its context.
79
+ *
80
+ * A try that did not leave the client (a refused connection) and an error that the server
81
+ * marks retryable go again in each case.
82
+ */
83
+ idempotent?: boolean | ((method: Method) => boolean | undefined);
84
+
48
85
  /**
49
86
  * Custom function to determine if a request should be retried.
50
- * Takes precedence over Status.retryable flag.
87
+ * Takes precedence over Status.retryable flag. The rule of `idempotent` still applies.
51
88
  *
52
89
  * @param item - The response item
53
90
  * @param attempt - Current attempt number (0-indexed)
@@ -96,7 +133,7 @@ export interface RetryOptions {
96
133
  * @example
97
134
  * ```typescript
98
135
  * // Create middleware with defaults
99
- * client.use(createRetryMiddleware({ maxRetries: 3 }));
136
+ * client.use(createRetryMiddleware({ maxRetries: 3, retryDelay: ms(500) }));
100
137
  *
101
138
  * // Override per-call via context
102
139
  * await client.call(method, payload, {
@@ -105,54 +142,121 @@ export interface RetryOptions {
105
142
  * ```
106
143
  */
107
144
  export function createRetryMiddleware(options: RetryOptions = {}): TypedClientMiddleware<RetryContext, {}> {
145
+ const defaultRetryDelay = optionalMs(options.retryDelay, "retryDelay") ?? TIMER_DEFAULTS.retry.retryDelayMs;
108
146
  const {
109
- retryDelay: defaultRetryDelay = 1000,
110
- jitter: defaultJitter = 0.1,
147
+ jitter: defaultJitter = 1,
111
148
  shouldRetry: customShouldRetry,
112
149
  onBeforeRetry,
113
150
  onAfterRetry,
151
+ idempotent: idempotentOption,
114
152
  } = options;
115
- const defaultMaxRetries = options.maxRetries ?? 3;
116
- const defaultMultiplier = options.backoffMultiplier ?? 2;
117
- const defaultMaxDelay = options.maxDelay ?? Number.POSITIVE_INFINITY;
153
+ const defaultMaxRetries = options.maxRetries ?? TIMER_DEFAULTS.retry.maxRetries;
154
+ const defaultMultiplier = options.backoffMultiplier ?? TIMER_DEFAULTS.retry.backoffMultiplier;
155
+ const defaultMaxDelay = optionalMs(options.maxDelay, "maxDelay") ?? TIMER_DEFAULTS.retry.maxDelayMs;
118
156
 
119
- /** The overrides of one call (`retry: { ... }` in its context). Before, only maxAttempts was read. */
120
- const overridesOf = (value: unknown): Partial<Pick<RetryContext["retry"], "maxAttempts" | "delay" | "backoffMultiplier" | "maxDelay">> => {
157
+ /**
158
+ * The overrides of one call (`retry: { ... }` in its context), with the delays in milliseconds.
159
+ * Before, only maxAttempts was read.
160
+ */
161
+ const overridesOf = (value: unknown): { maxAttempts?: number; delay?: number; backoffMultiplier?: number; maxDelay?: number } => {
121
162
  if (typeof value !== "object" || value === null) return {};
122
- const out: Partial<Pick<RetryContext["retry"], "maxAttempts" | "delay" | "backoffMultiplier" | "maxDelay">> = {};
123
- for (const key of ["maxAttempts", "delay", "backoffMultiplier", "maxDelay"] as const) {
163
+ const out: { maxAttempts?: number; delay?: number; backoffMultiplier?: number; maxDelay?: number } = {};
164
+ for (const key of ["maxAttempts", "backoffMultiplier"] as const) {
124
165
  const field = (value as Record<string, unknown>)[key];
125
166
  if (typeof field === "number" && Number.isFinite(field) && field >= 0) out[key] = field;
126
167
  }
168
+ for (const key of ["delay", "maxDelay"] as const) {
169
+ const field = (value as Record<string, unknown>)[key];
170
+ if (field !== undefined) {
171
+ if (!isDuration(field)) throw new TypeError(`retry.${key} must be a duration such as ms(500) (roadmap R11)`);
172
+ out[key] = optionalMs(field, `retry.${key}`)!;
173
+ }
174
+ }
127
175
  return out;
128
176
  };
129
177
 
130
- /** The backoff delay of an attempt: exponential, with jitter, at most the maximum delay */
131
- const backoffDelay = (attempt: number, overrides: ReturnType<typeof overridesOf>): number => {
132
- const baseDelay = (overrides.delay ?? defaultRetryDelay) * Math.pow(overrides.backoffMultiplier ?? defaultMultiplier, attempt);
133
- const jitterAmount = defaultJitter * baseDelay * (Math.random() * 2 - 1);
134
- return Math.max(0, Math.min(overrides.maxDelay ?? defaultMaxDelay, baseDelay + jitterAmount));
178
+ /**
179
+ * The backoff delay of an attempt (from 0): exponential, at most the maximum delay, with the
180
+ * random part of `jitter` (full jitter by default). The backoff is `backoffDelay` of
181
+ * `@mark1russell7/service/backoff` (roadmap R37), whose tries start at 1.
182
+ */
183
+ const retryDelayOf = (attempt: number, overrides: ReturnType<typeof overridesOf>): number =>
184
+ backoffDelay(
185
+ {
186
+ base: overrides.delay ?? defaultRetryDelay,
187
+ factor: overrides.backoffMultiplier ?? defaultMultiplier,
188
+ cap: overrides.maxDelay ?? defaultMaxDelay,
189
+ jitter: { kind: "subtract", fraction: Math.min(1, Math.max(0, defaultJitter)) },
190
+ },
191
+ attempt + 1,
192
+ );
193
+
194
+ // The retry budget (R12): a balance of retries that each call fills by `ratio`
195
+ const budget = options.budget === false ? undefined : { ratio: options.budget?.ratio ?? 0.2, minPerSecond: options.budget?.minPerSecond ?? 10 };
196
+ const budgetCap = budget ? Math.max(1, budget.ratio * 100) : 0;
197
+ let balance = budgetCap;
198
+ const clock = options.clock ?? realClock;
199
+ let secondStart = clock.now();
200
+ let retriesThisSecond = 0;
201
+ /** This function takes one retry from the budget, or gives false when the budget is empty. */
202
+ const takeRetry = (): boolean => {
203
+ if (!budget) return true;
204
+ if (balance >= 1) {
205
+ balance -= 1;
206
+ return true;
207
+ }
208
+ const now = clock.now();
209
+ if (now - secondStart >= 1000) {
210
+ secondStart = now;
211
+ retriesThisSecond = 0;
212
+ }
213
+ if (retriesThisSecond < budget.minPerSecond) {
214
+ retriesThisSecond++;
215
+ return true;
216
+ }
217
+ return false;
218
+ };
219
+
220
+ /** True when the call can go again after an indeterminate try: the call says so, or the option. */
221
+ const isIdempotent = (method: Method, value: unknown): boolean => {
222
+ const own = typeof value === "object" && value !== null ? (value as { idempotent?: unknown }).idempotent : undefined;
223
+ if (typeof own === "boolean") return own;
224
+ if (typeof idempotentOption === "function") return idempotentOption(method) === true;
225
+ return idempotentOption === true;
135
226
  };
136
227
 
137
228
  return <TReq, TRes>(next: ClientRunner<TReq, TRes>): ClientRunner<TReq, TRes> => {
138
229
  return async function* (context: ClientContext<TReq>) {
139
230
  // Read from metadata first (user-provided context), then fall back to options
140
- const overrides = overridesOf(context.message.metadata.retry);
141
- const maxRetries = overrides.maxAttempts ?? defaultMaxRetries;
142
- const shouldRetryItem = (item: ResponseItem<TRes>, attempt: number): boolean =>
143
- customShouldRetry
231
+ const given = context.message.metadata.retry;
232
+ const overrides = overridesOf(given);
233
+ // maxAttempts counts the attempts: the first try and the retries (R12: before, the override
234
+ // was used as the number of retries, against its name and its documentation)
235
+ const maxRetries = overrides.maxAttempts !== undefined ? Math.max(0, overrides.maxAttempts - 1) : defaultMaxRetries;
236
+ if (budget) balance = Math.min(budgetCap, balance + budget.ratio);
237
+ const idempotent = isIdempotent(context.message.method, context.message.metadata.retry);
238
+ const shouldRetryItem = (item: ResponseItem<TRes>, attempt: number): boolean => {
239
+ const wanted = customShouldRetry
144
240
  ? customShouldRetry(item, attempt)
145
241
  : item.status.type === "error" && item.status.retryable;
242
+ // A request that can have run goes again only when the call is idempotent (R1). Before,
243
+ // a reset or a timeout of one try sent a write again.
244
+ return wanted && (deliveryOf(item) !== "indeterminate" || idempotent);
245
+ };
146
246
  const signal = context.message.signal;
147
247
  const baseId = context.message.id;
248
+ // One key for all the tries of the call: a server that removes duplicates runs it one time
249
+ const idempotencyKey = idempotencyKeyOf(context.message.metadata) ?? newIdempotencyKey();
250
+ context.message.metadata[IDEMPOTENCY_KEY_METADATA] = idempotencyKey;
148
251
 
149
252
  let attempt = 0;
150
253
  for (;;) {
151
254
  // Update retry metadata with current state
152
255
  context.message.metadata.retry = {
153
- ...overrides,
256
+ ...(typeof given === "object" && given !== null ? (given as object) : {}),
154
257
  attempt,
155
- maxAttempts: maxRetries,
258
+ maxAttempts: maxRetries + 1,
259
+ idempotent,
156
260
  };
157
261
  // Each attempt gets its own request id. The id correlates the frames of one attempt
158
262
  // on the wire: a cancelled attempt can still answer under its id (deep dive TRN-8/9).
@@ -204,7 +308,15 @@ export function createRetryMiddleware(options: RetryOptions = {}): TypedClientMi
204
308
  return;
205
309
  }
206
310
 
207
- let delay = backoffDelay(attempt, overrides);
311
+ // The budget is empty: the error goes to the caller (R12)
312
+ if (!takeRetry()) {
313
+ yield retryItem;
314
+ if (onAfterRetry) await onAfterRetry(false, attempt);
315
+ return;
316
+ }
317
+
318
+ // A server that asks for a wait (Retry-After) gets at least that wait (R12)
319
+ let delay = Math.max(retryDelayOf(attempt, overrides), retryAfterOf(retryItem) ?? 0);
208
320
 
209
321
  // Before retry hook
210
322
  if (onBeforeRetry) {
@@ -225,7 +337,7 @@ export function createRetryMiddleware(options: RetryOptions = {}): TypedClientMi
225
337
  }
226
338
 
227
339
  // The backoff stops at once when the caller aborts (deep dive TRN-8)
228
- if (!(await sleep(delay, signal))) {
340
+ if (!(await sleep(delay, signal, clock))) {
229
341
  yield abortedItem<TRes>(context.message.id);
230
342
  return;
231
343
  }