@moltzap/client 2026.501.1 → 2026.503.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 (44) hide show
  1. package/dist/cli/commands/messages.d.ts.map +1 -1
  2. package/dist/cli/commands/messages.js +1 -1
  3. package/dist/cli/commands/messages.js.map +1 -1
  4. package/dist/cli/commands/messages.test.js +6 -2
  5. package/dist/cli/commands/messages.test.js.map +1 -1
  6. package/dist/internal/__tests__/s2c-partition-key.test.d.ts +2 -0
  7. package/dist/internal/__tests__/s2c-partition-key.test.d.ts.map +1 -0
  8. package/dist/internal/__tests__/s2c-partition-key.test.js +230 -0
  9. package/dist/internal/__tests__/s2c-partition-key.test.js.map +1 -0
  10. package/dist/internal/__tests__/s2c-partition-worker.test.d.ts +2 -0
  11. package/dist/internal/__tests__/s2c-partition-worker.test.d.ts.map +1 -0
  12. package/dist/internal/__tests__/s2c-partition-worker.test.js +267 -0
  13. package/dist/internal/__tests__/s2c-partition-worker.test.js.map +1 -0
  14. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.d.ts +2 -0
  15. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.d.ts.map +1 -0
  16. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.js +254 -0
  17. package/dist/internal/__tests__/s2c-partitioned-dispatcher-real-ws.test.js.map +1 -0
  18. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.d.ts +2 -0
  19. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.d.ts.map +1 -0
  20. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.js +571 -0
  21. package/dist/internal/__tests__/s2c-partitioned-dispatcher.test.js.map +1 -0
  22. package/dist/internal/s2c-dispatcher-errors.d.ts +66 -0
  23. package/dist/internal/s2c-dispatcher-errors.d.ts.map +1 -0
  24. package/dist/internal/s2c-dispatcher-errors.js +46 -0
  25. package/dist/internal/s2c-dispatcher-errors.js.map +1 -0
  26. package/dist/internal/s2c-partition-key.d.ts +70 -0
  27. package/dist/internal/s2c-partition-key.d.ts.map +1 -0
  28. package/dist/internal/s2c-partition-key.js +140 -0
  29. package/dist/internal/s2c-partition-key.js.map +1 -0
  30. package/dist/internal/s2c-partition-worker.d.ts +160 -0
  31. package/dist/internal/s2c-partition-worker.d.ts.map +1 -0
  32. package/dist/internal/s2c-partition-worker.js +141 -0
  33. package/dist/internal/s2c-partition-worker.js.map +1 -0
  34. package/dist/internal/s2c-partitioned-dispatcher.d.ts +181 -0
  35. package/dist/internal/s2c-partitioned-dispatcher.d.ts.map +1 -0
  36. package/dist/internal/s2c-partitioned-dispatcher.js +290 -0
  37. package/dist/internal/s2c-partitioned-dispatcher.js.map +1 -0
  38. package/dist/ws-client.d.ts +27 -0
  39. package/dist/ws-client.d.ts.map +1 -1
  40. package/dist/ws-client.js +107 -57
  41. package/dist/ws-client.js.map +1 -1
  42. package/dist/ws-client.test.js +7 -1
  43. package/dist/ws-client.test.js.map +1 -1
  44. package/package.json +3 -3
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Partitioned s2c dispatcher.
3
+ *
4
+ * Spec: moltzap#356.
5
+ *
6
+ * Replaces the single `Stream.runForEach(handler)` over `s2cInboundQueue`
7
+ * (`packages/client/src/ws-client.ts:658-687@f0df363`) with a
8
+ * partition-router that owns a `Ref<HashMap<PartitionKey,
9
+ * PartitionWorker>>`. For each inbound request:
10
+ *
11
+ * 1. The reader fiber calls `dispatcher.offer(request)`.
12
+ * 2. The dispatcher extracts the partition key (pure, no schema decode).
13
+ * 3. If a worker exists for that key, `worker.offer(request)`.
14
+ * 4. Otherwise, allocate a worker (subject to soft cap) and offer.
15
+ *
16
+ * The dispatcher itself does NOT run a long-lived router fiber: routing
17
+ * is synchronous-up-to-effect on the reader's call thread. The only
18
+ * dispatcher-owned fiber is the **idle reaper**, which periodically
19
+ * walks the partition map and finalizes any worker whose
20
+ * `idleSince._tag === "Idle"` exceeds `idlePartitionTtlMs`.
21
+ *
22
+ * Single-producer guarantee: `dispatcher.offer` is only called from
23
+ * the per-connection reader fiber's `Stream.runForEach` loop in
24
+ * `ws-client.ts:1052-1086`. There is no second producer; concurrent
25
+ * `offer` calls within one dispatcher cannot occur. The actual
26
+ * concurrency property a router would otherwise be defending against
27
+ * is route-vs-drain: the route step (one `Ref.modify` against the
28
+ * partition map + a non-blocking `worker.offer`) is wait-free against
29
+ * every running worker fiber's drain loop. No router fiber means no
30
+ * intermediate buffer between the reader and the per-key worker
31
+ * queue, and no scheduling overhead per route decision.
32
+ *
33
+ * Lifetime: `make` returns `Effect<…, never, Scope.Scope>`. The caller
34
+ * (`ws-client.ts` inside `connectEffect`) calls `Scope.make()` to
35
+ * produce a **dedicated dispatcher Scope** that is NOT bound to the
36
+ * socket Scope, then provides that Scope to `makePartitionedDispatcher`.
37
+ * The dispatcher Scope is held in `ConnState`; teardown from
38
+ * `close()` / `disconnectSync()` is
39
+ * `runtime.runFork(Scope.close(dispatcherScope, Exit.void))`, mirroring
40
+ * today's `runFork(Queue.shutdown(s2cInboundQueue))` +
41
+ * `runFork(Fiber.interrupt(s2cDispatcherFiber))` pair.
42
+ *
43
+ * Why off-Scope from the socket: `close()` is invoked via `runSync` at
44
+ * `packages/openclaw-channel/src/__tests__/reconnection.integration.test.ts:14`;
45
+ * binding the dispatcher to the socket Scope would make `Scope.close`
46
+ * yield through the runtime (queue.shutdown + fiber.interrupt are
47
+ * non-synchronous), breaking the regression gate at
48
+ * `packages/client/src/ws-client.test.ts:1233-1259`.
49
+ * The `runSync(client.close())` contract is load-bearing — see the
50
+ * test commentary there.
51
+ *
52
+ * Worker scopes are forked children of the dispatcher Scope
53
+ * (`getOrCreatePartitionWorker` calls `Scope.fork(dispatcherScope, …)`
54
+ * for each new worker and provides the child scope to
55
+ * `makePartitionWorker`). Closing the dispatcher Scope cascades to
56
+ * every worker; closing one worker's child scope retires only that
57
+ * worker without leaking finalizers onto the dispatcher scope.
58
+ */
59
+ import { Effect, Scope } from "effect";
60
+ import { type OfferRejected } from "./s2c-dispatcher-errors.js";
61
+ import { type PartitionKey, type PartitionableRequest } from "./s2c-partition-key.js";
62
+ import { type PartitionHandler } from "./s2c-partition-worker.js";
63
+ import type { WsClientLogger } from "../ws-client.js";
64
+ /**
65
+ * Tunable knobs. Defaults named in the design doc; downstream picks
66
+ * exact values at the `MoltZapWsClient` constructor edge.
67
+ */
68
+ export interface PartitionedDispatcherConfig {
69
+ /**
70
+ * Soft cap on simultaneously-active partitions per connection.
71
+ * Default 256. Hit → new partitions fail with `PartitionLimitError`
72
+ * until the idle reaper retires one.
73
+ */
74
+ readonly maxPartitions: number;
75
+ /**
76
+ * Bounded queue depth per partition. Default 32. Hit → `offer` fails
77
+ * with `PartitionQueueFullError` for THIS partition only.
78
+ */
79
+ readonly partitionQueueCapacity: number;
80
+ /**
81
+ * Idle reaper threshold: a worker whose queue has been continuously
82
+ * empty for at least this many milliseconds is finalized on the next
83
+ * reaper tick. Default 60_000.
84
+ */
85
+ readonly idlePartitionTtlMs: number;
86
+ /**
87
+ * Idle reaper tick period. Default 5_000. Lower = tighter reclamation,
88
+ * higher CPU cost; higher = looser reclamation, lower overhead.
89
+ */
90
+ readonly idleReaperIntervalMs: number;
91
+ }
92
+ /** Defaults. Documented as the named knob values for the implementation. */
93
+ export declare const DEFAULT_PARTITIONED_DISPATCHER_CONFIG: PartitionedDispatcherConfig;
94
+ /**
95
+ * Snapshot of dispatcher state for tests + observability. NOT stable
96
+ * across releases; consumers must treat as best-effort instrumentation.
97
+ */
98
+ export interface DispatcherStats {
99
+ readonly activePartitions: number;
100
+ readonly totalOffered: number;
101
+ readonly totalRejected: {
102
+ readonly malformed: number;
103
+ readonly partitionLimit: number;
104
+ readonly partitionQueueFull: number;
105
+ };
106
+ readonly partitions: ReadonlyArray<{
107
+ readonly key: PartitionKey;
108
+ readonly queueSize: number;
109
+ readonly idle: boolean;
110
+ }>;
111
+ }
112
+ /**
113
+ * Public surface used by `ws-client.ts`. Construction is scoped — the
114
+ * caller's `Scope` owns every partition fiber.
115
+ */
116
+ export interface PartitionedDispatcher {
117
+ /**
118
+ * Route one inbound request. Non-blocking. Failure modes are
119
+ * exhaustive in `OfferRejected`; the reader fiber `Effect.match`es
120
+ * the tag set to pick a wire-error code.
121
+ *
122
+ * Per-tuple ordering: two `offer` calls with the same `PartitionKey`
123
+ * land on the same `PartitionWorker.offer` (FIFO).
124
+ * Cross-tuple concurrency: two `offer` calls with different keys
125
+ * produce no ordering relation.
126
+ */
127
+ readonly offer: (request: PartitionableRequest) => Effect.Effect<void, OfferRejected>;
128
+ /**
129
+ * Best-effort stats snapshot. Not used in the hot path; called by
130
+ * tests + the optional `client.stats()` debug surface.
131
+ */
132
+ readonly stats: Effect.Effect<DispatcherStats>;
133
+ /**
134
+ * Explicit shutdown. Equivalent to `Scope.close(dispatcherScope,
135
+ * Exit.void)`; exists as a typed handle so callsites that already
136
+ * hold the dispatcher (not the Scope) can tear it down without
137
+ * threading the Scope through `ConnState`. Idempotent.
138
+ *
139
+ * Callsites that invoke this from a `runSync` context (`close()`,
140
+ * `disconnectSync()`) MUST wrap with `runtime.runFork`. The Effect
141
+ * itself yields through the runtime (queue shutdown + fiber
142
+ * interrupt are non-synchronous); calling `runSync` on it would throw
143
+ * `AsyncFiberException`. See the regression gate at
144
+ * `packages/client/src/ws-client.test.ts:1233-1259@f0df363`.
145
+ */
146
+ readonly shutdown: Effect.Effect<void>;
147
+ }
148
+ /**
149
+ * Construction params. `handle` is the per-request work — passed in so
150
+ * the dispatcher has no direct dependency on the handler-registry
151
+ * `Ref<HashMap<string, ServerRpcHandler>>` (`s2cHandlersRef`); the
152
+ * caller (`ws-client.ts`) closes over the registry and supplies a
153
+ * pre-bound function. This preserves the wire-edge schema-decoding
154
+ * boundary already established in `dispatchInboundServerRequest`.
155
+ *
156
+ * `scope` is the `Scope.CloseableScope` that owns every per-partition
157
+ * worker fiber + the idle reaper. The caller (`ws-client.ts`)
158
+ * allocates it via `Scope.make()` outside the per-connect socket
159
+ * Scope so `runSync(client.close())` can `runFork(Scope.close(scope,
160
+ * Exit.void))` without yielding through the runtime.
161
+ */
162
+ export interface MakePartitionedDispatcherParams {
163
+ readonly handle: PartitionHandler;
164
+ readonly scope: Scope.CloseableScope;
165
+ readonly config?: Partial<PartitionedDispatcherConfig>;
166
+ readonly logger?: WsClientLogger;
167
+ /** Monotonic clock for tests. Defaults to `Date.now`. */
168
+ readonly clock?: () => number;
169
+ }
170
+ /**
171
+ * Construct a partitioned dispatcher. The caller-provided
172
+ * `params.scope` owns every per-partition fiber + the idle-reaper
173
+ * fiber; closing it cascades teardown via `Scope` finalizers.
174
+ *
175
+ * Returns `Effect<…, never>` — neither construction nor partition
176
+ * allocation can fail. The Effect itself does not require a `Scope`
177
+ * because every scoped allocation is provided to `params.scope`
178
+ * explicitly via `Scope.extend`.
179
+ */
180
+ export declare function makePartitionedDispatcher(params: MakePartitionedDispatcherParams): Effect.Effect<PartitionedDispatcher, never>;
181
+ //# sourceMappingURL=s2c-partitioned-dispatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s2c-partitioned-dispatcher.d.ts","sourceRoot":"","sources":["../../src/internal/s2c-partitioned-dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,OAAO,EAGL,MAAM,EASN,KAAK,EACN,MAAM,QAAQ,CAAC;AAChB,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,oBAAoB,EAC1B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAEL,KAAK,gBAAgB,EAEtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC;;;OAGG;IACH,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;CACvC;AAED,4EAA4E;AAC5E,eAAO,MAAM,qCAAqC,EAAE,2BAMjD,CAAC;AAEJ;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE;QACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;QAChC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;KACrC,CAAC;IACF,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;QACjC,QAAQ,CAAC,GAAG,EAAE,YAAY,CAAC;QAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;KACxB,CAAC,CAAC;CACJ;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;OASG;IACH,QAAQ,CAAC,KAAK,EAAE,CACd,OAAO,EAAE,oBAAoB,KAC1B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IACxC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAC/C;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACxC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,cAAc,CAAC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACvD,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;IACjC,yDAAyD;IACzD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;CAC/B;AAqCD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,+BAA+B,GACtC,MAAM,CAAC,MAAM,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAqH7C"}
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Partitioned s2c dispatcher.
3
+ *
4
+ * Spec: moltzap#356.
5
+ *
6
+ * Replaces the single `Stream.runForEach(handler)` over `s2cInboundQueue`
7
+ * (`packages/client/src/ws-client.ts:658-687@f0df363`) with a
8
+ * partition-router that owns a `Ref<HashMap<PartitionKey,
9
+ * PartitionWorker>>`. For each inbound request:
10
+ *
11
+ * 1. The reader fiber calls `dispatcher.offer(request)`.
12
+ * 2. The dispatcher extracts the partition key (pure, no schema decode).
13
+ * 3. If a worker exists for that key, `worker.offer(request)`.
14
+ * 4. Otherwise, allocate a worker (subject to soft cap) and offer.
15
+ *
16
+ * The dispatcher itself does NOT run a long-lived router fiber: routing
17
+ * is synchronous-up-to-effect on the reader's call thread. The only
18
+ * dispatcher-owned fiber is the **idle reaper**, which periodically
19
+ * walks the partition map and finalizes any worker whose
20
+ * `idleSince._tag === "Idle"` exceeds `idlePartitionTtlMs`.
21
+ *
22
+ * Single-producer guarantee: `dispatcher.offer` is only called from
23
+ * the per-connection reader fiber's `Stream.runForEach` loop in
24
+ * `ws-client.ts:1052-1086`. There is no second producer; concurrent
25
+ * `offer` calls within one dispatcher cannot occur. The actual
26
+ * concurrency property a router would otherwise be defending against
27
+ * is route-vs-drain: the route step (one `Ref.modify` against the
28
+ * partition map + a non-blocking `worker.offer`) is wait-free against
29
+ * every running worker fiber's drain loop. No router fiber means no
30
+ * intermediate buffer between the reader and the per-key worker
31
+ * queue, and no scheduling overhead per route decision.
32
+ *
33
+ * Lifetime: `make` returns `Effect<…, never, Scope.Scope>`. The caller
34
+ * (`ws-client.ts` inside `connectEffect`) calls `Scope.make()` to
35
+ * produce a **dedicated dispatcher Scope** that is NOT bound to the
36
+ * socket Scope, then provides that Scope to `makePartitionedDispatcher`.
37
+ * The dispatcher Scope is held in `ConnState`; teardown from
38
+ * `close()` / `disconnectSync()` is
39
+ * `runtime.runFork(Scope.close(dispatcherScope, Exit.void))`, mirroring
40
+ * today's `runFork(Queue.shutdown(s2cInboundQueue))` +
41
+ * `runFork(Fiber.interrupt(s2cDispatcherFiber))` pair.
42
+ *
43
+ * Why off-Scope from the socket: `close()` is invoked via `runSync` at
44
+ * `packages/openclaw-channel/src/__tests__/reconnection.integration.test.ts:14`;
45
+ * binding the dispatcher to the socket Scope would make `Scope.close`
46
+ * yield through the runtime (queue.shutdown + fiber.interrupt are
47
+ * non-synchronous), breaking the regression gate at
48
+ * `packages/client/src/ws-client.test.ts:1233-1259`.
49
+ * The `runSync(client.close())` contract is load-bearing — see the
50
+ * test commentary there.
51
+ *
52
+ * Worker scopes are forked children of the dispatcher Scope
53
+ * (`getOrCreatePartitionWorker` calls `Scope.fork(dispatcherScope, …)`
54
+ * for each new worker and provides the child scope to
55
+ * `makePartitionWorker`). Closing the dispatcher Scope cascades to
56
+ * every worker; closing one worker's child scope retires only that
57
+ * worker without leaking finalizers onto the dispatcher scope.
58
+ */
59
+ import { Cause, Duration, Effect, Either, ExecutionStrategy, Exit, HashMap, MutableRef, Option, Ref, Schedule, Scope, } from "effect";
60
+ import { PartitionLimitError, } from "./s2c-dispatcher-errors.js";
61
+ import { extractPartitionKey, } from "./s2c-partition-key.js";
62
+ import { makePartitionWorker, } from "./s2c-partition-worker.js";
63
+ /** Defaults. Documented as the named knob values for the implementation. */
64
+ export const DEFAULT_PARTITIONED_DISPATCHER_CONFIG = {
65
+ maxPartitions: 256,
66
+ partitionQueueCapacity: 32,
67
+ idlePartitionTtlMs: 60_000,
68
+ idleReaperIntervalMs: 5_000,
69
+ };
70
+ /**
71
+ * Construct a partitioned dispatcher. The caller-provided
72
+ * `params.scope` owns every per-partition fiber + the idle-reaper
73
+ * fiber; closing it cascades teardown via `Scope` finalizers.
74
+ *
75
+ * Returns `Effect<…, never>` — neither construction nor partition
76
+ * allocation can fail. The Effect itself does not require a `Scope`
77
+ * because every scoped allocation is provided to `params.scope`
78
+ * explicitly via `Scope.extend`.
79
+ */
80
+ export function makePartitionedDispatcher(params) {
81
+ const config = {
82
+ ...DEFAULT_PARTITIONED_DISPATCHER_CONFIG,
83
+ ...params.config,
84
+ };
85
+ const clock = params.clock ?? Date.now;
86
+ return Effect.gen(function* () {
87
+ const partitionsRef = yield* Ref.make(HashMap.empty());
88
+ const counters = {
89
+ totalOffered: yield* Ref.make(0),
90
+ malformed: yield* Ref.make(0),
91
+ partitionLimit: yield* Ref.make(0),
92
+ partitionQueueFull: yield* Ref.make(0),
93
+ };
94
+ const dispatcherScope = params.scope;
95
+ const internal = {
96
+ partitionsRef,
97
+ dispatcherScope,
98
+ config,
99
+ handle: params.handle,
100
+ ...(params.logger !== undefined ? { logger: params.logger } : {}),
101
+ clock,
102
+ counters,
103
+ };
104
+ // Idle-reaper fiber. One fiber per dispatcher (NOT per partition);
105
+ // walks the map every `idleReaperIntervalMs` and finalizes
106
+ // partitions whose `idleSince._tag === "Idle"` exceeds the TTL.
107
+ const reaperEffect = reapIdlePartitions(internal).pipe(Effect.repeat(Schedule.spaced(Duration.millis(config.idleReaperIntervalMs))), Effect.asVoid, Effect.catchAllCause((cause) => Effect.sync(() => params.logger?.warn("s2c idle reaper exited", Cause.pretty(cause)))));
108
+ yield* Scope.extend(Effect.forkScoped(reaperEffect), dispatcherScope);
109
+ const offer = (request) => Effect.gen(function* () {
110
+ yield* Ref.update(counters.totalOffered, (n) => n + 1);
111
+ const keyResult = extractPartitionKey(request);
112
+ if (Either.isLeft(keyResult)) {
113
+ yield* Ref.update(counters.malformed, (n) => n + 1);
114
+ return yield* Effect.fail(keyResult.left);
115
+ }
116
+ const key = keyResult.right;
117
+ const worker = yield* getOrCreatePartitionWorker({
118
+ key,
119
+ requestId: request.id,
120
+ state: internal,
121
+ }).pipe(Effect.tapError((err) => err._tag === "PartitionLimitError"
122
+ ? Ref.update(counters.partitionLimit, (n) => n + 1)
123
+ : Effect.void));
124
+ yield* worker
125
+ .offer(request)
126
+ .pipe(Effect.tapError(() => Ref.update(counters.partitionQueueFull, (n) => n + 1)));
127
+ });
128
+ const stats = Effect.gen(function* () {
129
+ const map = yield* Ref.get(partitionsRef);
130
+ const partitions = [];
131
+ for (const [key, worker] of HashMap.entries(map)) {
132
+ const queueSize = yield* worker.queueSize;
133
+ const idle = MutableRef.get(worker.idleSince)._tag === "Idle";
134
+ partitions.push({ key, queueSize, idle });
135
+ }
136
+ const totalOffered = yield* Ref.get(counters.totalOffered);
137
+ const malformed = yield* Ref.get(counters.malformed);
138
+ const partitionLimit = yield* Ref.get(counters.partitionLimit);
139
+ const partitionQueueFull = yield* Ref.get(counters.partitionQueueFull);
140
+ return {
141
+ activePartitions: HashMap.size(map),
142
+ totalOffered,
143
+ totalRejected: {
144
+ malformed,
145
+ partitionLimit,
146
+ partitionQueueFull,
147
+ },
148
+ partitions,
149
+ };
150
+ });
151
+ const shutdown = Scope.close(dispatcherScope, Exit.void).pipe(Effect.catchAllCause(() => Effect.void));
152
+ return {
153
+ offer,
154
+ stats,
155
+ shutdown,
156
+ };
157
+ });
158
+ }
159
+ // ─── Internal helpers ────────────────────────────────────────────────
160
+ /**
161
+ * Allocate a worker for `key` if absent and return it, or return the
162
+ * existing worker. Returns `PartitionLimitError` if creation would
163
+ * exceed `maxPartitions` AND no idle partition is reclaimable.
164
+ *
165
+ * NOT part of the public surface — the only public entry is `offer`.
166
+ *
167
+ * Race-safety: under concurrent offers for the same new key, exactly
168
+ * one allocation wins. The losing offer's worker is interrupted and
169
+ * the winner is returned. Since worker construction is `Effect`-shaped,
170
+ * we cannot put it inside `Ref.modify`'s synchronous lambda; instead
171
+ * we build outside, then atomic-insert with a check for prior winners
172
+ * inside `modify`. Worst-case overhead under contention: one extra
173
+ * worker built and immediately interrupted. Bounded by partition cap.
174
+ */
175
+ function getOrCreatePartitionWorker(params) {
176
+ return Effect.gen(function* () {
177
+ const { key, requestId, state } = params;
178
+ const existing = yield* Ref.get(state.partitionsRef).pipe(Effect.map((m) => HashMap.get(m, key)));
179
+ if (Option.isSome(existing)) {
180
+ return existing.value;
181
+ }
182
+ // Soft cap check is best-effort: under concurrent allocation we
183
+ // may temporarily exceed by 1-2 before the next reaper tick. The
184
+ // cap exists as a DoS guard, not an exact-cardinality invariant.
185
+ const sizeBeforeAlloc = yield* Ref.get(state.partitionsRef).pipe(Effect.map((m) => HashMap.size(m)));
186
+ if (sizeBeforeAlloc >= state.config.maxPartitions) {
187
+ return yield* Effect.fail(new PartitionLimitError({
188
+ key,
189
+ activePartitions: sizeBeforeAlloc,
190
+ maxPartitions: state.config.maxPartitions,
191
+ requestId,
192
+ }));
193
+ }
194
+ // Fork a per-worker child scope from the dispatcher scope. Every
195
+ // finalizer the worker registers (queue.shutdown, drain fiber)
196
+ // binds to this child scope, so the reaper can close it
197
+ // independently to retire one worker without orphaning finalizers
198
+ // on the dispatcher scope. Closing the dispatcher scope still
199
+ // cascades through every child scope via the fork link.
200
+ const workerScope = yield* Scope.fork(state.dispatcherScope, ExecutionStrategy.sequential);
201
+ const built = yield* makePartitionWorker({
202
+ key,
203
+ capacity: state.config.partitionQueueCapacity,
204
+ handle: state.handle,
205
+ scope: workerScope,
206
+ ...(state.logger !== undefined ? { logger: state.logger } : {}),
207
+ clock: state.clock,
208
+ });
209
+ // Insert (or recover the racer's winner). Atomic.
210
+ const inserted = yield* Ref.modify(state.partitionsRef, (m) => {
211
+ const present = HashMap.get(m, key);
212
+ if (Option.isSome(present)) {
213
+ return [{ kind: "lost-race", winner: present.value }, m];
214
+ }
215
+ return [{ kind: "won", winner: built }, HashMap.set(m, key, built)];
216
+ });
217
+ if (inserted.kind === "lost-race") {
218
+ // Discard the redundant worker. Closing its scope cascades to
219
+ // queue.shutdown + the drain fiber's interrupt, releasing every
220
+ // resource we just allocated.
221
+ state.logger?.warn(`s2c partition allocation lost race (key=${key}); discarding redundant worker`);
222
+ yield* Scope.close(workerScope, Exit.void).pipe(Effect.catchAllCause(() => Effect.void));
223
+ }
224
+ return inserted.winner;
225
+ });
226
+ }
227
+ /**
228
+ * One reaper tick. Walks the partition map, claiming any worker whose
229
+ * `idleSince._tag === "Idle"` AND whose age exceeds
230
+ * `idlePartitionTtlMs`, then closes each claimed worker's per-worker
231
+ * scope so its queue + drain fiber are torn down via Scope finalizers
232
+ * (no orphan finalizers on the dispatcher scope).
233
+ *
234
+ * Race-safety vs. concurrent offers (single-producer reader fiber):
235
+ *
236
+ * - The claim decision lives inside `Ref.modify(partitionsRef, …)`.
237
+ * The lambda is synchronous; idle reads go through
238
+ * `MutableRef.get` and the claim itself is a synchronous
239
+ * `MutableRef.set(worker.idleSince, Retiring)` inside the same
240
+ * lambda. From the producer's perspective the partition-map
241
+ * removal AND the `Retiring` transition happen as one atomic
242
+ * step.
243
+ * - The producer's `worker.offer` is the counter-CAS: it does
244
+ * `MutableRef.updateAndGet(idleSince, current → Retiring ? current
245
+ * : Busy)` and refuses to enqueue if it observes `Retiring`. Two
246
+ * interleavings are possible and both are safe:
247
+ * - Producer wins first: `idleSince` flips Idle→Busy. Reaper
248
+ * observes `Busy` (≠ Idle); skips. Producer's `Queue.offer`
249
+ * lands; drain processes it.
250
+ * - Reaper wins first: `idleSince` flips Idle→Retiring + map
251
+ * removal. Producer's `MutableRef.updateAndGet` returns
252
+ * `Retiring`; producer fails with
253
+ * `PartitionQueueFullError`. Reader-fiber writes a wire
254
+ * error response. Server's `Deferred.await` resolves on the
255
+ * error. No hang.
256
+ * - There is no third interleaving: the JS event loop cannot
257
+ * interleave inside a synchronous `Ref.modify` lambda or inside
258
+ * a synchronous `MutableRef.updateAndGet` call.
259
+ */
260
+ function reapIdlePartitions(state) {
261
+ return Effect.gen(function* () {
262
+ const now = state.clock();
263
+ const ttl = state.config.idlePartitionTtlMs;
264
+ // Atomic claim + remove. The lambda is synchronous; for each
265
+ // worker we decide to retire we (a) flip `idleSince` to
266
+ // `Retiring` so any concurrent `worker.offer` refuses to enqueue,
267
+ // and (b) drop the entry from the map. The `Ref.modify` commit
268
+ // makes both visible atomically.
269
+ const reaped = yield* Ref.modify(state.partitionsRef, (current) => {
270
+ const removed = [];
271
+ let next = current;
272
+ for (const [key, worker] of HashMap.entries(current)) {
273
+ const idle = MutableRef.get(worker.idleSince);
274
+ if (idle._tag === "Idle" && now - idle.sinceMs >= ttl) {
275
+ MutableRef.set(worker.idleSince, { _tag: "Retiring" });
276
+ removed.push(worker);
277
+ next = HashMap.remove(next, key);
278
+ }
279
+ }
280
+ return [removed, next];
281
+ });
282
+ // Close per-worker scopes outside the `Ref.modify` (Scope.close
283
+ // yields). Cascading finalizers: queue.shutdown → Stream exits →
284
+ // drain fiber returns. No leak on the dispatcher scope.
285
+ for (const worker of reaped) {
286
+ yield* Scope.close(worker.scope, Exit.void).pipe(Effect.catchAllCause(() => Effect.void));
287
+ }
288
+ });
289
+ }
290
+ //# sourceMappingURL=s2c-partitioned-dispatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s2c-partitioned-dispatcher.js","sourceRoot":"","sources":["../../src/internal/s2c-partitioned-dispatcher.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@ import { type EventFrame, type RpcDefinition, type TSchema, type Static } from "
3
3
  import { DuplicateServerRpcHandlerError, NotConnectedError, RpcServerError, RpcTimeoutError } from "./runtime/errors.js";
4
4
  import { type EventSubscription, type SubscriberHandler, type SubscriptionFilter } from "./runtime/subscribers.js";
5
5
  import { type CloseInfo } from "./runtime/close-info.js";
6
+ import { type PartitionedDispatcherConfig } from "./internal/s2c-partitioned-dispatcher.js";
6
7
  export type { CloseInfo };
7
8
  /**
8
9
  * Default per-RPC timeout. Exported so tests driving `TestClock` can match
@@ -12,6 +13,17 @@ export declare const RPC_TIMEOUT_MS = 30000;
12
13
  export interface RpcCallOptions {
13
14
  readonly timeoutMs?: number;
14
15
  }
16
+ /**
17
+ * s2c dispatcher knobs exposed on `MoltZapWsClientOptions`. All optional;
18
+ * defaults from `DEFAULT_PARTITIONED_DISPATCHER_CONFIG`.
19
+ *
20
+ * Spec #356 OQ-3 (`idlePartitionTtlMs`) ships at the architect's
21
+ * recommended default of 60_000 ms. Per OQ-5, 256 active partitions
22
+ * comfortably absorbs current arena workloads (≤15 keys per session
23
+ * at peak), so the cap doubles as a per-tenant DoS guard, not a
24
+ * cardinality lever.
25
+ */
26
+ export type S2cDispatcherConfig = Partial<PartitionedDispatcherConfig>;
15
27
  /**
16
28
  * Handler signature for `handleServerRpc`. The handler returns an
17
29
  * `Effect<unknown, RpcServerError>` — success values are encoded as the
@@ -48,6 +60,11 @@ export interface MoltZapWsClientOptions {
48
60
  onDisconnect?: (close: CloseInfo) => void;
49
61
  onReconnect?: (helloOk: unknown) => void;
50
62
  logger?: WsClientLogger;
63
+ /**
64
+ * Spec #356 — partitioned s2c dispatcher knobs. Optional; omitted
65
+ * fields fall back to `DEFAULT_PARTITIONED_DISPATCHER_CONFIG`.
66
+ */
67
+ s2cDispatcher?: S2cDispatcherConfig;
51
68
  }
52
69
  /**
53
70
  * Return shape of `MoltZapWsClient.sendRpcTracked`. Spec #222 OQ-1
@@ -203,6 +220,16 @@ export declare class MoltZapWsClient {
203
220
  * race described below.
204
221
  */
205
222
  private sendRpcEffect;
223
+ /**
224
+ * Translate a typed `OfferRejected` failure from the partitioned
225
+ * dispatcher into a wire-level error response. Spec #356 §5: each
226
+ * tag maps to a JSON-RPC error code so the server's
227
+ * `Deferred.await` settles deterministically rather than hanging.
228
+ *
229
+ * Exhaustive over `OfferRejected`'s discriminated union: adding a
230
+ * new tag fails the compile here.
231
+ */
232
+ private writeOfferRejection;
206
233
  /**
207
234
  * Dispatch one inbound s2c request to the registered handler, encode
208
235
  * the response, and write it back to the server. Errors are projected
@@ -1 +1 @@
1
- {"version":3,"file":"ws-client.d.ts","sourceRoot":"","sources":["../src/ws-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ws-client.d.ts","sourceRoot":"","sources":["../src/ws-client.ts"],"names":[],"mappings":"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"}