@moltzap/client 2026.503.4 → 2026.504.1
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.
- package/dist/channel-core.d.ts +1 -4
- package/dist/channel-core.d.ts.map +1 -1
- package/dist/channel-core.js +0 -9
- package/dist/channel-core.js.map +1 -1
- package/dist/channel-core.test.js +90 -71
- package/dist/channel-core.test.js.map +1 -1
- package/dist/cli/commands/agents.js +2 -2
- package/dist/cli/commands/agents.js.map +1 -1
- package/dist/cli/commands/apps.d.ts +2 -24
- package/dist/cli/commands/apps.d.ts.map +1 -1
- package/dist/cli/commands/apps.js +22 -47
- package/dist/cli/commands/apps.js.map +1 -1
- package/dist/cli/commands/apps.test.js +59 -72
- package/dist/cli/commands/apps.test.js.map +1 -1
- package/dist/cli/commands/contacts.d.ts +1 -5
- package/dist/cli/commands/contacts.d.ts.map +1 -1
- package/dist/cli/commands/contacts.js +11 -26
- package/dist/cli/commands/contacts.js.map +1 -1
- package/dist/cli/commands/conversations-v2.test.js +30 -26
- package/dist/cli/commands/conversations-v2.test.js.map +1 -1
- package/dist/cli/commands/conversations.d.ts.map +1 -1
- package/dist/cli/commands/conversations.js +48 -33
- package/dist/cli/commands/conversations.js.map +1 -1
- package/dist/cli/commands/messages.d.ts.map +1 -1
- package/dist/cli/commands/messages.js +11 -8
- package/dist/cli/commands/messages.js.map +1 -1
- package/dist/cli/commands/messages.test.js +28 -30
- package/dist/cli/commands/messages.test.js.map +1 -1
- package/dist/cli/commands/presence.d.ts.map +1 -1
- package/dist/cli/commands/presence.js +5 -4
- package/dist/cli/commands/presence.js.map +1 -1
- package/dist/cli/commands/send.d.ts.map +1 -1
- package/dist/cli/commands/send.js +20 -11
- package/dist/cli/commands/send.js.map +1 -1
- package/dist/cli/commands/send.test.js +11 -9
- package/dist/cli/commands/send.test.js.map +1 -1
- package/dist/cli/commands/status.js +2 -2
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/test-transport.d.ts +10 -0
- package/dist/cli/commands/test-transport.d.ts.map +1 -0
- package/dist/cli/commands/test-transport.js +29 -0
- package/dist/cli/commands/test-transport.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +0 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/socket-client.d.ts +9 -4
- package/dist/cli/socket-client.d.ts.map +1 -1
- package/dist/cli/socket-client.js +27 -12
- package/dist/cli/socket-client.js.map +1 -1
- package/dist/cli/transport.d.ts +3 -2
- package/dist/cli/transport.d.ts.map +1 -1
- package/dist/cli/transport.js +6 -6
- package/dist/cli/transport.js.map +1 -1
- package/dist/cli/transport.test.js +4 -4
- package/dist/cli/transport.test.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/__tests__/app-callback-partition-key.test.d.ts +2 -0
- package/dist/internal/__tests__/app-callback-partition-key.test.d.ts.map +1 -0
- package/dist/internal/__tests__/app-callback-partition-key.test.js +33 -0
- package/dist/internal/__tests__/app-callback-partition-key.test.js.map +1 -0
- package/dist/internal/__tests__/app-callback-partition-worker.test.d.ts +2 -0
- package/dist/internal/__tests__/app-callback-partition-worker.test.d.ts.map +1 -0
- package/dist/internal/__tests__/{s2c-partition-worker.test.js → app-callback-partition-worker.test.js} +6 -9
- package/dist/internal/__tests__/app-callback-partition-worker.test.js.map +1 -0
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.d.ts +2 -0
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.d.ts.map +1 -0
- package/dist/internal/__tests__/{s2c-partitioned-dispatcher-real-ws.test.js → app-callback-partitioned-dispatcher-real-ws.test.js} +69 -94
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher-real-ws.test.js.map +1 -0
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.d.ts +2 -0
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.d.ts.map +1 -0
- package/dist/internal/__tests__/{s2c-partitioned-dispatcher.test.js → app-callback-partitioned-dispatcher.test.js} +35 -69
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.js.map +1 -0
- package/dist/internal/__tests__/app-callback-test-requests.d.ts +43 -0
- package/dist/internal/__tests__/app-callback-test-requests.d.ts.map +1 -0
- package/dist/internal/__tests__/app-callback-test-requests.js +101 -0
- package/dist/internal/__tests__/app-callback-test-requests.js.map +1 -0
- package/dist/internal/{s2c-dispatcher-errors.d.ts → app-callback-dispatcher-errors.d.ts} +5 -21
- package/dist/internal/app-callback-dispatcher-errors.d.ts.map +1 -0
- package/dist/internal/{s2c-dispatcher-errors.js → app-callback-dispatcher-errors.js} +3 -13
- package/dist/internal/app-callback-dispatcher-errors.js.map +1 -0
- package/dist/internal/app-callback-partition-key.d.ts +68 -0
- package/dist/internal/app-callback-partition-key.d.ts.map +1 -0
- package/dist/internal/app-callback-partition-key.js +90 -0
- package/dist/internal/app-callback-partition-key.js.map +1 -0
- package/dist/internal/{s2c-partition-worker.d.ts → app-callback-partition-worker.d.ts} +5 -5
- package/dist/internal/app-callback-partition-worker.d.ts.map +1 -0
- package/dist/internal/{s2c-partition-worker.js → app-callback-partition-worker.js} +5 -5
- package/dist/internal/app-callback-partition-worker.js.map +1 -0
- package/dist/internal/{s2c-partitioned-dispatcher.d.ts → app-callback-partitioned-dispatcher.d.ts} +9 -10
- package/dist/internal/app-callback-partitioned-dispatcher.d.ts.map +1 -0
- package/dist/internal/{s2c-partitioned-dispatcher.js → app-callback-partitioned-dispatcher.js} +12 -19
- package/dist/internal/app-callback-partitioned-dispatcher.js.map +1 -0
- package/dist/runtime/errors.d.ts +2 -28
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +0 -9
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/errors.test.js +2 -2
- package/dist/runtime/errors.test.js.map +1 -1
- package/dist/runtime/frame.d.ts +17 -18
- package/dist/runtime/frame.d.ts.map +1 -1
- package/dist/runtime/frame.js +83 -31
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/frame.test.js +15 -11
- package/dist/runtime/frame.test.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/local-service-commands.d.ts +7 -0
- package/dist/runtime/local-service-commands.d.ts.map +1 -0
- package/dist/runtime/local-service-commands.js +6 -0
- package/dist/runtime/local-service-commands.js.map +1 -0
- package/dist/runtime/runtime.test.js +1 -2
- package/dist/runtime/runtime.test.js.map +1 -1
- package/dist/runtime/subscribers.d.ts +34 -29
- package/dist/runtime/subscribers.d.ts.map +1 -1
- package/dist/runtime/subscribers.js +19 -19
- package/dist/runtime/subscribers.js.map +1 -1
- package/dist/runtime/subscribers.test.js +50 -31
- package/dist/runtime/subscribers.test.js.map +1 -1
- package/dist/service.d.ts +24 -39
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +165 -161
- package/dist/service.js.map +1 -1
- package/dist/service.test.js +111 -170
- package/dist/service.test.js.map +1 -1
- package/dist/test-utils/channel-service-fixture.d.ts +1 -2
- package/dist/test-utils/channel-service-fixture.d.ts.map +1 -1
- package/dist/test-utils/channel-service-fixture.js +28 -20
- package/dist/test-utils/channel-service-fixture.js.map +1 -1
- package/dist/test-utils/conformance-adapter.d.ts +2 -2
- package/dist/test-utils/conformance-adapter.d.ts.map +1 -1
- package/dist/test-utils/conformance-adapter.js +33 -26
- package/dist/test-utils/conformance-adapter.js.map +1 -1
- package/dist/test-utils/fake-service.d.ts +14 -39
- package/dist/test-utils/fake-service.d.ts.map +1 -1
- package/dist/test-utils/fake-service.js +33 -39
- package/dist/test-utils/fake-service.js.map +1 -1
- package/dist/test-utils/ids.d.ts +9 -0
- package/dist/test-utils/ids.d.ts.map +1 -0
- package/dist/test-utils/ids.js +52 -0
- package/dist/test-utils/ids.js.map +1 -0
- package/dist/test-utils/index.d.ts +10 -2
- package/dist/test-utils/index.d.ts.map +1 -1
- package/dist/test-utils/index.js +12 -5
- package/dist/test-utils/index.js.map +1 -1
- package/dist/ws-client.d.ts +58 -64
- package/dist/ws-client.d.ts.map +1 -1
- package/dist/ws-client.js +151 -133
- package/dist/ws-client.js.map +1 -1
- package/dist/ws-client.test.js +157 -167
- package/dist/ws-client.test.js.map +1 -1
- package/package.json +3 -3
- package/dist/cli/commands/permissions.d.ts +0 -70
- package/dist/cli/commands/permissions.d.ts.map +0 -1
- package/dist/cli/commands/permissions.js +0 -100
- package/dist/cli/commands/permissions.js.map +0 -1
- package/dist/cli/commands/permissions.test.d.ts +0 -2
- package/dist/cli/commands/permissions.test.d.ts.map +0 -1
- package/dist/cli/commands/permissions.test.js +0 -127
- package/dist/cli/commands/permissions.test.js.map +0 -1
- package/dist/internal/__tests__/s2c-partition-key.test.d.ts +0 -2
- package/dist/internal/__tests__/s2c-partition-key.test.d.ts.map +0 -1
- package/dist/internal/__tests__/s2c-partition-key.test.js +0 -231
- package/dist/internal/__tests__/s2c-partition-key.test.js.map +0 -1
- package/dist/internal/__tests__/s2c-partition-worker.test.d.ts +0 -2
- package/dist/internal/__tests__/s2c-partition-worker.test.d.ts.map +0 -1
- package/dist/internal/__tests__/s2c-partition-worker.test.js.map +0 -1
- package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.d.ts +0 -2
- package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.d.ts.map +0 -1
- package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.js.map +0 -1
- package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.d.ts +0 -2
- package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.d.ts.map +0 -1
- package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.js.map +0 -1
- package/dist/internal/s2c-dispatcher-errors.d.ts.map +0 -1
- package/dist/internal/s2c-dispatcher-errors.js.map +0 -1
- package/dist/internal/s2c-partition-key.d.ts +0 -79
- package/dist/internal/s2c-partition-key.d.ts.map +0 -1
- package/dist/internal/s2c-partition-key.js +0 -146
- package/dist/internal/s2c-partition-key.js.map +0 -1
- package/dist/internal/s2c-partition-worker.d.ts.map +0 -1
- package/dist/internal/s2c-partition-worker.js.map +0 -1
- package/dist/internal/s2c-partitioned-dispatcher.d.ts.map +0 -1
- 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
|
|
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/
|
|
10
|
-
import {
|
|
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
|
|
33
|
-
* `
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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,
|
|
51
|
-
: HashMap.set(m,
|
|
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
|
-
|
|
72
|
+
notificationsBufferRef;
|
|
71
73
|
/**
|
|
72
|
-
* Waiters keyed by
|
|
73
|
-
* pops the most recently registered waiter (the tail).
|
|
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
|
-
|
|
77
|
+
notificationWaitersRef;
|
|
78
78
|
runtime;
|
|
79
79
|
/**
|
|
80
|
-
* Per-subscription
|
|
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
|
|
90
|
+
* dispatcher fiber when an appCallback request frame arrives.
|
|
91
91
|
*/
|
|
92
|
-
|
|
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.
|
|
104
|
-
this.
|
|
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.
|
|
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
|
|
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(
|
|
126
|
+
handleServerRpc(definition, handler) {
|
|
127
127
|
return Effect.gen(this, function* () {
|
|
128
|
-
const swapped = yield* Ref.modify(this.
|
|
129
|
-
if (HashMap.has(m,
|
|
128
|
+
const swapped = yield* Ref.modify(this.appCallbackHandlersRef, (m) => {
|
|
129
|
+
if (HashMap.has(m, definition))
|
|
130
130
|
return [false, m];
|
|
131
|
-
return [
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
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` / `
|
|
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.
|
|
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
|
|
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
|
-
|
|
265
|
+
waitForNotification(definition, timeoutMs = EVENT_WAIT_TIMEOUT_MS) {
|
|
233
266
|
return Effect.gen(this, function* () {
|
|
234
|
-
const buffered = yield* Ref.modify(this.
|
|
235
|
-
const idx
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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 = {
|
|
246
|
-
|
|
247
|
-
|
|
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,
|
|
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
|
|
293
|
+
onTimeout: () => new Error(`Timeout waiting for notification: ${definition.name}`),
|
|
254
294
|
}), Effect.onExit((exit) => exit._tag === "Failure"
|
|
255
|
-
? Ref.update(this.
|
|
295
|
+
? Ref.update(this.notificationWaitersRef, (m) => removeWaiter(m, definition, waiter))
|
|
256
296
|
: Effect.void));
|
|
257
297
|
});
|
|
258
298
|
}
|
|
259
|
-
/** Return all buffered
|
|
260
|
-
|
|
261
|
-
const snapshot = this.runtime.runSync(Ref.get(this.
|
|
262
|
-
this.runtime.runSync(Ref.set(this.
|
|
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
|
|
312
|
-
// single `Stream.runForEach`-driven `
|
|
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.
|
|
334
|
-
? { config: this.options.
|
|
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
|
|
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
|
|
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.
|
|
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,
|
|
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(
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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(
|
|
488
|
-
return this.sendRpcTrackedEffect(
|
|
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("
|
|
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(
|
|
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(
|
|
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
|
|
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.
|
|
549
|
-
const lookup = HashMap.get(handlers, request.
|
|
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(
|
|
575
|
+
? Effect.succeed(responseFrame(request.id, {
|
|
552
576
|
error: {
|
|
553
577
|
code: -32601,
|
|
554
|
-
message:
|
|
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
|
-
|
|
561
|
-
traceparent: request.traceparent,
|
|
584
|
+
definition: request.definition,
|
|
562
585
|
})
|
|
563
586
|
.pipe(Effect.match({
|
|
564
|
-
onSuccess: (result) => responseFrame(
|
|
587
|
+
onSuccess: (result) => responseFrame(request.id, {
|
|
565
588
|
result,
|
|
566
589
|
}),
|
|
567
|
-
onFailure: (err) => responseFrame(
|
|
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(
|
|
576
|
-
return responseFrame(
|
|
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("
|
|
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;
|
|
585
|
-
* frames dispatch to `
|
|
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
|
-
//
|
|
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
|
|
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 === "
|
|
678
|
+
if (decoded._tag === "Notification") {
|
|
661
679
|
// Spec #222 §5.3 (C4 + subscribe-stub): per-subscription
|
|
662
|
-
// fan-out replaces the deleted top-level `
|
|
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
|
|
666
|
-
const delivered = yield* Ref.modify(this.
|
|
667
|
-
const bucket = HashMap.get(m, decoded.
|
|
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.
|
|
676
|
-
: HashMap.set(m, decoded.
|
|
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*
|
|
698
|
+
yield* delivered.complete(decoded);
|
|
681
699
|
continue;
|
|
682
700
|
}
|
|
683
|
-
yield* Ref.update(this.
|
|
684
|
-
const appended = [...xs, decoded
|
|
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
|
|
694
|
-
|
|
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.
|
|
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*
|
|
717
|
+
yield* w.fail(new Error(message));
|
|
700
718
|
}
|
|
701
719
|
}
|
|
702
720
|
});
|