@moltzap/client 2026.508.0 → 2026.508.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 +118 -2
- package/dist/channel-core.d.ts.map +1 -1
- package/dist/channel-core.js +195 -27
- package/dist/channel-core.js.map +1 -1
- package/dist/channel-core.test.js +117 -26
- package/dist/channel-core.test.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/frame.d.ts +6 -10
- package/dist/runtime/frame.d.ts.map +1 -1
- package/dist/runtime/frame.js +4 -28
- package/dist/runtime/frame.js.map +1 -1
- package/dist/service.d.ts +18 -3
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +28 -50
- package/dist/service.js.map +1 -1
- package/dist/service.test.js +27 -25
- package/dist/service.test.js.map +1 -1
- package/dist/test-utils/channel-service-fixture.d.ts +2 -1
- package/dist/test-utils/channel-service-fixture.d.ts.map +1 -1
- package/dist/test-utils/channel-service-fixture.js +8 -0
- package/dist/test-utils/channel-service-fixture.js.map +1 -1
- package/dist/ws-client.d.ts +5 -26
- package/dist/ws-client.d.ts.map +1 -1
- package/dist/ws-client.js +71 -97
- package/dist/ws-client.js.map +1 -1
- package/dist/ws-client.test.js +40 -12
- package/dist/ws-client.test.js.map +1 -1
- package/package.json +3 -3
- package/dist/internal/__tests__/app-callback-partition-key.test.d.ts +0 -2
- package/dist/internal/__tests__/app-callback-partition-key.test.d.ts.map +0 -1
- package/dist/internal/__tests__/app-callback-partition-key.test.js +0 -20
- package/dist/internal/__tests__/app-callback-partition-key.test.js.map +0 -1
- package/dist/internal/__tests__/app-callback-partition-worker.test.d.ts +0 -2
- package/dist/internal/__tests__/app-callback-partition-worker.test.d.ts.map +0 -1
- package/dist/internal/__tests__/app-callback-partition-worker.test.js +0 -265
- package/dist/internal/__tests__/app-callback-partition-worker.test.js.map +0 -1
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.d.ts +0 -2
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.d.ts.map +0 -1
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.js +0 -228
- package/dist/internal/__tests__/app-callback-partitioned-dispatcher.test.js.map +0 -1
- package/dist/internal/__tests__/app-callback-test-requests.d.ts +0 -22
- package/dist/internal/__tests__/app-callback-test-requests.d.ts.map +0 -1
- package/dist/internal/__tests__/app-callback-test-requests.js +0 -45
- package/dist/internal/__tests__/app-callback-test-requests.js.map +0 -1
- package/dist/internal/app-callback-dispatcher-errors.d.ts +0 -50
- package/dist/internal/app-callback-dispatcher-errors.d.ts.map +0 -1
- package/dist/internal/app-callback-dispatcher-errors.js +0 -36
- package/dist/internal/app-callback-dispatcher-errors.js.map +0 -1
- package/dist/internal/app-callback-partition-key.d.ts +0 -58
- package/dist/internal/app-callback-partition-key.d.ts.map +0 -1
- package/dist/internal/app-callback-partition-key.js +0 -80
- package/dist/internal/app-callback-partition-key.js.map +0 -1
- package/dist/internal/app-callback-partition-worker.d.ts +0 -160
- package/dist/internal/app-callback-partition-worker.d.ts.map +0 -1
- package/dist/internal/app-callback-partition-worker.js +0 -141
- package/dist/internal/app-callback-partition-worker.js.map +0 -1
- package/dist/internal/app-callback-partitioned-dispatcher.d.ts +0 -180
- package/dist/internal/app-callback-partitioned-dispatcher.d.ts.map +0 -1
- package/dist/internal/app-callback-partitioned-dispatcher.js +0 -282
- package/dist/internal/app-callback-partitioned-dispatcher.js.map +0 -1
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Partitioned appCallback dispatcher.
|
|
3
|
-
*
|
|
4
|
-
* Spec: moltzap#356.
|
|
5
|
-
*
|
|
6
|
-
* Replaces the single `Stream.runForEach(handler)` over `appCallbackInboundQueue`
|
|
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(appCallbackInboundQueue))` +
|
|
41
|
-
* `runFork(Fiber.interrupt(appCallbackDispatcherFiber))` 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 "./app-callback-dispatcher-errors.js";
|
|
61
|
-
import { type PartitionKey, type PartitionableRequest } from "./app-callback-partition-key.js";
|
|
62
|
-
import { type PartitionHandler } from "./app-callback-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 partitionLimit: number;
|
|
103
|
-
readonly partitionQueueFull: number;
|
|
104
|
-
};
|
|
105
|
-
readonly partitions: ReadonlyArray<{
|
|
106
|
-
readonly key: PartitionKey;
|
|
107
|
-
readonly queueSize: number;
|
|
108
|
-
readonly idle: boolean;
|
|
109
|
-
}>;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Public surface used by `ws-client.ts`. Construction is scoped — the
|
|
113
|
-
* caller's `Scope` owns every partition fiber.
|
|
114
|
-
*/
|
|
115
|
-
export interface PartitionedDispatcher {
|
|
116
|
-
/**
|
|
117
|
-
* Route one inbound request. Non-blocking. Failure modes are
|
|
118
|
-
* exhaustive in `OfferRejected`; the reader fiber `Effect.match`es
|
|
119
|
-
* the tag set to pick a wire-error code.
|
|
120
|
-
*
|
|
121
|
-
* Per-tuple ordering: two `offer` calls with the same `PartitionKey`
|
|
122
|
-
* land on the same `PartitionWorker.offer` (FIFO).
|
|
123
|
-
* Cross-tuple concurrency: two `offer` calls with different keys
|
|
124
|
-
* produce no ordering relation.
|
|
125
|
-
*/
|
|
126
|
-
readonly offer: (request: PartitionableRequest) => Effect.Effect<void, OfferRejected>;
|
|
127
|
-
/**
|
|
128
|
-
* Best-effort stats snapshot. Not used in the hot path; called by
|
|
129
|
-
* tests + the optional `client.stats()` debug surface.
|
|
130
|
-
*/
|
|
131
|
-
readonly stats: Effect.Effect<DispatcherStats>;
|
|
132
|
-
/**
|
|
133
|
-
* Explicit shutdown. Equivalent to `Scope.close(dispatcherScope,
|
|
134
|
-
* Exit.void)`; exists as a typed handle so callsites that already
|
|
135
|
-
* hold the dispatcher (not the Scope) can tear it down without
|
|
136
|
-
* threading the Scope through `ConnState`. Idempotent.
|
|
137
|
-
*
|
|
138
|
-
* Callsites that invoke this from a `runSync` context (`close()`,
|
|
139
|
-
* `disconnectSync()`) MUST wrap with `runtime.runFork`. The Effect
|
|
140
|
-
* itself yields through the runtime (queue shutdown + fiber
|
|
141
|
-
* interrupt are non-synchronous); calling `runSync` on it would throw
|
|
142
|
-
* `AsyncFiberException`. See the regression gate at
|
|
143
|
-
* `packages/client/src/ws-client.test.ts:1233-1259@f0df363`.
|
|
144
|
-
*/
|
|
145
|
-
readonly shutdown: Effect.Effect<void>;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Construction params. `handle` is the per-request work — passed in so
|
|
149
|
-
* the dispatcher has no direct dependency on the handler-registry
|
|
150
|
-
* `Ref<HashMap<string, ServerRpcHandler>>` (`appCallbackHandlersRef`); the
|
|
151
|
-
* caller (`ws-client.ts`) closes over the registry and supplies a
|
|
152
|
-
* pre-bound function. This preserves the wire-edge schema-decoding
|
|
153
|
-
* boundary already established in `dispatchInboundServerRequest`.
|
|
154
|
-
*
|
|
155
|
-
* `scope` is the `Scope.CloseableScope` that owns every per-partition
|
|
156
|
-
* worker fiber + the idle reaper. The caller (`ws-client.ts`)
|
|
157
|
-
* allocates it via `Scope.make()` outside the per-connect socket
|
|
158
|
-
* Scope so `runSync(client.close())` can `runFork(Scope.close(scope,
|
|
159
|
-
* Exit.void))` without yielding through the runtime.
|
|
160
|
-
*/
|
|
161
|
-
export interface MakePartitionedDispatcherParams {
|
|
162
|
-
readonly handle: PartitionHandler;
|
|
163
|
-
readonly scope: Scope.CloseableScope;
|
|
164
|
-
readonly config?: Partial<PartitionedDispatcherConfig>;
|
|
165
|
-
readonly logger?: WsClientLogger;
|
|
166
|
-
/** Monotonic clock for tests. Defaults to `Date.now`. */
|
|
167
|
-
readonly clock?: () => number;
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Construct a partitioned dispatcher. The caller-provided
|
|
171
|
-
* `params.scope` owns every per-partition fiber + the idle-reaper
|
|
172
|
-
* fiber; closing it cascades teardown via `Scope` finalizers.
|
|
173
|
-
*
|
|
174
|
-
* Returns `Effect<…, never>` — neither construction nor partition
|
|
175
|
-
* allocation can fail. The Effect itself does not require a `Scope`
|
|
176
|
-
* because every scoped allocation is provided to `params.scope`
|
|
177
|
-
* explicitly via `Scope.extend`.
|
|
178
|
-
*/
|
|
179
|
-
export declare function makePartitionedDispatcher(params: MakePartitionedDispatcherParams): Effect.Effect<PartitionedDispatcher, never>;
|
|
180
|
-
//# sourceMappingURL=app-callback-partitioned-dispatcher.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"app-callback-partitioned-dispatcher.d.ts","sourceRoot":"","sources":["../../src/internal/app-callback-partitioned-dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,OAAO,EAGL,MAAM,EAQN,KAAK,EACN,MAAM,QAAQ,CAAC;AAEhB,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,oBAAoB,EAC1B,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAEL,KAAK,gBAAgB,EAEtB,MAAM,oCAAoC,CAAC;AAC5C,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,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;AAoCD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,+BAA+B,GACtC,MAAM,CAAC,MAAM,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAgH7C"}
|
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Partitioned appCallback dispatcher.
|
|
3
|
-
*
|
|
4
|
-
* Spec: moltzap#356.
|
|
5
|
-
*
|
|
6
|
-
* Replaces the single `Stream.runForEach(handler)` over `appCallbackInboundQueue`
|
|
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(appCallbackInboundQueue))` +
|
|
41
|
-
* `runFork(Fiber.interrupt(appCallbackDispatcherFiber))` 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, ExecutionStrategy, Exit, HashMap, MutableRef, Option, Ref, Schedule, Scope, } from "effect";
|
|
60
|
-
import { PartitionLimitError, } from "./app-callback-dispatcher-errors.js";
|
|
61
|
-
import { extractPartitionKey, } from "./app-callback-partition-key.js";
|
|
62
|
-
import { makePartitionWorker, } from "./app-callback-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
|
-
partitionLimit: yield* Ref.make(0),
|
|
91
|
-
partitionQueueFull: yield* Ref.make(0),
|
|
92
|
-
};
|
|
93
|
-
const dispatcherScope = params.scope;
|
|
94
|
-
const internal = {
|
|
95
|
-
partitionsRef,
|
|
96
|
-
dispatcherScope,
|
|
97
|
-
config,
|
|
98
|
-
handle: params.handle,
|
|
99
|
-
...(params.logger !== undefined ? { logger: params.logger } : {}),
|
|
100
|
-
clock,
|
|
101
|
-
counters,
|
|
102
|
-
};
|
|
103
|
-
// Idle-reaper fiber. One fiber per dispatcher (NOT per partition);
|
|
104
|
-
// walks the map every `idleReaperIntervalMs` and finalizes
|
|
105
|
-
// partitions whose `idleSince._tag === "Idle"` exceeds the TTL.
|
|
106
|
-
const reaperEffect = reapIdlePartitions(internal).pipe(Effect.repeat(Schedule.spaced(Duration.millis(config.idleReaperIntervalMs))), Effect.asVoid, Effect.catchAllCause((cause) => Effect.sync(() => params.logger?.warn("appCallback idle reaper exited", Cause.pretty(cause)))));
|
|
107
|
-
yield* Scope.extend(Effect.forkScoped(reaperEffect), dispatcherScope);
|
|
108
|
-
const offer = (request) => Effect.gen(function* () {
|
|
109
|
-
yield* Ref.update(counters.totalOffered, (n) => n + 1);
|
|
110
|
-
const key = extractPartitionKey(request);
|
|
111
|
-
const worker = yield* getOrCreatePartitionWorker({
|
|
112
|
-
key,
|
|
113
|
-
requestId: request.id,
|
|
114
|
-
state: internal,
|
|
115
|
-
}).pipe(Effect.tapError((err) => err instanceof PartitionLimitError
|
|
116
|
-
? Ref.update(counters.partitionLimit, (n) => n + 1)
|
|
117
|
-
: Effect.void));
|
|
118
|
-
yield* worker
|
|
119
|
-
.offer(request)
|
|
120
|
-
.pipe(Effect.tapError(() => Ref.update(counters.partitionQueueFull, (n) => n + 1)));
|
|
121
|
-
});
|
|
122
|
-
const stats = Effect.gen(function* () {
|
|
123
|
-
const map = yield* Ref.get(partitionsRef);
|
|
124
|
-
const partitions = [];
|
|
125
|
-
for (const [key, worker] of HashMap.entries(map)) {
|
|
126
|
-
const queueSize = yield* worker.queueSize;
|
|
127
|
-
const idle = MutableRef.get(worker.idleSince)._tag === "Idle";
|
|
128
|
-
partitions.push({ key, queueSize, idle });
|
|
129
|
-
}
|
|
130
|
-
const totalOffered = yield* Ref.get(counters.totalOffered);
|
|
131
|
-
const partitionLimit = yield* Ref.get(counters.partitionLimit);
|
|
132
|
-
const partitionQueueFull = yield* Ref.get(counters.partitionQueueFull);
|
|
133
|
-
return {
|
|
134
|
-
activePartitions: HashMap.size(map),
|
|
135
|
-
totalOffered,
|
|
136
|
-
totalRejected: {
|
|
137
|
-
partitionLimit,
|
|
138
|
-
partitionQueueFull,
|
|
139
|
-
},
|
|
140
|
-
partitions,
|
|
141
|
-
};
|
|
142
|
-
});
|
|
143
|
-
const shutdown = Scope.close(dispatcherScope, Exit.void).pipe(Effect.catchAllCause(() => Effect.void));
|
|
144
|
-
return {
|
|
145
|
-
offer,
|
|
146
|
-
stats,
|
|
147
|
-
shutdown,
|
|
148
|
-
};
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
// ─── Internal helpers ────────────────────────────────────────────────
|
|
152
|
-
/**
|
|
153
|
-
* Allocate a worker for `key` if absent and return it, or return the
|
|
154
|
-
* existing worker. Returns `PartitionLimitError` if creation would
|
|
155
|
-
* exceed `maxPartitions` AND no idle partition is reclaimable.
|
|
156
|
-
*
|
|
157
|
-
* NOT part of the public surface — the only public entry is `offer`.
|
|
158
|
-
*
|
|
159
|
-
* Race-safety: under concurrent offers for the same new key, exactly
|
|
160
|
-
* one allocation wins. The losing offer's worker is interrupted and
|
|
161
|
-
* the winner is returned. Since worker construction is `Effect`-shaped,
|
|
162
|
-
* we cannot put it inside `Ref.modify`'s synchronous lambda; instead
|
|
163
|
-
* we build outside, then atomic-insert with a check for prior winners
|
|
164
|
-
* inside `modify`. Worst-case overhead under contention: one extra
|
|
165
|
-
* worker built and immediately interrupted. Bounded by partition cap.
|
|
166
|
-
*/
|
|
167
|
-
function getOrCreatePartitionWorker(params) {
|
|
168
|
-
return Effect.gen(function* () {
|
|
169
|
-
const { key, requestId, state } = params;
|
|
170
|
-
const existing = yield* Ref.get(state.partitionsRef).pipe(Effect.map((m) => HashMap.get(m, key)));
|
|
171
|
-
if (Option.isSome(existing)) {
|
|
172
|
-
return existing.value;
|
|
173
|
-
}
|
|
174
|
-
// Soft cap check is best-effort: under concurrent allocation we
|
|
175
|
-
// may temporarily exceed by 1-2 before the next reaper tick. The
|
|
176
|
-
// cap exists as a DoS guard, not an exact-cardinality invariant.
|
|
177
|
-
const sizeBeforeAlloc = yield* Ref.get(state.partitionsRef).pipe(Effect.map((m) => HashMap.size(m)));
|
|
178
|
-
if (sizeBeforeAlloc >= state.config.maxPartitions) {
|
|
179
|
-
return yield* Effect.fail(new PartitionLimitError({
|
|
180
|
-
key,
|
|
181
|
-
activePartitions: sizeBeforeAlloc,
|
|
182
|
-
maxPartitions: state.config.maxPartitions,
|
|
183
|
-
requestId,
|
|
184
|
-
}));
|
|
185
|
-
}
|
|
186
|
-
// Fork a per-worker child scope from the dispatcher scope. Every
|
|
187
|
-
// finalizer the worker registers (queue.shutdown, drain fiber)
|
|
188
|
-
// binds to this child scope, so the reaper can close it
|
|
189
|
-
// independently to retire one worker without orphaning finalizers
|
|
190
|
-
// on the dispatcher scope. Closing the dispatcher scope still
|
|
191
|
-
// cascades through every child scope via the fork link.
|
|
192
|
-
const workerScope = yield* Scope.fork(state.dispatcherScope, ExecutionStrategy.sequential);
|
|
193
|
-
const built = yield* makePartitionWorker({
|
|
194
|
-
key,
|
|
195
|
-
capacity: state.config.partitionQueueCapacity,
|
|
196
|
-
handle: state.handle,
|
|
197
|
-
scope: workerScope,
|
|
198
|
-
...(state.logger !== undefined ? { logger: state.logger } : {}),
|
|
199
|
-
clock: state.clock,
|
|
200
|
-
});
|
|
201
|
-
// Insert (or recover the racer's winner). Atomic.
|
|
202
|
-
const inserted = yield* Ref.modify(state.partitionsRef, (m) => {
|
|
203
|
-
const present = HashMap.get(m, key);
|
|
204
|
-
if (Option.isSome(present)) {
|
|
205
|
-
return [{ kind: "lost-race", winner: present.value }, m];
|
|
206
|
-
}
|
|
207
|
-
return [{ kind: "won", winner: built }, HashMap.set(m, key, built)];
|
|
208
|
-
});
|
|
209
|
-
if (inserted.kind === "lost-race") {
|
|
210
|
-
// Discard the redundant worker. Closing its scope cascades to
|
|
211
|
-
// queue.shutdown + the drain fiber's interrupt, releasing every
|
|
212
|
-
// resource we just allocated.
|
|
213
|
-
state.logger?.warn(`appCallback partition allocation lost race (key=${key}); discarding redundant worker`);
|
|
214
|
-
yield* Scope.close(workerScope, Exit.void).pipe(Effect.catchAllCause(() => Effect.void));
|
|
215
|
-
}
|
|
216
|
-
return inserted.winner;
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* One reaper tick. Walks the partition map, claiming any worker whose
|
|
221
|
-
* `idleSince._tag === "Idle"` AND whose age exceeds
|
|
222
|
-
* `idlePartitionTtlMs`, then closes each claimed worker's per-worker
|
|
223
|
-
* scope so its queue + drain fiber are torn down via Scope finalizers
|
|
224
|
-
* (no orphan finalizers on the dispatcher scope).
|
|
225
|
-
*
|
|
226
|
-
* Race-safety vs. concurrent offers (single-producer reader fiber):
|
|
227
|
-
*
|
|
228
|
-
* - The claim decision lives inside `Ref.modify(partitionsRef, …)`.
|
|
229
|
-
* The lambda is synchronous; idle reads go through
|
|
230
|
-
* `MutableRef.get` and the claim itself is a synchronous
|
|
231
|
-
* `MutableRef.set(worker.idleSince, Retiring)` inside the same
|
|
232
|
-
* lambda. From the producer's perspective the partition-map
|
|
233
|
-
* removal AND the `Retiring` transition happen as one atomic
|
|
234
|
-
* step.
|
|
235
|
-
* - The producer's `worker.offer` is the counter-CAS: it does
|
|
236
|
-
* `MutableRef.updateAndGet(idleSince, current → Retiring ? current
|
|
237
|
-
* : Busy)` and refuses to enqueue if it observes `Retiring`. Two
|
|
238
|
-
* interleavings are possible and both are safe:
|
|
239
|
-
* - Producer wins first: `idleSince` flips Idle→Busy. Reaper
|
|
240
|
-
* observes `Busy` (≠ Idle); skips. Producer's `Queue.offer`
|
|
241
|
-
* lands; drain processes it.
|
|
242
|
-
* - Reaper wins first: `idleSince` flips Idle→Retiring + map
|
|
243
|
-
* removal. Producer's `MutableRef.updateAndGet` returns
|
|
244
|
-
* `Retiring`; producer fails with
|
|
245
|
-
* `PartitionQueueFullError`. Reader-fiber writes a wire
|
|
246
|
-
* error response. Server's `Deferred.await` resolves on the
|
|
247
|
-
* error. No hang.
|
|
248
|
-
* - There is no third interleaving: the JS event loop cannot
|
|
249
|
-
* interleave inside a synchronous `Ref.modify` lambda or inside
|
|
250
|
-
* a synchronous `MutableRef.updateAndGet` call.
|
|
251
|
-
*/
|
|
252
|
-
function reapIdlePartitions(state) {
|
|
253
|
-
return Effect.gen(function* () {
|
|
254
|
-
const now = state.clock();
|
|
255
|
-
const ttl = state.config.idlePartitionTtlMs;
|
|
256
|
-
// Atomic claim + remove. The lambda is synchronous; for each
|
|
257
|
-
// worker we decide to retire we (a) flip `idleSince` to
|
|
258
|
-
// `Retiring` so any concurrent `worker.offer` refuses to enqueue,
|
|
259
|
-
// and (b) drop the entry from the map. The `Ref.modify` commit
|
|
260
|
-
// makes both visible atomically.
|
|
261
|
-
const reaped = yield* Ref.modify(state.partitionsRef, (current) => {
|
|
262
|
-
const removed = [];
|
|
263
|
-
let next = current;
|
|
264
|
-
for (const [key, worker] of HashMap.entries(current)) {
|
|
265
|
-
const idle = MutableRef.get(worker.idleSince);
|
|
266
|
-
if (idle._tag === "Idle" && now - idle.sinceMs >= ttl) {
|
|
267
|
-
MutableRef.set(worker.idleSince, { _tag: "Retiring" });
|
|
268
|
-
removed.push(worker);
|
|
269
|
-
next = HashMap.remove(next, key);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return [removed, next];
|
|
273
|
-
});
|
|
274
|
-
// Close per-worker scopes outside the `Ref.modify` (Scope.close
|
|
275
|
-
// yields). Cascading finalizers: queue.shutdown → Stream exits →
|
|
276
|
-
// drain fiber returns. No leak on the dispatcher scope.
|
|
277
|
-
for (const worker of reaped) {
|
|
278
|
-
yield* Scope.close(worker.scope, Exit.void).pipe(Effect.catchAllCause(() => Effect.void));
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
//# sourceMappingURL=app-callback-partitioned-dispatcher.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"app-callback-partitioned-dispatcher.js","sourceRoot":"","sources":["../../src/internal/app-callback-partitioned-dispatcher.ts"],"names":[],"mappings":"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"}
|