@moltzap/client 2026.503.4 → 2026.504.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 (187) hide show
  1. package/dist/channel-core.d.ts +1 -4
  2. package/dist/channel-core.d.ts.map +1 -1
  3. package/dist/channel-core.js +0 -9
  4. package/dist/channel-core.js.map +1 -1
  5. package/dist/channel-core.test.js +90 -71
  6. package/dist/channel-core.test.js.map +1 -1
  7. package/dist/cli/commands/agents.js +2 -2
  8. package/dist/cli/commands/agents.js.map +1 -1
  9. package/dist/cli/commands/apps.d.ts +2 -24
  10. package/dist/cli/commands/apps.d.ts.map +1 -1
  11. package/dist/cli/commands/apps.js +22 -47
  12. package/dist/cli/commands/apps.js.map +1 -1
  13. package/dist/cli/commands/apps.test.js +59 -72
  14. package/dist/cli/commands/apps.test.js.map +1 -1
  15. package/dist/cli/commands/contacts.d.ts +1 -5
  16. package/dist/cli/commands/contacts.d.ts.map +1 -1
  17. package/dist/cli/commands/contacts.js +11 -26
  18. package/dist/cli/commands/contacts.js.map +1 -1
  19. package/dist/cli/commands/conversations-v2.test.js +30 -26
  20. package/dist/cli/commands/conversations-v2.test.js.map +1 -1
  21. package/dist/cli/commands/conversations.d.ts.map +1 -1
  22. package/dist/cli/commands/conversations.js +48 -33
  23. package/dist/cli/commands/conversations.js.map +1 -1
  24. package/dist/cli/commands/messages.d.ts.map +1 -1
  25. package/dist/cli/commands/messages.js +11 -8
  26. package/dist/cli/commands/messages.js.map +1 -1
  27. package/dist/cli/commands/messages.test.js +28 -30
  28. package/dist/cli/commands/messages.test.js.map +1 -1
  29. package/dist/cli/commands/presence.d.ts.map +1 -1
  30. package/dist/cli/commands/presence.js +5 -4
  31. package/dist/cli/commands/presence.js.map +1 -1
  32. package/dist/cli/commands/send.d.ts.map +1 -1
  33. package/dist/cli/commands/send.js +20 -11
  34. package/dist/cli/commands/send.js.map +1 -1
  35. package/dist/cli/commands/send.test.js +11 -9
  36. package/dist/cli/commands/send.test.js.map +1 -1
  37. package/dist/cli/commands/status.js +2 -2
  38. package/dist/cli/commands/status.js.map +1 -1
  39. package/dist/cli/commands/test-transport.d.ts +10 -0
  40. package/dist/cli/commands/test-transport.d.ts.map +1 -0
  41. package/dist/cli/commands/test-transport.js +29 -0
  42. package/dist/cli/commands/test-transport.js.map +1 -0
  43. package/dist/cli/index.d.ts.map +1 -1
  44. package/dist/cli/index.js +0 -2
  45. package/dist/cli/index.js.map +1 -1
  46. package/dist/cli/socket-client.d.ts +9 -4
  47. package/dist/cli/socket-client.d.ts.map +1 -1
  48. package/dist/cli/socket-client.js +27 -12
  49. package/dist/cli/socket-client.js.map +1 -1
  50. package/dist/cli/transport.d.ts +3 -2
  51. package/dist/cli/transport.d.ts.map +1 -1
  52. package/dist/cli/transport.js +6 -6
  53. package/dist/cli/transport.js.map +1 -1
  54. package/dist/cli/transport.test.js +4 -4
  55. package/dist/cli/transport.test.js.map +1 -1
  56. package/dist/index.d.ts +2 -3
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +1 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/internal/__tests__/app-callback-partition-key.test.d.ts +2 -0
  61. package/dist/internal/__tests__/app-callback-partition-key.test.d.ts.map +1 -0
  62. package/dist/internal/__tests__/app-callback-partition-key.test.js +33 -0
  63. package/dist/internal/__tests__/app-callback-partition-key.test.js.map +1 -0
  64. package/dist/internal/__tests__/app-callback-partition-worker.test.d.ts +2 -0
  65. package/dist/internal/__tests__/app-callback-partition-worker.test.d.ts.map +1 -0
  66. package/dist/internal/__tests__/{s2c-partition-worker.test.js → app-callback-partition-worker.test.js} +6 -9
  67. package/dist/internal/__tests__/app-callback-partition-worker.test.js.map +1 -0
  68. package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.d.ts +2 -0
  69. package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.d.ts.map +1 -0
  70. package/dist/internal/__tests__/{s2c-partitioned-dispatcher-real-ws.test.js → app-callback-partitioned-dispatcher-real-ws.test.js} +69 -94
  71. package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.js.map +1 -0
  72. package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.d.ts +2 -0
  73. package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.d.ts.map +1 -0
  74. package/dist/internal/__tests__/{s2c-partitioned-dispatcher.test.js → app-callback-partitioned-dispatcher.test.js} +35 -69
  75. package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.js.map +1 -0
  76. package/dist/internal/__tests__/app-callback-test-requests.d.ts +58 -0
  77. package/dist/internal/__tests__/app-callback-test-requests.d.ts.map +1 -0
  78. package/dist/internal/__tests__/app-callback-test-requests.js +124 -0
  79. package/dist/internal/__tests__/app-callback-test-requests.js.map +1 -0
  80. package/dist/internal/{s2c-dispatcher-errors.d.ts → app-callback-dispatcher-errors.d.ts} +5 -21
  81. package/dist/internal/app-callback-dispatcher-errors.d.ts.map +1 -0
  82. package/dist/internal/{s2c-dispatcher-errors.js → app-callback-dispatcher-errors.js} +3 -13
  83. package/dist/internal/app-callback-dispatcher-errors.js.map +1 -0
  84. package/dist/internal/app-callback-partition-key.d.ts +68 -0
  85. package/dist/internal/app-callback-partition-key.d.ts.map +1 -0
  86. package/dist/internal/app-callback-partition-key.js +90 -0
  87. package/dist/internal/app-callback-partition-key.js.map +1 -0
  88. package/dist/internal/{s2c-partition-worker.d.ts → app-callback-partition-worker.d.ts} +5 -5
  89. package/dist/internal/app-callback-partition-worker.d.ts.map +1 -0
  90. package/dist/internal/{s2c-partition-worker.js → app-callback-partition-worker.js} +5 -5
  91. package/dist/internal/app-callback-partition-worker.js.map +1 -0
  92. package/dist/internal/{s2c-partitioned-dispatcher.d.ts → app-callback-partitioned-dispatcher.d.ts} +9 -10
  93. package/dist/internal/app-callback-partitioned-dispatcher.d.ts.map +1 -0
  94. package/dist/internal/{s2c-partitioned-dispatcher.js → app-callback-partitioned-dispatcher.js} +12 -19
  95. package/dist/internal/app-callback-partitioned-dispatcher.js.map +1 -0
  96. package/dist/runtime/errors.d.ts +2 -28
  97. package/dist/runtime/errors.d.ts.map +1 -1
  98. package/dist/runtime/errors.js +0 -9
  99. package/dist/runtime/errors.js.map +1 -1
  100. package/dist/runtime/errors.test.js +2 -2
  101. package/dist/runtime/errors.test.js.map +1 -1
  102. package/dist/runtime/frame.d.ts +17 -18
  103. package/dist/runtime/frame.d.ts.map +1 -1
  104. package/dist/runtime/frame.js +86 -31
  105. package/dist/runtime/frame.js.map +1 -1
  106. package/dist/runtime/frame.test.js +15 -11
  107. package/dist/runtime/frame.test.js.map +1 -1
  108. package/dist/runtime/index.d.ts +1 -1
  109. package/dist/runtime/index.d.ts.map +1 -1
  110. package/dist/runtime/index.js +1 -1
  111. package/dist/runtime/index.js.map +1 -1
  112. package/dist/runtime/local-service-commands.d.ts +7 -0
  113. package/dist/runtime/local-service-commands.d.ts.map +1 -0
  114. package/dist/runtime/local-service-commands.js +6 -0
  115. package/dist/runtime/local-service-commands.js.map +1 -0
  116. package/dist/runtime/runtime.test.js +1 -2
  117. package/dist/runtime/runtime.test.js.map +1 -1
  118. package/dist/runtime/subscribers.d.ts +34 -29
  119. package/dist/runtime/subscribers.d.ts.map +1 -1
  120. package/dist/runtime/subscribers.js +19 -19
  121. package/dist/runtime/subscribers.js.map +1 -1
  122. package/dist/runtime/subscribers.test.js +50 -31
  123. package/dist/runtime/subscribers.test.js.map +1 -1
  124. package/dist/service.d.ts +24 -39
  125. package/dist/service.d.ts.map +1 -1
  126. package/dist/service.js +165 -161
  127. package/dist/service.js.map +1 -1
  128. package/dist/service.test.js +111 -170
  129. package/dist/service.test.js.map +1 -1
  130. package/dist/test-utils/channel-service-fixture.d.ts +1 -2
  131. package/dist/test-utils/channel-service-fixture.d.ts.map +1 -1
  132. package/dist/test-utils/channel-service-fixture.js +28 -20
  133. package/dist/test-utils/channel-service-fixture.js.map +1 -1
  134. package/dist/test-utils/conformance-adapter.d.ts +2 -2
  135. package/dist/test-utils/conformance-adapter.d.ts.map +1 -1
  136. package/dist/test-utils/conformance-adapter.js +33 -26
  137. package/dist/test-utils/conformance-adapter.js.map +1 -1
  138. package/dist/test-utils/fake-service.d.ts +14 -39
  139. package/dist/test-utils/fake-service.d.ts.map +1 -1
  140. package/dist/test-utils/fake-service.js +33 -39
  141. package/dist/test-utils/fake-service.js.map +1 -1
  142. package/dist/test-utils/ids.d.ts +9 -0
  143. package/dist/test-utils/ids.d.ts.map +1 -0
  144. package/dist/test-utils/ids.js +52 -0
  145. package/dist/test-utils/ids.js.map +1 -0
  146. package/dist/test-utils/index.d.ts +10 -2
  147. package/dist/test-utils/index.d.ts.map +1 -1
  148. package/dist/test-utils/index.js +12 -5
  149. package/dist/test-utils/index.js.map +1 -1
  150. package/dist/ws-client.d.ts +58 -64
  151. package/dist/ws-client.d.ts.map +1 -1
  152. package/dist/ws-client.js +151 -133
  153. package/dist/ws-client.js.map +1 -1
  154. package/dist/ws-client.test.js +157 -167
  155. package/dist/ws-client.test.js.map +1 -1
  156. package/package.json +3 -3
  157. package/dist/cli/commands/permissions.d.ts +0 -70
  158. package/dist/cli/commands/permissions.d.ts.map +0 -1
  159. package/dist/cli/commands/permissions.js +0 -100
  160. package/dist/cli/commands/permissions.js.map +0 -1
  161. package/dist/cli/commands/permissions.test.d.ts +0 -2
  162. package/dist/cli/commands/permissions.test.d.ts.map +0 -1
  163. package/dist/cli/commands/permissions.test.js +0 -127
  164. package/dist/cli/commands/permissions.test.js.map +0 -1
  165. package/dist/internal/__tests__/s2c-partition-key.test.d.ts +0 -2
  166. package/dist/internal/__tests__/s2c-partition-key.test.d.ts.map +0 -1
  167. package/dist/internal/__tests__/s2c-partition-key.test.js +0 -231
  168. package/dist/internal/__tests__/s2c-partition-key.test.js.map +0 -1
  169. package/dist/internal/__tests__/s2c-partition-worker.test.d.ts +0 -2
  170. package/dist/internal/__tests__/s2c-partition-worker.test.d.ts.map +0 -1
  171. package/dist/internal/__tests__/s2c-partition-worker.test.js.map +0 -1
  172. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.d.ts +0 -2
  173. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.d.ts.map +0 -1
  174. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.js.map +0 -1
  175. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.d.ts +0 -2
  176. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.d.ts.map +0 -1
  177. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.js.map +0 -1
  178. package/dist/internal/s2c-dispatcher-errors.d.ts.map +0 -1
  179. package/dist/internal/s2c-dispatcher-errors.js.map +0 -1
  180. package/dist/internal/s2c-partition-key.d.ts +0 -79
  181. package/dist/internal/s2c-partition-key.d.ts.map +0 -1
  182. package/dist/internal/s2c-partition-key.js +0 -146
  183. package/dist/internal/s2c-partition-key.js.map +0 -1
  184. package/dist/internal/s2c-partition-worker.d.ts.map +0 -1
  185. package/dist/internal/s2c-partition-worker.js.map +0 -1
  186. package/dist/internal/s2c-partitioned-dispatcher.d.ts.map +0 -1
  187. package/dist/internal/s2c-partitioned-dispatcher.js.map +0 -1
package/dist/ws-client.js CHANGED
@@ -1,14 +1,13 @@
1
1
  import * as Socket from "@effect/platform/Socket";
2
2
  import * as NodeSocket from "@effect/platform-node/NodeSocket";
3
3
  import { Cause, Data, Deferred, Duration, Either, Effect, Exit, Fiber, HashMap, ManagedRuntime, Option, Ref, Schedule, Scope, } from "effect";
4
- import { PROTOCOL_VERSION, responseFrame, } from "@moltzap/protocol";
5
- import { DuplicateServerRpcHandlerError, NotConnectedError, RpcServerError, RpcTimeoutError, } from "./runtime/errors.js";
4
+ import { PROTOCOL_VERSION, Connect, decodeRpcResult, jsonRpcStringId, NotConnectedError, requestFrame, responseFrame, RpcServerError, RpcTimeoutError, } from "@moltzap/protocol";
5
+ import { DuplicateServerRpcHandlerError } from "./runtime/errors.js";
6
6
  import { decodeFrames } from "./runtime/frame.js";
7
7
  import { makeSubscriberRegistry, } from "./runtime/subscribers.js";
8
8
  import { extractCloseInfo } from "./runtime/close-info.js";
9
- import { makePartitionedDispatcher, } from "./internal/s2c-partitioned-dispatcher.js";
10
- import { MalformedPartitionKeyError, PartitionLimitError, PartitionQueueFullError, } from "./internal/s2c-dispatcher-errors.js";
11
- import { Connect } from "@moltzap/protocol";
9
+ import { makePartitionedDispatcher, } from "./internal/app-callback-partitioned-dispatcher.js";
10
+ import { PartitionLimitError, PartitionQueueFullError, } from "./internal/app-callback-dispatcher-errors.js";
12
11
  /**
13
12
  * Default per-RPC timeout. Exported so tests driving `TestClock` can match
14
13
  * exactly — keeps tests from silently drifting if this changes.
@@ -29,8 +28,8 @@ const JSON_RPC_INTERNAL_ERROR_CODE = -32603;
29
28
  */
30
29
  const MALFORMED_LOG_EVERY = 50;
31
30
  /**
32
- * Cap on the per-client event buffer. Any frame that has no live
33
- * `waitForEvent` awaiter lands here until someone drains it. Excess
31
+ * Cap on the per-client notification buffer. Any frame that has no live
32
+ * `waitForNotification` awaiter lands here until someone drains it. Excess
34
33
  * frames are evicted FIFO so a slow consumer can't leak memory.
35
34
  */
36
35
  const MAX_EVENT_BUFFER = 1000;
@@ -40,15 +39,18 @@ const UTF8_DECODER = new TextDecoder("utf-8");
40
39
  const makeNotConnectedError = () => new NotConnectedError({ message: MSG_NOT_CONNECTED });
41
40
  class ReconnectAttemptFailedError extends Data.TaggedError("ReconnectAttemptFailedError") {
42
41
  }
43
- /** Drop `waiter` from its event-name bucket, pruning an empty bucket. */
44
- function removeWaiter(m, eventName, waiter) {
45
- const bucket = HashMap.get(m, eventName);
42
+ function notificationMatches(definition, notification) {
43
+ return notification.definition === definition;
44
+ }
45
+ /** Drop `waiter` from its notification-definition bucket, pruning an empty bucket. */
46
+ function removeWaiter(m, definition, waiter) {
47
+ const bucket = HashMap.get(m, definition);
46
48
  if (bucket._tag === "None")
47
49
  return m;
48
50
  const filtered = bucket.value.filter((w) => w !== waiter);
49
51
  return filtered.length === 0
50
- ? HashMap.remove(m, eventName)
51
- : HashMap.set(m, eventName, filtered);
52
+ ? HashMap.remove(m, definition)
53
+ : HashMap.set(m, definition, filtered);
52
54
  }
53
55
  /**
54
56
  * WebSocket lifecycle: open → auth/connect → active. On disconnect,
@@ -67,17 +69,15 @@ export class MoltZapWsClient {
67
69
  pendingRef;
68
70
  stateRef;
69
71
  malformedRef;
70
- eventsBufferRef;
72
+ notificationsBufferRef;
71
73
  /**
72
- * Waiters keyed by event name. Each bucket is a FIFO stack: delivery
73
- * pops the most recently registered waiter (the tail). Keying by event
74
- * name keeps dispatch O(1) per inbound frame regardless of total
75
- * outstanding waiters.
74
+ * Waiters keyed by notification descriptor identity. Each bucket is a FIFO
75
+ * stack: delivery pops the most recently registered waiter (the tail).
76
76
  */
77
- eventWaitersRef;
77
+ notificationWaitersRef;
78
78
  runtime;
79
79
  /**
80
- * Per-subscription event registry. Spec #222 §5.3 (C4 + the
80
+ * Per-subscription notification registry. Spec #222 §5.3 (C4 + the
81
81
  * `RealClientEventSubscriber.subscribe` filter stub). Constructed
82
82
  * synchronously alongside the other Refs; `MoltZapWsClient.subscribe`
83
83
  * delegates to it directly.
@@ -87,9 +87,9 @@ export class MoltZapWsClient {
87
87
  * Per-method handler registry for server-initiated RPCs. Survives
88
88
  * reconnects so apps register once and re-attach automatically when the
89
89
  * socket comes back. Each entry is invoked by the per-connection
90
- * dispatcher fiber when an s2c request frame arrives.
90
+ * dispatcher fiber when an appCallback request frame arrives.
91
91
  */
92
- s2cHandlersRef;
92
+ appCallbackHandlersRef;
93
93
  requestCounter = 0;
94
94
  closed = false;
95
95
  reconnectFiber = null;
@@ -100,15 +100,15 @@ export class MoltZapWsClient {
100
100
  this.pendingRef = this.runtime.runSync(Ref.make(HashMap.empty()));
101
101
  this.stateRef = this.runtime.runSync(Ref.make(Option.none()));
102
102
  this.malformedRef = this.runtime.runSync(Ref.make(0));
103
- this.eventsBufferRef = this.runtime.runSync(Ref.make([]));
104
- this.eventWaitersRef = this.runtime.runSync(Ref.make(HashMap.empty()));
103
+ this.notificationsBufferRef = this.runtime.runSync(Ref.make([]));
104
+ this.notificationWaitersRef = this.runtime.runSync(Ref.make(HashMap.empty()));
105
105
  // Registry construction is `Effect<…, never>`; running it sync here
106
106
  // matches every other Ref initializer in this constructor and
107
107
  // keeps `subscribers` non-nullable inside the class.
108
108
  this.subscribers = this.runtime.runSync(makeSubscriberRegistry({
109
109
  warn: (...args) => this.options.logger?.warn(...args),
110
110
  }));
111
- this.s2cHandlersRef = this.runtime.runSync(Ref.make(HashMap.empty()));
111
+ this.appCallbackHandlersRef = this.runtime.runSync(Ref.make(HashMap.empty()));
112
112
  }
113
113
  /**
114
114
  * Register a handler for a server-initiated RPC method. Survives
@@ -120,18 +120,21 @@ export class MoltZapWsClient {
120
120
  *
121
121
  * The dispatcher fiber forked at `connect()` time picks up handlers via
122
122
  * `Ref.get` per-frame, so a registration made BEFORE `connect()` is
123
- * visible to the very first inbound s2c request, and a registration
123
+ * visible to the very first inbound appCallback request, and a registration
124
124
  * made AFTER `connect()` takes effect on the next inbound frame.
125
125
  */
126
- handleServerRpc(method, handler) {
126
+ handleServerRpc(definition, handler) {
127
127
  return Effect.gen(this, function* () {
128
- const swapped = yield* Ref.modify(this.s2cHandlersRef, (m) => {
129
- if (HashMap.has(m, method))
128
+ const swapped = yield* Ref.modify(this.appCallbackHandlersRef, (m) => {
129
+ if (HashMap.has(m, definition))
130
130
  return [false, m];
131
- return [true, HashMap.set(m, method, handler)];
131
+ return [
132
+ true,
133
+ HashMap.set(m, definition, handler),
134
+ ];
132
135
  });
133
136
  if (!swapped) {
134
- return yield* Effect.fail(new DuplicateServerRpcHandlerError({ method }));
137
+ return yield* Effect.fail(new DuplicateServerRpcHandlerError({ method: definition.name }));
135
138
  }
136
139
  });
137
140
  }
@@ -152,18 +155,48 @@ export class MoltZapWsClient {
152
155
  Effect.provide(NodeSocket.layerWebSocketConstructor));
153
156
  });
154
157
  }
155
- sendRpc(method, params, opts) {
156
- const methodName = typeof method === "string" ? method : method.name;
157
- return this.sendRpcEffect(methodName, params, opts);
158
+ /**
159
+ * Send an RPC. Fails with a typed error:
160
+ * - `NotConnectedError` if the socket isn't OPEN or closes mid-RPC
161
+ * - `RpcTimeoutError` after `RPC_TIMEOUT_MS` — no automatic retry
162
+ * - `RpcServerError` on a typed server-error frame
163
+ *
164
+ * Descriptor-backed RPC call. Callers pass the protocol descriptor, and the
165
+ * client extracts the wire method only inside the encoder path.
166
+ */
167
+ sendRpc(definition, params, opts) {
168
+ return this.sendRpcEffect(definition, params, opts).pipe(Effect.flatMap((result) => definition.validateResult(result)
169
+ ? Effect.succeed(result)
170
+ : Effect.fail(new RpcServerError({
171
+ code: JSON_RPC_INTERNAL_ERROR_CODE,
172
+ message: `Invalid result for method: ${definition.name}`,
173
+ data: result,
174
+ }))));
158
175
  }
159
- sendRpcTracked(method, params) {
160
- const methodName = typeof method === "string" ? method : method.name;
161
- return this.sendRpcTrackedEffect(methodName, params);
176
+ /**
177
+ * Send an RPC and surface the outbound request id alongside the
178
+ * response envelope `type` and `result`. Spec #222 §5.1–5.2:
179
+ * un-vacuates B4 (request-id tracking) and V5 (response-type
180
+ * exposure). Mirrors `sendRpc`'s descriptor-backed call shape.
181
+ *
182
+ * Invariant 3 (spec #222 §4): the returned `id` is the same identity
183
+ * minted inside the existing `rpc-${++counter}` site — no parallel
184
+ * counter, no mirror. The single-id pre-condition is what makes B4 a
185
+ * real check rather than a tautology.
186
+ */
187
+ sendRpcTracked(definition, params) {
188
+ return this.sendRpcTrackedEffect(definition, params).pipe(Effect.flatMap((tracked) => definition.validateResult(tracked.result)
189
+ ? Effect.succeed({ id: tracked.id, result: tracked.result })
190
+ : Effect.fail(new RpcServerError({
191
+ code: JSON_RPC_INTERNAL_ERROR_CODE,
192
+ message: `Invalid result for method: ${definition.name}`,
193
+ data: tracked.result,
194
+ }))));
162
195
  }
163
196
  /**
164
- * Register a per-subscription event handler. Spec #222 §5.3 + OQ-2
197
+ * Register a per-subscription notification handler. Spec #222 §5.3 + OQ-2
165
198
  * (A): filter grammar is the three-field `SubscriptionFilter`
166
- * (`emissionTag` / `conversationId` / `eventNamePrefix`). Returns a
199
+ * (`emissionTag` / `conversationId` / `notificationNamePrefix`). Returns a
167
200
  * handle whose `unsubscribe` Effect drops delivery starting with the
168
201
  * next inbound frame (OQ-3 A snapshot semantics).
169
202
  *
@@ -199,7 +232,7 @@ export class MoltZapWsClient {
199
232
  yield* Effect.forkDaemon(Fiber.interrupt(f));
200
233
  }
201
234
  yield* this.failAllPending(MSG_NOT_CONNECTED);
202
- yield* this.failAllEventWaiters(MSG_NOT_CONNECTED);
235
+ yield* this.failAllNotificationWaiters(MSG_NOT_CONNECTED);
203
236
  // Drop every live subscription so handlers stop firing once
204
237
  // the client is permanently torn down. Idempotent.
205
238
  yield* this.subscribers.closeAll;
@@ -226,40 +259,47 @@ export class MoltZapWsClient {
226
259
  void this.runtime.dispose();
227
260
  })));
228
261
  }
229
- /** Wait for the next inbound event whose `event` field equals `eventName`.
262
+ /** Wait for the next inbound notification matching `definition`.
230
263
  * Consumes a buffered match if present; otherwise awaits the next match
231
264
  * with a per-call timeout. */
232
- waitForEvent(eventName, timeoutMs = EVENT_WAIT_TIMEOUT_MS) {
265
+ waitForNotification(definition, timeoutMs = EVENT_WAIT_TIMEOUT_MS) {
233
266
  return Effect.gen(this, function* () {
234
- const buffered = yield* Ref.modify(this.eventsBufferRef, (events) => {
235
- const idx = events.findIndex((e) => e.event === eventName);
236
- if (idx === -1)
237
- return [null, events];
238
- const chosen = events[idx];
239
- const next = [...events.slice(0, idx), ...events.slice(idx + 1)];
240
- return [chosen, next];
267
+ const buffered = yield* Ref.modify(this.notificationsBufferRef, (frames) => {
268
+ for (const [idx, frame] of frames.entries()) {
269
+ if (!notificationMatches(definition, frame))
270
+ continue;
271
+ const next = [...frames.slice(0, idx), ...frames.slice(idx + 1)];
272
+ return [frame, next];
273
+ }
274
+ return [null, frames];
241
275
  });
242
276
  if (buffered !== null)
243
277
  return buffered;
244
278
  const deferred = yield* Deferred.make();
245
- const waiter = { eventName, deferred };
246
- yield* Ref.update(this.eventWaitersRef, (m) => {
247
- const existing = HashMap.get(m, eventName);
279
+ const waiter = {
280
+ definition,
281
+ complete: (notification) => notificationMatches(definition, notification)
282
+ ? Deferred.succeed(deferred, notification).pipe(Effect.asVoid)
283
+ : Effect.void,
284
+ fail: (error) => Deferred.fail(deferred, error).pipe(Effect.asVoid),
285
+ };
286
+ yield* Ref.update(this.notificationWaitersRef, (m) => {
287
+ const existing = HashMap.get(m, definition);
248
288
  const next = existing._tag === "Some" ? [...existing.value, waiter] : [waiter];
249
- return HashMap.set(m, eventName, next);
289
+ return HashMap.set(m, definition, next);
250
290
  });
251
291
  return yield* Deferred.await(deferred).pipe(Effect.timeoutFail({
252
292
  duration: `${timeoutMs} millis`,
253
- onTimeout: () => new Error(`Timeout waiting for event: ${eventName}`),
293
+ onTimeout: () => new Error(`Timeout waiting for notification: ${definition.name}`),
254
294
  }), Effect.onExit((exit) => exit._tag === "Failure"
255
- ? Ref.update(this.eventWaitersRef, (m) => removeWaiter(m, eventName, waiter))
295
+ ? Ref.update(this.notificationWaitersRef, (m) => removeWaiter(m, definition, waiter))
256
296
  : Effect.void));
257
297
  });
258
298
  }
259
- /** Return all buffered events and clear the buffer. Synchronous. */
260
- drainEvents() {
261
- const snapshot = this.runtime.runSync(Ref.get(this.eventsBufferRef));
262
- this.runtime.runSync(Ref.set(this.eventsBufferRef, []));
299
+ /** Return all buffered notifications and clear the buffer. Synchronous. */
300
+ drainNotifications() {
301
+ const snapshot = this.runtime.runSync(Ref.get(this.notificationsBufferRef));
302
+ this.runtime.runSync(Ref.set(this.notificationsBufferRef, []));
263
303
  return [...snapshot];
264
304
  }
265
305
  /** Close the socket without marking as permanently closed, triggering reconnection. */
@@ -308,8 +348,8 @@ export class MoltZapWsClient {
308
348
  // Settled first by whichever fires: the auth/connect response, or
309
349
  // reader-fiber exit on any close/error before handshake.
310
350
  const handshakeSettled = yield* Deferred.make();
311
- // Spec #356 — partitioned s2c dispatcher. Replaces the pre-#356
312
- // single `Stream.runForEach`-driven `s2cInboundQueue` with a
351
+ // Spec #356 — partitioned appCallback dispatcher. Replaces the pre-#356
352
+ // single `Stream.runForEach`-driven `appCallbackInboundQueue` with a
313
353
  // partition router keyed on `(sessionId, conversationId,
314
354
  // hookKind)`. Each tuple owns one bounded queue + one drain
315
355
  // fiber; cross-tuple offers run on independent fibers, so a
@@ -330,8 +370,8 @@ export class MoltZapWsClient {
330
370
  const dispatcher = yield* makePartitionedDispatcher({
331
371
  handle: (req) => this.dispatchInboundServerRequest(req, write),
332
372
  scope: dispatcherScope,
333
- ...(this.options.s2cDispatcher !== undefined
334
- ? { config: this.options.s2cDispatcher }
373
+ ...(this.options.appCallbackDispatcher !== undefined
374
+ ? { config: this.options.appCallbackDispatcher }
335
375
  : {}),
336
376
  ...(this.options.logger !== undefined
337
377
  ? { logger: this.options.logger }
@@ -351,10 +391,10 @@ export class MoltZapWsClient {
351
391
  yield* this.failAllPending(MSG_NOT_CONNECTED);
352
392
  // Unblock any `connect()` still awaiting the handshake.
353
393
  yield* Deferred.fail(handshakeSettled, makeNotConnectedError()).pipe(Effect.ignore);
354
- // Clear connection state BEFORE the s2c teardown so
394
+ // Clear connection state BEFORE the appCallback teardown so
355
395
  // `sendRpc` and observers see the closed state immediately.
356
396
  // Awaiting `Fiber.interrupt` would block this branch on a
357
- // slow s2c handler still draining (codex P2).
397
+ // slow appCallback handler still draining (codex P2).
358
398
  yield* Ref.set(this.stateRef, Option.none());
359
399
  // Tear down the partitioned dispatcher on socket-level
360
400
  // close (e.g. server-initiated). `close()` /
@@ -395,7 +435,7 @@ export class MoltZapWsClient {
395
435
  dispatcher,
396
436
  dispatcherScope,
397
437
  }));
398
- const authEffect = this.sendRpcEffect(Connect.name, {
438
+ const authEffect = this.sendRpc(Connect, {
399
439
  agentKey: this.options.agentKey,
400
440
  minProtocol: PROTOCOL_VERSION,
401
441
  maxProtocol: PROTOCOL_VERSION,
@@ -413,26 +453,20 @@ export class MoltZapWsClient {
413
453
  /**
414
454
  * Tracked variant of `sendRpcEffect`. Reuses the same Deferred /
415
455
  * pendingRef plumbing — the only difference is the resolved value:
416
- * `{id, type: "response", result}` instead of bare `result`. This
456
+ * `{id, result}` instead of bare `result`. This
417
457
  * keeps Invariant 3 (single id source) trivially: we mint `id` once
418
458
  * inside this body, register the Deferred under that id, and return
419
459
  * the same id to the caller alongside the result.
420
460
  */
421
- sendRpcTrackedEffect(method, params, opts) {
461
+ sendRpcTrackedEffect(definition, params, opts) {
422
462
  return Effect.gen(this, function* () {
463
+ const method = definition.name;
423
464
  const state = yield* Ref.get(this.stateRef);
424
465
  if (Option.isNone(state)) {
425
466
  return yield* Effect.fail(makeNotConnectedError());
426
467
  }
427
- const id = `rpc-${++this.requestCounter}`;
428
- const frame = {
429
- jsonrpc: "2.0",
430
- type: "request",
431
- direction: "c2s",
432
- id,
433
- method,
434
- params,
435
- };
468
+ const id = jsonRpcStringId(`rpc-${++this.requestCounter}`);
469
+ const frame = requestFrame(id, definition, params);
436
470
  // Register the Deferred BEFORE writing. `write` yields to the
437
471
  // scheduler; the reader could interleave, see a close, and
438
472
  // `failAllPending` before we register — leaving us to await a
@@ -466,14 +500,12 @@ export class MoltZapWsClient {
466
500
  }), Effect.onExit((exit) => Exit.isFailure(exit)
467
501
  ? Ref.update(this.pendingRef, (m) => HashMap.remove(m, id))
468
502
  : Effect.void));
469
- // `type: "response"` is surfaced as an observable literal (V5):
470
- // the value the caller sees comes from the same response
471
- // envelope the reader fiber decoded, not a synthesized adapter
472
- // constant. The Frame schema at
473
- // `packages/protocol/src/schema/frames.ts:18` pins `type` to
474
- // that literal at decode time, so this assignment is the
475
- // value-level projection of that schema constraint.
476
- return { id, type: "response", result };
503
+ const decodedResult = yield* decodeRpcResult(definition, result).pipe(Effect.mapError(() => new RpcServerError({
504
+ code: JSON_RPC_INTERNAL_ERROR_CODE,
505
+ message: `Invalid result for method: ${definition.name}`,
506
+ data: result,
507
+ })));
508
+ return { id, result: decodedResult };
477
509
  });
478
510
  }
479
511
  /**
@@ -484,8 +516,8 @@ export class MoltZapWsClient {
484
516
  * Invariant 5 (typed error channel), and the `socket.writer` latch
485
517
  * race described below.
486
518
  */
487
- sendRpcEffect(method, params, opts) {
488
- return this.sendRpcTrackedEffect(method, params, opts).pipe(Effect.map((tracked) => tracked.result));
519
+ sendRpcEffect(definition, params, opts) {
520
+ return this.sendRpcTrackedEffect(definition, params, opts).pipe(Effect.map((tracked) => tracked.result));
489
521
  }
490
522
  /**
491
523
  * Translate a typed `OfferRejected` failure from the partitioned
@@ -498,19 +530,11 @@ export class MoltZapWsClient {
498
530
  */
499
531
  writeOfferRejection(err, requestId, write) {
500
532
  const reply = this.offerRejectedResponse(err, requestId);
501
- return write(JSON.stringify(reply)).pipe(Effect.catchAll((werr) => Effect.sync(() => this.options.logger?.warn("s2c offer-rejection write failed", werr))));
533
+ return write(JSON.stringify(reply)).pipe(Effect.catchAll((werr) => Effect.sync(() => this.options.logger?.warn("appCallback offer-rejection write failed", werr))));
502
534
  }
503
535
  offerRejectedResponse(err, requestId) {
504
- if (err instanceof MalformedPartitionKeyError) {
505
- return responseFrame("s2c", requestId, {
506
- error: {
507
- code: -32602,
508
- message: `Invalid params: ${err.reason}`,
509
- },
510
- });
511
- }
512
536
  if (err instanceof PartitionLimitError) {
513
- return responseFrame("s2c", requestId, {
537
+ return responseFrame(requestId, {
514
538
  error: {
515
539
  code: -32000,
516
540
  message: `Server busy: partition limit reached (${err.activePartitions}/${err.maxPartitions})`,
@@ -518,7 +542,7 @@ export class MoltZapWsClient {
518
542
  });
519
543
  }
520
544
  if (err instanceof PartitionQueueFullError) {
521
- return responseFrame("s2c", requestId, {
545
+ return responseFrame(requestId, {
522
546
  error: {
523
547
  code: -32000,
524
548
  message: `Server busy: partition queue full (capacity=${err.capacity})`,
@@ -529,7 +553,7 @@ export class MoltZapWsClient {
529
553
  return _exhaustive;
530
554
  }
531
555
  /**
532
- * Dispatch one inbound s2c request to the registered handler, encode
556
+ * Dispatch one inbound appCallback request to the registered handler, encode
533
557
  * the response, and write it back to the server. Errors are projected
534
558
  * onto an error response so the server's `Deferred.await` always
535
559
  * settles deterministically — never hangs on a missing or crashing
@@ -545,26 +569,25 @@ export class MoltZapWsClient {
545
569
  */
546
570
  dispatchInboundServerRequest(request, write) {
547
571
  return Effect.gen(this, function* () {
548
- const handlers = yield* Ref.get(this.s2cHandlersRef);
549
- const lookup = HashMap.get(handlers, request.method);
572
+ const handlers = yield* Ref.get(this.appCallbackHandlersRef);
573
+ const lookup = HashMap.get(handlers, request.definition);
550
574
  const buildReply = lookup._tag === "None"
551
- ? Effect.succeed(responseFrame("s2c", request.id, {
575
+ ? Effect.succeed(responseFrame(request.id, {
552
576
  error: {
553
577
  code: -32601,
554
- message: `No handler registered for method: ${request.method}`,
578
+ message: "No handler registered for app callback descriptor",
555
579
  },
556
580
  }))
557
581
  : lookup
558
582
  .value(request.params, {
559
583
  requestId: request.id,
560
- method: request.method,
561
- traceparent: request.traceparent,
584
+ definition: request.definition,
562
585
  })
563
586
  .pipe(Effect.match({
564
- onSuccess: (result) => responseFrame("s2c", request.id, {
587
+ onSuccess: (result) => responseFrame(request.id, {
565
588
  result,
566
589
  }),
567
- onFailure: (err) => responseFrame("s2c", request.id, {
590
+ onFailure: (err) => responseFrame(request.id, {
568
591
  error: {
569
592
  code: err.code,
570
593
  message: err.message,
@@ -572,17 +595,17 @@ export class MoltZapWsClient {
572
595
  },
573
596
  }),
574
597
  }), Effect.catchAllCause((cause) => Effect.sync(() => {
575
- this.options.logger?.warn(`s2c handler ${request.method} defected`, Cause.pretty(cause));
576
- return responseFrame("s2c", request.id, {
598
+ this.options.logger?.warn("appCallback handler defected", Cause.pretty(cause));
599
+ return responseFrame(request.id, {
577
600
  error: { code: -32603, message: "Internal error" },
578
601
  });
579
602
  })));
580
603
  const reply = yield* buildReply;
581
- yield* write(JSON.stringify(reply)).pipe(Effect.catchAll((err) => Effect.sync(() => this.options.logger?.warn("s2c response write failed", err))));
604
+ yield* write(JSON.stringify(reply)).pipe(Effect.catchAll((err) => Effect.sync(() => this.options.logger?.warn("appCallback response write failed", err))));
582
605
  });
583
606
  }
584
- /** Route an inbound frame. Malformed frames are logged + dropped; event
585
- * frames dispatch to `onEvent` after the shape check. */
607
+ /** Route an inbound frame. Malformed frames are logged + dropped; notification
608
+ * frames dispatch to `onNotification` after the shape check. */
586
609
  handleIncoming(raw) {
587
610
  return Effect.gen(this, function* () {
588
611
  const decodedFrames = yield* decodeFrames(raw).pipe(Effect.catchTag("MalformedFrameError", (err) => Effect.gen(this, function* () {
@@ -620,7 +643,7 @@ export class MoltZapWsClient {
620
643
  continue;
621
644
  }
622
645
  if (decoded._tag === "ServerRequest") {
623
- // s2c request — hand off to the partitioned dispatcher
646
+ // appCallback request — hand off to the partitioned dispatcher
624
647
  // (spec #356). The dispatcher routes by
625
648
  // `(sessionId, conversationId, hookKind)`; the matching
626
649
  // per-tuple worker fiber drains and runs
@@ -632,8 +655,7 @@ export class MoltZapWsClient {
632
655
  // `dispatcher.offer` is non-blocking: every failure mode is
633
656
  // tagged in `OfferRejected` and translated below to a wire
634
657
  // error response so the server's `Deferred.await` always
635
- // settles (no hangs on a malformed-key or partition-full
636
- // request).
658
+ // settles (no hangs on partition-full requests).
637
659
  const state = yield* Ref.get(this.stateRef);
638
660
  if (Option.isNone(state)) {
639
661
  // Reader fiber observed a frame without a corresponding
@@ -643,11 +665,7 @@ export class MoltZapWsClient {
643
665
  // gone too.
644
666
  continue;
645
667
  }
646
- const offered = yield* Effect.either(state.value.dispatcher.offer({
647
- id: decoded.id,
648
- method: decoded.method,
649
- params: decoded.params,
650
- }));
668
+ const offered = yield* Effect.either(state.value.dispatcher.offer(decoded));
651
669
  const offerFailure = Either.match(offered, {
652
670
  onLeft: (err) => err,
653
671
  onRight: () => null,
@@ -657,14 +675,14 @@ export class MoltZapWsClient {
657
675
  }
658
676
  continue;
659
677
  }
660
- if (decoded._tag === "Event") {
678
+ if (decoded._tag === "Notification") {
661
679
  // Spec #222 §5.3 (C4 + subscribe-stub): per-subscription
662
- // fan-out replaces the deleted top-level `onEvent` callback.
680
+ // fan-out replaces the deleted top-level `onNotification` callback.
663
681
  // Snapshot-at-dispatch semantics live inside the registry
664
682
  // (OQ-3 A); see runtime/subscribers.ts.
665
- yield* this.subscribers.dispatch(decoded.frame);
666
- const delivered = yield* Ref.modify(this.eventWaitersRef, (m) => {
667
- const bucket = HashMap.get(m, decoded.frame.event);
683
+ yield* this.subscribers.dispatch(decoded);
684
+ const delivered = yield* Ref.modify(this.notificationWaitersRef, (m) => {
685
+ const bucket = HashMap.get(m, decoded.definition);
668
686
  if (bucket._tag === "None" || bucket.value.length === 0) {
669
687
  return [null, m];
670
688
  }
@@ -672,16 +690,16 @@ export class MoltZapWsClient {
672
690
  const chosen = arr[arr.length - 1];
673
691
  const rest = arr.slice(0, -1);
674
692
  const nextMap = rest.length === 0
675
- ? HashMap.remove(m, decoded.frame.event)
676
- : HashMap.set(m, decoded.frame.event, rest);
693
+ ? HashMap.remove(m, decoded.definition)
694
+ : HashMap.set(m, decoded.definition, rest);
677
695
  return [chosen, nextMap];
678
696
  });
679
697
  if (delivered !== null) {
680
- yield* Deferred.succeed(delivered.deferred, decoded.frame).pipe(Effect.ignore);
698
+ yield* delivered.complete(decoded);
681
699
  continue;
682
700
  }
683
- yield* Ref.update(this.eventsBufferRef, (xs) => {
684
- const appended = [...xs, decoded.frame];
701
+ yield* Ref.update(this.notificationsBufferRef, (xs) => {
702
+ const appended = [...xs, decoded];
685
703
  return appended.length > MAX_EVENT_BUFFER
686
704
  ? appended.slice(-MAX_EVENT_BUFFER)
687
705
  : appended;
@@ -690,13 +708,13 @@ export class MoltZapWsClient {
690
708
  }
691
709
  });
692
710
  }
693
- /** Fail every outstanding event waiter with `message` and clear the map. */
694
- failAllEventWaiters(message) {
711
+ /** Fail every outstanding notification waiter with `message` and clear the map. */
712
+ failAllNotificationWaiters(message) {
695
713
  return Effect.gen(this, function* () {
696
- const waiters = yield* Ref.getAndSet(this.eventWaitersRef, HashMap.empty());
714
+ const waiters = yield* Ref.getAndSet(this.notificationWaitersRef, HashMap.empty());
697
715
  for (const [, bucket] of HashMap.entries(waiters)) {
698
716
  for (const w of bucket) {
699
- yield* Deferred.fail(w.deferred, new Error(message)).pipe(Effect.ignore);
717
+ yield* w.fail(new Error(message));
700
718
  }
701
719
  }
702
720
  });