@procedures/client 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +61 -38
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/adapters/http/client/transport.d.ts.map +1 -1
  5. package/dist/adapters/http/client/transport.js +35 -3
  6. package/dist/adapters/http/client/transport.js.map +1 -1
  7. package/dist/adapters/http/client/types.d.ts +3 -2
  8. package/dist/adapters/http/client/types.d.ts.map +1 -1
  9. package/dist/adapters/http/server/transport.d.ts +2 -2
  10. package/dist/adapters/http/server/transport.d.ts.map +1 -1
  11. package/dist/adapters/http/server/transport.js +53 -20
  12. package/dist/adapters/http/server/transport.js.map +1 -1
  13. package/dist/adapters/http/shared/headers.d.ts +1 -1
  14. package/dist/adapters/http/shared/headers.d.ts.map +1 -1
  15. package/dist/adapters/http/shared/headers.js +7 -6
  16. package/dist/adapters/http/shared/headers.js.map +1 -1
  17. package/dist/adapters/local/client/transport.d.ts +3 -0
  18. package/dist/adapters/local/client/transport.d.ts.map +1 -1
  19. package/dist/adapters/local/client/transport.js +63 -4
  20. package/dist/adapters/local/client/transport.js.map +1 -1
  21. package/dist/adapters/local/client/types.d.ts +15 -0
  22. package/dist/adapters/local/client/types.d.ts.map +1 -1
  23. package/dist/adapters/local/client/types.js.map +1 -1
  24. package/dist/adapters/mock/client/transport.d.ts +3 -2
  25. package/dist/adapters/mock/client/transport.d.ts.map +1 -1
  26. package/dist/adapters/mock/client/transport.js +8 -5
  27. package/dist/adapters/mock/client/transport.js.map +1 -1
  28. package/dist/adapters/mock/client/types.d.ts +7 -6
  29. package/dist/adapters/mock/client/types.d.ts.map +1 -1
  30. package/dist/adapters/port/transport.d.ts +62 -0
  31. package/dist/adapters/port/transport.d.ts.map +1 -0
  32. package/dist/adapters/port/transport.js +94 -0
  33. package/dist/adapters/port/transport.js.map +1 -0
  34. package/dist/adapters/websocket/client/index.d.ts +1 -1
  35. package/dist/adapters/websocket/client/index.d.ts.map +1 -1
  36. package/dist/adapters/websocket/client/index.js.map +1 -1
  37. package/dist/adapters/websocket/client/transport.d.ts +47 -7
  38. package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
  39. package/dist/adapters/websocket/client/transport.js +213 -67
  40. package/dist/adapters/websocket/client/transport.js.map +1 -1
  41. package/dist/adapters/websocket/client/types.d.ts +70 -21
  42. package/dist/adapters/websocket/client/types.d.ts.map +1 -1
  43. package/dist/adapters/websocket/client/types.js.map +1 -1
  44. package/dist/adapters/websocket/server/transport.d.ts +65 -24
  45. package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
  46. package/dist/adapters/websocket/server/transport.js +192 -263
  47. package/dist/adapters/websocket/server/transport.js.map +1 -1
  48. package/dist/adapters/websocket/server/types.d.ts +20 -1
  49. package/dist/adapters/websocket/server/types.d.ts.map +1 -1
  50. package/dist/adapters/websocket/shared/rpc.d.ts +129 -0
  51. package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -0
  52. package/dist/adapters/websocket/shared/rpc.js +425 -0
  53. package/dist/adapters/websocket/shared/rpc.js.map +1 -0
  54. package/dist/browser.d.ts +29 -7
  55. package/dist/browser.d.ts.map +1 -1
  56. package/dist/browser.js +26 -3
  57. package/dist/browser.js.map +1 -1
  58. package/dist/client/call-types.d.ts +10 -10
  59. package/dist/client/call-types.d.ts.map +1 -1
  60. package/dist/client/call-types.js +1 -6
  61. package/dist/client/call-types.js.map +1 -1
  62. package/dist/client/client.d.ts +6 -1
  63. package/dist/client/client.d.ts.map +1 -1
  64. package/dist/client/client.js +23 -9
  65. package/dist/client/client.js.map +1 -1
  66. package/dist/client/deadline.d.ts +38 -0
  67. package/dist/client/deadline.d.ts.map +1 -0
  68. package/dist/client/deadline.js +60 -0
  69. package/dist/client/deadline.js.map +1 -0
  70. package/dist/client/errors/factory.d.ts.map +1 -1
  71. package/dist/client/errors/factory.js +11 -7
  72. package/dist/client/errors/factory.js.map +1 -1
  73. package/dist/client/index.d.ts +9 -2
  74. package/dist/client/index.d.ts.map +1 -1
  75. package/dist/client/index.js +8 -2
  76. package/dist/client/index.js.map +1 -1
  77. package/dist/client/middleware/cache.d.ts +19 -5
  78. package/dist/client/middleware/cache.d.ts.map +1 -1
  79. package/dist/client/middleware/cache.js +106 -18
  80. package/dist/client/middleware/cache.js.map +1 -1
  81. package/dist/client/middleware/circuit-breaker.d.ts +28 -10
  82. package/dist/client/middleware/circuit-breaker.d.ts.map +1 -1
  83. package/dist/client/middleware/circuit-breaker.js +35 -24
  84. package/dist/client/middleware/circuit-breaker.js.map +1 -1
  85. package/dist/client/middleware/contexts.d.ts +16 -10
  86. package/dist/client/middleware/contexts.d.ts.map +1 -1
  87. package/dist/client/middleware/contexts.js.map +1 -1
  88. package/dist/client/middleware/items.d.ts +31 -2
  89. package/dist/client/middleware/items.d.ts.map +1 -1
  90. package/dist/client/middleware/items.js +86 -6
  91. package/dist/client/middleware/items.js.map +1 -1
  92. package/dist/client/middleware/lru.d.ts +2 -0
  93. package/dist/client/middleware/lru.d.ts.map +1 -1
  94. package/dist/client/middleware/lru.js +11 -0
  95. package/dist/client/middleware/lru.js.map +1 -1
  96. package/dist/client/middleware/rate-limit.d.ts +7 -6
  97. package/dist/client/middleware/rate-limit.d.ts.map +1 -1
  98. package/dist/client/middleware/rate-limit.js +9 -6
  99. package/dist/client/middleware/rate-limit.js.map +1 -1
  100. package/dist/client/middleware/resilience.d.ts +61 -0
  101. package/dist/client/middleware/resilience.d.ts.map +1 -0
  102. package/dist/client/middleware/resilience.js +59 -0
  103. package/dist/client/middleware/resilience.js.map +1 -0
  104. package/dist/client/middleware/retry.d.ts +45 -11
  105. package/dist/client/middleware/retry.d.ts.map +1 -1
  106. package/dist/client/middleware/retry.js +102 -22
  107. package/dist/client/middleware/retry.js.map +1 -1
  108. package/dist/client/middleware/timeout.d.ts +15 -12
  109. package/dist/client/middleware/timeout.d.ts.map +1 -1
  110. package/dist/client/middleware/timeout.js +46 -37
  111. package/dist/client/middleware/timeout.js.map +1 -1
  112. package/dist/client/middleware/tracing.d.ts.map +1 -1
  113. package/dist/client/middleware/tracing.js +11 -3
  114. package/dist/client/middleware/tracing.js.map +1 -1
  115. package/dist/client/types.d.ts +9 -1
  116. package/dist/client/types.d.ts.map +1 -1
  117. package/dist/client/types.js.map +1 -1
  118. package/dist/clock.d.ts +19 -0
  119. package/dist/clock.d.ts.map +1 -0
  120. package/dist/clock.js +18 -0
  121. package/dist/clock.js.map +1 -0
  122. package/dist/duration.d.ts +37 -0
  123. package/dist/duration.d.ts.map +1 -0
  124. package/dist/duration.js +44 -0
  125. package/dist/duration.js.map +1 -0
  126. package/dist/machines.d.ts +90 -0
  127. package/dist/machines.d.ts.map +1 -0
  128. package/dist/machines.js +127 -0
  129. package/dist/machines.js.map +1 -0
  130. package/dist/procedures/capability.d.ts +3 -6
  131. package/dist/procedures/capability.d.ts.map +1 -1
  132. package/dist/procedures/capability.js +7 -8
  133. package/dist/procedures/capability.js.map +1 -1
  134. package/dist/procedures/collection/index.d.ts +3 -3
  135. package/dist/procedures/collection/index.d.ts.map +1 -1
  136. package/dist/procedures/collection/index.js +1 -1
  137. package/dist/procedures/collection/index.js.map +1 -1
  138. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  139. package/dist/procedures/collection/procedures.js +49 -1
  140. package/dist/procedures/collection/procedures.js.map +1 -1
  141. package/dist/procedures/collection/schemas.d.ts +33 -0
  142. package/dist/procedures/collection/schemas.d.ts.map +1 -1
  143. package/dist/procedures/collection/schemas.js +68 -0
  144. package/dist/procedures/collection/schemas.js.map +1 -1
  145. package/dist/procedures/collection/storage.d.ts +37 -0
  146. package/dist/procedures/collection/storage.d.ts.map +1 -1
  147. package/dist/procedures/core/array.d.ts.map +1 -1
  148. package/dist/procedures/core/array.js +26 -15
  149. package/dist/procedures/core/array.js.map +1 -1
  150. package/dist/procedures/core/index.d.ts.map +1 -1
  151. package/dist/procedures/core/index.js +8 -0
  152. package/dist/procedures/core/index.js.map +1 -1
  153. package/dist/procedures/core/meta.d.ts.map +1 -1
  154. package/dist/procedures/core/meta.js +2 -1
  155. package/dist/procedures/core/meta.js.map +1 -1
  156. package/dist/procedures/core/object.d.ts.map +1 -1
  157. package/dist/procedures/core/object.js +42 -13
  158. package/dist/procedures/core/object.js.map +1 -1
  159. package/dist/procedures/core/operands.d.ts.map +1 -1
  160. package/dist/procedures/core/operands.js +6 -1
  161. package/dist/procedures/core/operands.js.map +1 -1
  162. package/dist/procedures/core/string.d.ts.map +1 -1
  163. package/dist/procedures/core/string.js +4 -2
  164. package/dist/procedures/core/string.js.map +1 -1
  165. package/dist/procedures/define-procedure.d.ts +19 -7
  166. package/dist/procedures/define-procedure.d.ts.map +1 -1
  167. package/dist/procedures/define-procedure.js +57 -17
  168. package/dist/procedures/define-procedure.js.map +1 -1
  169. package/dist/procedures/effects.d.ts +16 -0
  170. package/dist/procedures/effects.d.ts.map +1 -1
  171. package/dist/procedures/effects.js +33 -0
  172. package/dist/procedures/effects.js.map +1 -1
  173. package/dist/procedures/idempotency.d.ts +73 -0
  174. package/dist/procedures/idempotency.d.ts.map +1 -0
  175. package/dist/procedures/idempotency.js +131 -0
  176. package/dist/procedures/idempotency.js.map +1 -0
  177. package/dist/procedures/index.d.ts +7 -3
  178. package/dist/procedures/index.d.ts.map +1 -1
  179. package/dist/procedures/index.js +6 -2
  180. package/dist/procedures/index.js.map +1 -1
  181. package/dist/procedures/interceptors.d.ts +17 -4
  182. package/dist/procedures/interceptors.d.ts.map +1 -1
  183. package/dist/procedures/interceptors.js +18 -7
  184. package/dist/procedures/interceptors.js.map +1 -1
  185. package/dist/procedures/invoke.d.ts +15 -3
  186. package/dist/procedures/invoke.d.ts.map +1 -1
  187. package/dist/procedures/invoke.js +8 -3
  188. package/dist/procedures/invoke.js.map +1 -1
  189. package/dist/procedures/manifest.d.ts +83 -0
  190. package/dist/procedures/manifest.d.ts.map +1 -0
  191. package/dist/procedures/manifest.js +90 -0
  192. package/dist/procedures/manifest.js.map +1 -0
  193. package/dist/procedures/runtime-programs.d.ts +19 -0
  194. package/dist/procedures/runtime-programs.d.ts.map +1 -0
  195. package/dist/procedures/runtime-programs.js +24 -0
  196. package/dist/procedures/runtime-programs.js.map +1 -0
  197. package/dist/procedures/types.d.ts +16 -0
  198. package/dist/procedures/types.d.ts.map +1 -1
  199. package/dist/procedures/types.js.map +1 -1
  200. package/dist/protocol.d.ts +40 -0
  201. package/dist/protocol.d.ts.map +1 -0
  202. package/dist/protocol.js +58 -0
  203. package/dist/protocol.js.map +1 -0
  204. package/dist/server/host-policy.d.ts +4 -3
  205. package/dist/server/host-policy.d.ts.map +1 -1
  206. package/dist/server/host-policy.js +6 -3
  207. package/dist/server/host-policy.js.map +1 -1
  208. package/dist/server/host-procedures.d.ts +31 -0
  209. package/dist/server/host-procedures.d.ts.map +1 -0
  210. package/dist/server/host-procedures.js +75 -0
  211. package/dist/server/host-procedures.js.map +1 -0
  212. package/dist/server/index.d.ts +3 -3
  213. package/dist/server/index.d.ts.map +1 -1
  214. package/dist/server/index.js +1 -1
  215. package/dist/server/index.js.map +1 -1
  216. package/dist/server/procedure-server.d.ts +110 -4
  217. package/dist/server/procedure-server.d.ts.map +1 -1
  218. package/dist/server/procedure-server.js +269 -17
  219. package/dist/server/procedure-server.js.map +1 -1
  220. package/dist/server/request-auth.d.ts +19 -0
  221. package/dist/server/request-auth.d.ts.map +1 -0
  222. package/dist/server/request-auth.js +32 -0
  223. package/dist/server/request-auth.js.map +1 -0
  224. package/dist/server/types.d.ts +32 -0
  225. package/dist/server/types.d.ts.map +1 -1
  226. package/dist/server/types.js.map +1 -1
  227. package/dist/timers.d.ts +60 -0
  228. package/dist/timers.d.ts.map +1 -0
  229. package/dist/timers.js +33 -0
  230. package/dist/timers.js.map +1 -0
  231. package/dist/tracing/traceparent.d.ts +26 -0
  232. package/dist/tracing/traceparent.d.ts.map +1 -0
  233. package/dist/tracing/traceparent.js +53 -0
  234. package/dist/tracing/traceparent.js.map +1 -0
  235. package/package.json +7 -1
  236. package/src/adapters/http/client/transport.ts +31 -3
  237. package/src/adapters/http/client/types.ts +3 -2
  238. package/src/adapters/http/server/transport.ts +60 -24
  239. package/src/adapters/http/shared/headers.ts +7 -6
  240. package/src/adapters/local/client/transport.ts +70 -5
  241. package/src/adapters/local/client/types.ts +18 -0
  242. package/src/adapters/mock/client/transport.ts +9 -6
  243. package/src/adapters/mock/client/types.ts +7 -6
  244. package/src/adapters/port/transport.ts +124 -0
  245. package/src/adapters/websocket/client/index.ts +1 -0
  246. package/src/adapters/websocket/client/transport.ts +232 -73
  247. package/src/adapters/websocket/client/types.ts +80 -21
  248. package/src/adapters/websocket/server/transport.ts +199 -301
  249. package/src/adapters/websocket/server/types.ts +20 -1
  250. package/src/adapters/websocket/shared/rpc.ts +453 -0
  251. package/src/browser.ts +51 -3
  252. package/src/client/call-types.ts +10 -14
  253. package/src/client/client.ts +28 -9
  254. package/src/client/deadline.ts +64 -0
  255. package/src/client/errors/factory.ts +12 -8
  256. package/src/client/index.ts +21 -3
  257. package/src/client/middleware/cache.ts +120 -23
  258. package/src/client/middleware/circuit-breaker.ts +65 -35
  259. package/src/client/middleware/contexts.ts +17 -10
  260. package/src/client/middleware/items.ts +101 -6
  261. package/src/client/middleware/lru.ts +12 -0
  262. package/src/client/middleware/rate-limit.ts +9 -7
  263. package/src/client/middleware/resilience.ts +81 -0
  264. package/src/client/middleware/retry.ts +146 -34
  265. package/src/client/middleware/timeout.ts +55 -46
  266. package/src/client/middleware/tracing.ts +13 -4
  267. package/src/client/types.ts +9 -1
  268. package/src/clock.ts +21 -0
  269. package/src/duration.ts +56 -0
  270. package/src/machines.ts +186 -0
  271. package/src/procedures/capability.ts +7 -8
  272. package/src/procedures/collection/index.ts +9 -1
  273. package/src/procedures/collection/procedures.ts +57 -0
  274. package/src/procedures/collection/schemas.ts +110 -0
  275. package/src/procedures/collection/storage.ts +38 -0
  276. package/src/procedures/core/array.ts +27 -16
  277. package/src/procedures/core/index.ts +8 -0
  278. package/src/procedures/core/meta.ts +2 -1
  279. package/src/procedures/core/object.ts +48 -14
  280. package/src/procedures/core/operands.ts +7 -0
  281. package/src/procedures/core/string.ts +4 -2
  282. package/src/procedures/define-procedure.ts +73 -20
  283. package/src/procedures/effects.ts +35 -0
  284. package/src/procedures/idempotency.ts +152 -0
  285. package/src/procedures/index.ts +18 -2
  286. package/src/procedures/interceptors.ts +28 -11
  287. package/src/procedures/invoke.ts +19 -5
  288. package/src/procedures/manifest.ts +158 -0
  289. package/src/procedures/runtime-programs.ts +27 -0
  290. package/src/procedures/types.ts +16 -0
  291. package/src/protocol.ts +97 -0
  292. package/src/server/host-policy.ts +8 -5
  293. package/src/server/host-procedures.ts +102 -0
  294. package/src/server/index.ts +5 -2
  295. package/src/server/procedure-server.ts +324 -10
  296. package/src/server/request-auth.ts +35 -0
  297. package/src/server/types.ts +37 -0
  298. package/src/timers.ts +82 -0
  299. package/src/tracing/traceparent.ts +61 -0
@@ -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
+ }
@@ -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
  }
@@ -19,6 +19,10 @@ export {
19
19
  hasOutputSchema,
20
20
  getAllInputSchema,
21
21
  getAllOutputSchema,
22
+ entriesInputSchema,
23
+ entriesOutputSchema,
24
+ changesInputSchema,
25
+ changesOutputSchema,
22
26
  sizeInputSchema,
23
27
  sizeOutputSchema,
24
28
  clearInputSchema,
@@ -37,6 +41,10 @@ export type {
37
41
  DeleteInput,
38
42
  HasInput,
39
43
  GetAllInput,
44
+ EntriesInput,
45
+ CollectionEntry,
46
+ ChangesInput,
47
+ ChangesOutput,
40
48
  SizeInput,
41
49
  ClearInput,
42
50
  GetBatchInput,
@@ -48,4 +56,4 @@ export type {
48
56
  export { createCollectionProcedures } from "./procedures.js";
49
57
 
50
58
  // The storage interface of the core (the shape of client-collections' CollectionStorage)
51
- export type { CollectionStorage, StorageMetadata } from "./storage.js";
59
+ export type { CollectionStorage, StorageChanges, StorageMetadata } from "./storage.js";
@@ -18,6 +18,10 @@ import {
18
18
  hasOutputSchema,
19
19
  getAllInputSchema,
20
20
  getAllOutputSchema,
21
+ entriesInputSchema,
22
+ entriesOutputSchema,
23
+ changesInputSchema,
24
+ changesOutputSchema,
21
25
  sizeInputSchema,
22
26
  sizeOutputSchema,
23
27
  clearInputSchema,
@@ -33,6 +37,9 @@ import {
33
37
  type DeleteInput,
34
38
  type HasInput,
35
39
  type GetAllInput,
40
+ type EntriesInput,
41
+ type ChangesInput,
42
+ type ChangesOutput,
36
43
  type SizeInput,
37
44
  type ClearInput,
38
45
  type GetBatchInput,
@@ -168,6 +175,56 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
168
175
  },
169
176
  }),
170
177
 
178
+ // Entries: each item with its id, for a sync (roadmap R2)
179
+ defineProcedure({
180
+ path: [...basePath, "entries"],
181
+ input: entriesInputSchema,
182
+ output: entriesOutputSchema,
183
+ metadata: {
184
+ effects: { reads: [resource], writes: [] },
185
+ description: `Get all items of ${collectionName} with their ids`,
186
+ tags: ["collections", collectionName, "read"],
187
+ },
188
+ handler: async (_input: EntriesInput, ctx: ProcedureContext) => {
189
+ const storage = getStorage(ctx);
190
+ if (!storage.getEntries) {
191
+ throw new Error(`The storage of ${collectionName} cannot give the ids of its items`);
192
+ }
193
+ return (await storage.getEntries()).map(([id, value]) => ({ id, value }));
194
+ },
195
+ }),
196
+
197
+ // Changes: the items that changed after a version, for a delta sync (roadmap R28)
198
+ defineProcedure({
199
+ path: [...basePath, "changes"],
200
+ input: changesInputSchema,
201
+ output: changesOutputSchema,
202
+ metadata: {
203
+ effects: { reads: [resource], writes: [] },
204
+ description: `Get the items of ${collectionName} that changed after a version`,
205
+ tags: ["collections", collectionName, "read"],
206
+ },
207
+ handler: async (input: ChangesInput, ctx: ProcedureContext): Promise<ChangesOutput> => {
208
+ const storage = getStorage(ctx);
209
+ if (storage.getChangesSince) {
210
+ const changes = await storage.getChangesSince(input.since);
211
+ return {
212
+ version: changes.version,
213
+ full: changes.full,
214
+ changed: changes.changed.map(([id, value]) => ({ id, value })),
215
+ deleted: changes.deleted,
216
+ };
217
+ }
218
+ // A storage without a change log gives all its items. The empty version asks for all
219
+ // items again at the next call.
220
+ if (storage.getEntries) {
221
+ const entries = await storage.getEntries();
222
+ return { version: "", full: true, changed: entries.map(([id, value]) => ({ id, value })), deleted: [] };
223
+ }
224
+ throw new Error(`The storage of ${collectionName} cannot give its changes or the ids of its items`);
225
+ },
226
+ }),
227
+
171
228
  // Size
172
229
  defineProcedure({
173
230
  path: [...basePath, "size"],
@@ -335,6 +335,116 @@ export const getAllOutputSchema : ZodLike<unknown[]> = createSchema<unknown[]>((
335
335
  };
336
336
  });
337
337
 
338
+ // =============================================================================
339
+ // Entries Operation Schemas
340
+ // =============================================================================
341
+
342
+ /**
343
+ * Input for entries operation - empty object.
344
+ */
345
+ export interface EntriesInput {}
346
+
347
+ export const entriesInputSchema : ZodLike<EntriesInput> = createSchema<EntriesInput>((data) => {
348
+ if (!isObject(data)) {
349
+ return {
350
+ success: false,
351
+ error: {
352
+ message: "Input must be an object",
353
+ errors: [{ path: [], message: "Input must be an object" }],
354
+ },
355
+ };
356
+ }
357
+ return { success: true, data: {} };
358
+ });
359
+
360
+ /** One item of a collection with its id. */
361
+ export interface CollectionEntry {
362
+ id: string;
363
+ value: unknown;
364
+ }
365
+
366
+ /**
367
+ * Output for entries operation - each item with its id. A sync needs the ids: `getAll` gives
368
+ * only the values (roadmap R2).
369
+ */
370
+ export const entriesOutputSchema : ZodLike<CollectionEntry[]> = createSchema<CollectionEntry[]>((data) => {
371
+ if (isArray(data) && data.every((entry) => isObject(entry) && isString(entry["id"]) && "value" in entry)) {
372
+ return { success: true, data: data as CollectionEntry[] };
373
+ }
374
+ return {
375
+ success: false,
376
+ error: {
377
+ message: "Output must be an array of { id, value } objects",
378
+ errors: [{ path: [], message: "Output must be an array of { id, value } objects" }],
379
+ },
380
+ };
381
+ });
382
+
383
+ // =============================================================================
384
+ // Changes Operation Schemas
385
+ // =============================================================================
386
+
387
+ /** Input for changes operation: the version of an earlier call (roadmap R28). */
388
+ export interface ChangesInput {
389
+ /** The `version` of an earlier result. Without it, the result has all items. */
390
+ since?: string;
391
+ }
392
+
393
+ export const changesInputSchema : ZodLike<ChangesInput> = createSchema<ChangesInput>((data) => {
394
+ if (!isObject(data)) {
395
+ return {
396
+ success: false,
397
+ error: {
398
+ message: "Input must be an object",
399
+ errors: [{ path: [], message: "Input must be an object" }],
400
+ },
401
+ };
402
+ }
403
+ const since = data["since"];
404
+ if (since !== undefined && !isString(since)) {
405
+ return {
406
+ success: false,
407
+ error: {
408
+ message: "since must be a string",
409
+ errors: [{ path: ["since"], message: "since must be a string" }],
410
+ },
411
+ };
412
+ }
413
+ return { success: true, data: since === undefined ? {} : { since } };
414
+ });
415
+
416
+ /**
417
+ * Output for changes operation: the items that changed after the version, the deleted ids, and
418
+ * the new version. When `full` is true, `changed` has each item (roadmap R28).
419
+ */
420
+ export interface ChangesOutput {
421
+ version: string;
422
+ full: boolean;
423
+ changed: CollectionEntry[];
424
+ deleted: string[];
425
+ }
426
+
427
+ export const changesOutputSchema : ZodLike<ChangesOutput> = createSchema<ChangesOutput>((data) => {
428
+ if (
429
+ isObject(data) &&
430
+ isString(data["version"]) &&
431
+ typeof data["full"] === "boolean" &&
432
+ isArray(data["changed"]) &&
433
+ data["changed"].every((entry) => isObject(entry) && isString(entry["id"]) && "value" in entry) &&
434
+ isArray(data["deleted"]) &&
435
+ data["deleted"].every((id) => isString(id))
436
+ ) {
437
+ return { success: true, data: data as unknown as ChangesOutput };
438
+ }
439
+ return {
440
+ success: false,
441
+ error: {
442
+ message: "Output must be { version, full, changed: [{ id, value }], deleted: [id] }",
443
+ errors: [{ path: [], message: "Output must be { version, full, changed: [{ id, value }], deleted: [id] }" }],
444
+ },
445
+ };
446
+ });
447
+
338
448
  // =============================================================================
339
449
  // Size Operation Schemas
340
450
  // =============================================================================
@@ -15,6 +15,18 @@ export interface CollectionStorage<T> {
15
15
  get(id: string): Promise<T | undefined>;
16
16
  /** Every item of the collection. */
17
17
  getAll(): Promise<T[]>;
18
+ /**
19
+ * Every item with its id. A sync needs the ids (roadmap R2). A storage that cannot give
20
+ * them does not have this method.
21
+ */
22
+ getEntries?(): Promise<Array<[string, T]>>;
23
+ /**
24
+ * The changes after a version (roadmap R28). A storage without a change log does not have
25
+ * this method, and a sync uses `getEntries()`.
26
+ *
27
+ * @param since - A `version` that an earlier call gave. Without it, the result has all items.
28
+ */
29
+ getChangesSince?(since?: string): Promise<StorageChanges<T>>;
18
30
  /** The items for which the predicate gives true. */
19
31
  find(predicate: (item: T) => boolean): Promise<T[]>;
20
32
  /** True when an item has this id. */
@@ -33,12 +45,38 @@ export interface CollectionStorage<T> {
33
45
  deleteBatch(ids: string[]): Promise<number>;
34
46
  /** The items of several ids. An id with no item is not in the map. */
35
47
  getBatch(ids: string[]): Promise<Map<string, T>>;
48
+ /**
49
+ * This method sends the writes that wait in the storage to their destination (roadmap R28). A storage
50
+ * that keeps no writes does not have this method. `ProcedureServer.useStorage` calls it.
51
+ */
52
+ flush?(): Promise<void>;
36
53
  /** Release the resources of the storage. */
37
54
  close(): Promise<void>;
38
55
  /** The kind, the size and the statistics of the storage. */
39
56
  getMetadata?(): Promise<StorageMetadata>;
40
57
  }
41
58
 
59
+ /**
60
+ * The changes of a storage after a version (roadmap R28).
61
+ *
62
+ * - `version` is opaque. The next call gives it back as `since`.
63
+ * - When `full` is true, `changed` has every item, and each id that it does not have was
64
+ * deleted. A storage gives a full result when it cannot find the changes since the version:
65
+ * the first call, a version of an older run, or a change log that dropped old deletes.
66
+ * - When `full` is false, `changed` has the items that changed after the version, and `deleted`
67
+ * has the ids that were deleted after it.
68
+ */
69
+ export interface StorageChanges<T> {
70
+ /** The version of the storage that includes these changes. */
71
+ version: string;
72
+ /** True when `changed` has every item of the storage. */
73
+ full: boolean;
74
+ /** The items that changed, with their ids. */
75
+ changed: Array<[string, T]>;
76
+ /** The ids that were deleted. Empty when `full` is true. */
77
+ deleted: string[];
78
+ }
79
+
42
80
  /** The metadata that a storage gives about itself. */
43
81
  export interface StorageMetadata {
44
82
  type: "memory" | "api" | "hybrid" | "synced" | "custom";
@@ -11,6 +11,16 @@ import { defineProcedure, namespace } from "../define.js";
11
11
  import type { AnyProcedure, Procedure } from "../types.js";
12
12
  import { PURE_EFFECTS, withEffects } from "../effects.js";
13
13
 
14
+ /**
15
+ * This function gives the own field `key` of an item, or undefined. A field of the prototype
16
+ * (`toString`, `constructor`) is not a field of the item: before, `count` with the key
17
+ * `toString` counted each item (catalog test, roadmap R29, as R6 for the object procedures).
18
+ */
19
+ function fieldOf(item: unknown, key: string): unknown {
20
+ if (item === null || item === undefined) return undefined;
21
+ return Object.hasOwn(Object(item), key) ? (item as Record<string, unknown>)[key] : undefined;
22
+ }
23
+
14
24
  // =============================================================================
15
25
  // First Procedure
16
26
  // =============================================================================
@@ -209,8 +219,8 @@ const sortProcedure: SortProcedure = defineProcedure({
209
219
  const direction = input.desc ? -1 : 1;
210
220
 
211
221
  return items.sort((a, b) => {
212
- let aVal = input.key ? (a as Record<string, unknown>)[input.key] : a;
213
- let bVal = input.key ? (b as Record<string, unknown>)[input.key] : b;
222
+ let aVal = input.key ? fieldOf(a, input.key) : a;
223
+ let bVal = input.key ? fieldOf(b, input.key) : b;
214
224
 
215
225
  if (typeof aVal === "string" && typeof bVal === "string") {
216
226
  return aVal.localeCompare(bVal) * direction;
@@ -309,7 +319,7 @@ const uniqueProcedure: UniqueProcedure = defineProcedure({
309
319
  if (input.key) {
310
320
  const seen = new Set<unknown>();
311
321
  return input.items.filter((item) => {
312
- const val = (item as Record<string, unknown>)[input.key!];
322
+ const val = fieldOf(item, input.key!);
313
323
  if (seen.has(val)) return false;
314
324
  seen.add(val);
315
325
  return true;
@@ -346,11 +356,13 @@ const groupByProcedure: GroupByProcedure = defineProcedure({
346
356
  handler: async (input: GroupByInput): Promise<Record<string, unknown[]>> => {
347
357
  const result: Record<string, unknown[]> = {};
348
358
  for (const item of input.items) {
349
- const keyVal = String((item as Record<string, unknown>)[input.key]);
350
- if (!result[keyVal]) {
351
- result[keyVal] = [];
359
+ const keyVal = String(fieldOf(item, input.key));
360
+ // Only an own group: before, a group "constructor" or "toString" found the function of the
361
+ // prototype, and push failed (catalog test, roadmap R29)
362
+ if (!Object.hasOwn(result, keyVal)) {
363
+ Object.defineProperty(result, keyVal, { value: [], writable: true, enumerable: true, configurable: true });
352
364
  }
353
- result[keyVal].push(item);
365
+ result[keyVal]!.push(item);
354
366
  }
355
367
  return result;
356
368
  },
@@ -640,7 +652,7 @@ const filterProcedure: FilterProcedure = defineProcedure({
640
652
  const { items, key, value, values, match = 'equals', invert = false } = input;
641
653
 
642
654
  const predicate = (item: unknown): boolean => {
643
- const itemValue = key ? (item as Record<string, unknown>)[key] : item;
655
+ const itemValue = key ? fieldOf(item, key) : item;
644
656
 
645
657
  let result: boolean;
646
658
  switch (match) {
@@ -716,10 +728,7 @@ const whereProcedure: WhereProcedure = defineProcedure({
716
728
  },
717
729
  handler: async (input: WhereInput): Promise<unknown[]> => {
718
730
  const { items, where } = input;
719
- return items.filter((item) => {
720
- const obj = item as Record<string, unknown>;
721
- return Object.entries(where).every(([key, value]) => obj[key] === value);
722
- });
731
+ return items.filter((item) => Object.entries(where).every(([key, value]) => fieldOf(item, key) === value));
723
732
  },
724
733
  });
725
734
 
@@ -750,7 +759,7 @@ const pluckProcedure: PluckProcedure = defineProcedure({
750
759
  tags: ["core", "array", "transform"],
751
760
  },
752
761
  handler: async (input: PluckInput): Promise<unknown[]> => {
753
- return input.items.map((item) => (item as Record<string, unknown>)[input.key]);
762
+ return input.items.map((item) => fieldOf(item, input.key));
754
763
  },
755
764
  });
756
765
 
@@ -785,7 +794,9 @@ const arrPickProcedure: ArrPickProcedure = defineProcedure({
785
794
  const obj = item as Record<string, unknown>;
786
795
  const result: Record<string, unknown> = {};
787
796
  for (const key of input.keys) {
788
- if (key in obj) {
797
+ // Only own fields, as client.pick: before, "toString" picked a function of the prototype
798
+ // (catalog test, roadmap R29)
799
+ if (Object.hasOwn(obj, key)) {
789
800
  result[key] = obj[key];
790
801
  }
791
802
  }
@@ -881,7 +892,7 @@ const partitionProcedure: PartitionProcedure = defineProcedure({
881
892
  const falsy: unknown[] = [];
882
893
 
883
894
  for (const item of input.items) {
884
- const itemValue = (item as Record<string, unknown>)[input.key];
895
+ const itemValue = fieldOf(item, input.key);
885
896
  const matches = input.value !== undefined ? itemValue === input.value : Boolean(itemValue);
886
897
  if (matches) {
887
898
  truthy.push(item);
@@ -929,7 +940,7 @@ const countProcedure: CountProcedure = defineProcedure({
929
940
  }
930
941
 
931
942
  return input.items.filter((item) => {
932
- const itemValue = (item as Record<string, unknown>)[input.key!];
943
+ const itemValue = fieldOf(item, input.key!);
933
944
  return input.value !== undefined ? itemValue === input.value : Boolean(itemValue);
934
945
  }).length;
935
946
  },
@@ -582,6 +582,10 @@ const mapProcedure: MapProcedure = defineProcedure({
582
582
  // map is a control-flow procedure: its operands arrive raw, and it runs them here.
583
583
  // Before, the handler only returned the raw items, so their refs never ran and no
584
584
  // function was applied.
585
+ if (input.fn !== undefined && !isAnyProcedureRef(input.fn)) {
586
+ // Before, a fn that was not a call was ignored without a word (roadmap R4)
587
+ throw new Error("client.map: fn must be one call ({ $proc, input }), or absent");
588
+ }
585
589
  const outer = scopeOf(input);
586
590
  const items = await hydrateItems(input.items, ctx, outer);
587
591
  if (!isAnyProcedureRef(input.fn) || !ctx?.client) {
@@ -625,6 +629,10 @@ const reduceProcedure: ReduceProcedure = defineProcedure({
625
629
  tags: controlFlowTags("collection"),
626
630
  },
627
631
  handler: async (input: ReduceInput, ctx?: ProcedureContext): Promise<unknown> => {
632
+ if (input.fn !== undefined && !isAnyProcedureRef(input.fn)) {
633
+ // Before, a fn that was not a call was ignored without a word (roadmap R4)
634
+ throw new Error("client.reduce: fn must be one call ({ $proc, input }), or absent");
635
+ }
628
636
  if (!ctx?.client) {
629
637
  return input.accumulated ?? input.initial;
630
638
  }
@@ -331,7 +331,8 @@ const lookupProcedure: LookupProcedure = defineProcedure({
331
331
  handler: async (input: LookupInput, ctx): Promise<LookupOutput> => {
332
332
  // The registry of the caller (a server or a client with its own registry)
333
333
  const proc = (ctx?.registry ?? PROCEDURE_REGISTRY).get(input.path);
334
- if (proc) {
334
+ // A procedure outside the expose rule of the host does not exist for its callers (R17)
335
+ if (proc && (!ctx?.expose || ctx.expose(proc.path))) {
335
336
  return {
336
337
  exists: true,
337
338
  metadata: proc.metadata,