@moltzap/client 2026.519.0 → 2026.519.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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/notification/__tests__/filter-equivalence.test.d.ts +2 -0
- package/dist/notification/__tests__/filter-equivalence.test.d.ts.map +1 -0
- package/dist/notification/__tests__/filter-equivalence.test.js +123 -0
- package/dist/notification/__tests__/filter-equivalence.test.js.map +1 -0
- package/dist/notification/__tests__/snapshot-semantics.test.d.ts +2 -0
- package/dist/notification/__tests__/snapshot-semantics.test.d.ts.map +1 -0
- package/dist/notification/__tests__/snapshot-semantics.test.js +261 -0
- package/dist/notification/__tests__/snapshot-semantics.test.js.map +1 -0
- package/dist/notification/__tests__/snapshot-semantics.types-check.d.ts +3 -0
- package/dist/notification/__tests__/snapshot-semantics.types-check.d.ts.map +1 -0
- package/dist/notification/__tests__/snapshot-semantics.types-check.js +62 -0
- package/dist/notification/__tests__/snapshot-semantics.types-check.js.map +1 -0
- package/dist/notification/errors.d.ts +27 -0
- package/dist/notification/errors.d.ts.map +1 -0
- package/dist/notification/errors.js +24 -0
- package/dist/notification/errors.js.map +1 -0
- package/dist/notification/stream.d.ts +84 -0
- package/dist/notification/stream.d.ts.map +1 -0
- package/dist/notification/stream.js +106 -0
- package/dist/notification/stream.js.map +1 -0
- package/dist/runtime/index.d.ts +3 -4
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +3 -3
- package/dist/runtime/service-teardown.d.ts +5 -0
- package/dist/runtime/service-teardown.d.ts.map +1 -0
- package/dist/runtime/service-teardown.js +36 -0
- package/dist/runtime/service-teardown.js.map +1 -0
- package/dist/runtime/service-teardown.test.d.ts +2 -0
- package/dist/runtime/service-teardown.test.d.ts.map +1 -0
- package/dist/runtime/service-teardown.test.js +80 -0
- package/dist/runtime/service-teardown.test.js.map +1 -0
- package/dist/runtime/subscribers.d.ts +74 -136
- package/dist/runtime/subscribers.d.ts.map +1 -1
- package/dist/runtime/subscribers.js +187 -93
- package/dist/runtime/subscribers.js.map +1 -1
- package/dist/service.d.ts +10 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +45 -12
- package/dist/service.js.map +1 -1
- package/dist/test-utils/conformance-adapter.d.ts +6 -16
- package/dist/test-utils/conformance-adapter.d.ts.map +1 -1
- package/dist/test-utils/conformance-adapter.js +100 -59
- package/dist/test-utils/conformance-adapter.js.map +1 -1
- package/dist/ws-client-test-support.d.ts +1 -1
- package/dist/ws-client-test-support.d.ts.map +1 -1
- package/dist/ws-client-test-support.js +24 -3
- package/dist/ws-client-test-support.js.map +1 -1
- package/dist/ws-client.d.ts +52 -54
- package/dist/ws-client.d.ts.map +1 -1
- package/dist/ws-client.js +40 -168
- package/dist/ws-client.js.map +1 -1
- package/package.json +3 -3
- package/dist/runtime/subscribers.test.d.ts +0 -2
- package/dist/runtime/subscribers.test.d.ts.map +0 -1
- package/dist/runtime/subscribers.test.js +0 -193
- package/dist/runtime/subscribers.test.js.map +0 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream-returning constructors for `MoltZapWsClient.subscribe` and
|
|
3
|
+
* `MoltZapWsClient.subscribeAll` (Spec B, #596).
|
|
4
|
+
*
|
|
5
|
+
* Architect decision **AD1 — path (a)**: trust `Stream.async` cancellation
|
|
6
|
+
* to drive registry-stored `unregister` finalizer. The registry's
|
|
7
|
+
* `dispatch` snapshots `subsRef` at iteration start, so the spec #222
|
|
8
|
+
* §5.3 OQ-3 snapshot semantic is preserved by Ref atomicity without an
|
|
9
|
+
* additional per-sub cancelled-flag check (architect plan §3).
|
|
10
|
+
*
|
|
11
|
+
* Stream construction uses `Stream.async<DecodedNotification<D>, NotConnectedError>`
|
|
12
|
+
* with the registry storing typed callback references — NOT `Queue` of
|
|
13
|
+
* `Take` items combined with `Stream.fromQueue`/`Stream.flattenTake`,
|
|
14
|
+
* which codex empirically verified is racy under
|
|
15
|
+
* `Queue.offer(Take.fail); Queue.shutdown` and does not reliably propagate
|
|
16
|
+
* the typed failure to the consumer (architect plan §3.2, codex r4).
|
|
17
|
+
*
|
|
18
|
+
* Stream lifecycle (architect-mandated, spec §"Stream lifecycle contract"):
|
|
19
|
+
* 1. `subscribe(def)` returns a Stream value. Pure; no I/O, no scope.
|
|
20
|
+
* 2. Materialization opens the `Stream.async` source: `registry.register`
|
|
21
|
+
* runs synchronously, callbacks are installed; consumer pulls suspend
|
|
22
|
+
* until `emit.single` fires from dispatch.
|
|
23
|
+
* 3. Pre-`connect()` consumer pulls suspend inside `Stream.async`'s
|
|
24
|
+
* internal queue. No `NotConnectedError` until terminal close.
|
|
25
|
+
* 4. Reconnect leaves registry callbacks intact; `subsRef` survives
|
|
26
|
+
* transient disconnects (preserved invariant).
|
|
27
|
+
* 5. `MoltZapWsClient.close` invokes `SubscriberRegistry.closeAll`, which
|
|
28
|
+
* calls each live `sub.onClose(new NotConnectedError(...))`. Each
|
|
29
|
+
* `Stream.async`-backed consumer fails with `NotConnectedError`
|
|
30
|
+
* via `emit.fail` deterministically (no Queue/shutdown race).
|
|
31
|
+
*/
|
|
32
|
+
import { Stream } from "effect";
|
|
33
|
+
import { type AnyNotificationDefinition, type DecodedNotification, type NotConnectedError, type NotificationParamsOf } from "@moltzap/protocol";
|
|
34
|
+
import type { SubscriberRegistry } from "../runtime/subscribers.js";
|
|
35
|
+
/**
|
|
36
|
+
* Typed-payload subscribe. Returns a Stream of `DecodedNotification<D>`
|
|
37
|
+
* whose error channel is `NotConnectedError` and whose requirement set is
|
|
38
|
+
* `never` (the registry handle is bound at materialization time inside
|
|
39
|
+
* `Stream.async`'s register callback, so neither Scope nor any other
|
|
40
|
+
* requirement leaks to the consumer).
|
|
41
|
+
*
|
|
42
|
+
* `refinement` is a typed predicate over the definition's params. When the
|
|
43
|
+
* type-guard overload form is used, the Stream's payload narrows to
|
|
44
|
+
* `DecodedNotification<D, R>` via the optional `R` parameter on
|
|
45
|
+
* `DecodedNotification<D>`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function subscribe<D extends AnyNotificationDefinition>(registry: SubscriberRegistry, definition: D, refinement?: (params: NotificationParamsOf<D>) => boolean): Stream.Stream<DecodedNotification<D>, NotConnectedError, never>;
|
|
48
|
+
export declare function subscribe<D extends AnyNotificationDefinition, R extends NotificationParamsOf<D>>(registry: SubscriberRegistry, definition: D, refinement: (params: NotificationParamsOf<D>) => params is R): Stream.Stream<DecodedNotification<D, R>, NotConnectedError, never>;
|
|
49
|
+
/**
|
|
50
|
+
* Broad-union escape hatch. The only intended in-tree consumer is
|
|
51
|
+
* `MoltZapService.connect`, which uses this to fan every inbound
|
|
52
|
+
* notification through `MoltZapService.handleNotification`. Payload
|
|
53
|
+
* narrowing is intentionally lost; callers wanting typed payloads use
|
|
54
|
+
* `subscribe(def, refinement?)`.
|
|
55
|
+
*
|
|
56
|
+
* The `subscribeAll` Stream uses a synthetic "match every definition"
|
|
57
|
+
* filter — implemented by the registry treating a `null` definition
|
|
58
|
+
* pointer as "match all", but for the Stream API we instead register
|
|
59
|
+
* one subscription per inbound frame's definition. Simpler: model
|
|
60
|
+
* "match all" by passing an in-band sentinel.
|
|
61
|
+
*
|
|
62
|
+
* Implementation: the registry has no native "match all" subscription
|
|
63
|
+
* shape (intentional — `subscribe<D>` is per-definition). To preserve
|
|
64
|
+
* the registry's typed dispatch surface, `subscribeAll` constructs a
|
|
65
|
+
* Stream that taps the registry via a per-arrival path: registering
|
|
66
|
+
* once with a sentinel definition would require a registry-level "match
|
|
67
|
+
* any" capability we deliberately avoid (would complicate the typed
|
|
68
|
+
* dispatch in subscribers.ts). Instead, the dispatcher feeds every
|
|
69
|
+
* frame to `subscribeAll`'s emit via a dedicated `subscribeAllRef`
|
|
70
|
+
* callback list maintained alongside `subsRef`.
|
|
71
|
+
*
|
|
72
|
+
* Architect-mandated code shape: per spec Goal #2 / plan §5.3, the
|
|
73
|
+
* surface returns a Stream value. Implementation passes the literal
|
|
74
|
+
* `AnyNotificationDefinition` sentinel via the registry's broad-union
|
|
75
|
+
* channel — see `notification/stream.ts → subscribeAllStream` below
|
|
76
|
+
* which registers via a new `SubscriberRegistry.registerAll` helper.
|
|
77
|
+
*
|
|
78
|
+
* To keep the registry minimal we route `subscribeAll` through a thin
|
|
79
|
+
* wrapper that the registry exposes as `registerAll(callbacks)` —
|
|
80
|
+
* identical lifecycle to `register(def, …)` but with no definition
|
|
81
|
+
* match (the dispatcher hits these callbacks for every inbound frame).
|
|
82
|
+
*/
|
|
83
|
+
export declare function subscribeAll(registry: SubscriberRegistry, refinement?: (notification: DecodedNotification<AnyNotificationDefinition>) => boolean): Stream.Stream<DecodedNotification<AnyNotificationDefinition>, NotConnectedError, never>;
|
|
84
|
+
//# sourceMappingURL=stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/notification/stream.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EAAU,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EACL,KAAK,yBAAyB,EAC9B,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEpE;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,yBAAyB,EAC3D,QAAQ,EAAE,kBAAkB,EAC5B,UAAU,EAAE,CAAC,EACb,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC,KAAK,OAAO,GACxD,MAAM,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,KAAK,CAAC,CAAC;AACnE,wBAAgB,SAAS,CACvB,CAAC,SAAS,yBAAyB,EACnC,CAAC,SAAS,oBAAoB,CAAC,CAAC,CAAC,EAEjC,QAAQ,EAAE,kBAAkB,EAC5B,UAAU,EAAE,CAAC,EACb,UAAU,EAAE,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC,KAAK,MAAM,IAAI,CAAC,GAC3D,MAAM,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,iBAAiB,EAAE,KAAK,CAAC,CAAC;AAkCtE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,kBAAkB,EAC5B,UAAU,CAAC,EAAE,CACX,YAAY,EAAE,mBAAmB,CAAC,yBAAyB,CAAC,KACzD,OAAO,GACX,MAAM,CAAC,MAAM,CACd,mBAAmB,CAAC,yBAAyB,CAAC,EAC9C,iBAAiB,EACjB,KAAK,CACN,CAmBA"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/* eslint-disable jsdoc/text-escaping -- JSDoc references to generic types like `Stream.async<DecodedNotification<D>>` use the natural angle-bracket form (TS source style) inside backtick-fenced code spans; the lint rule's pre-render check fires false positives on these multi-line spans. Matches the precedent in filter-equivalence.test.ts. */
|
|
2
|
+
/**
|
|
3
|
+
* Stream-returning constructors for `MoltZapWsClient.subscribe` and
|
|
4
|
+
* `MoltZapWsClient.subscribeAll` (Spec B, #596).
|
|
5
|
+
*
|
|
6
|
+
* Architect decision **AD1 — path (a)**: trust `Stream.async` cancellation
|
|
7
|
+
* to drive registry-stored `unregister` finalizer. The registry's
|
|
8
|
+
* `dispatch` snapshots `subsRef` at iteration start, so the spec #222
|
|
9
|
+
* §5.3 OQ-3 snapshot semantic is preserved by Ref atomicity without an
|
|
10
|
+
* additional per-sub cancelled-flag check (architect plan §3).
|
|
11
|
+
*
|
|
12
|
+
* Stream construction uses `Stream.async<DecodedNotification<D>, NotConnectedError>`
|
|
13
|
+
* with the registry storing typed callback references — NOT `Queue` of
|
|
14
|
+
* `Take` items combined with `Stream.fromQueue`/`Stream.flattenTake`,
|
|
15
|
+
* which codex empirically verified is racy under
|
|
16
|
+
* `Queue.offer(Take.fail); Queue.shutdown` and does not reliably propagate
|
|
17
|
+
* the typed failure to the consumer (architect plan §3.2, codex r4).
|
|
18
|
+
*
|
|
19
|
+
* Stream lifecycle (architect-mandated, spec §"Stream lifecycle contract"):
|
|
20
|
+
* 1. `subscribe(def)` returns a Stream value. Pure; no I/O, no scope.
|
|
21
|
+
* 2. Materialization opens the `Stream.async` source: `registry.register`
|
|
22
|
+
* runs synchronously, callbacks are installed; consumer pulls suspend
|
|
23
|
+
* until `emit.single` fires from dispatch.
|
|
24
|
+
* 3. Pre-`connect()` consumer pulls suspend inside `Stream.async`'s
|
|
25
|
+
* internal queue. No `NotConnectedError` until terminal close.
|
|
26
|
+
* 4. Reconnect leaves registry callbacks intact; `subsRef` survives
|
|
27
|
+
* transient disconnects (preserved invariant).
|
|
28
|
+
* 5. `MoltZapWsClient.close` invokes `SubscriberRegistry.closeAll`, which
|
|
29
|
+
* calls each live `sub.onClose(new NotConnectedError(...))`. Each
|
|
30
|
+
* `Stream.async`-backed consumer fails with `NotConnectedError`
|
|
31
|
+
* via `emit.fail` deterministically (no Queue/shutdown race).
|
|
32
|
+
*/
|
|
33
|
+
import { Effect, Stream } from "effect";
|
|
34
|
+
import {} from "@moltzap/protocol";
|
|
35
|
+
export function subscribe(registry, definition, refinement) {
|
|
36
|
+
return Stream.async((emit) => {
|
|
37
|
+
// Synchronous registration — the registry stores the typed callbacks
|
|
38
|
+
// and returns an `unregister` Effect that the Stream's runtime will
|
|
39
|
+
// invoke as the cancellation finalizer. #ignore-sloppy-code[async-keyword]: comment references `Stream.async`, not a function modifier
|
|
40
|
+
//
|
|
41
|
+
// `register` is `Effect<SubscriptionHandle, never>`; `runSync` is safe
|
|
42
|
+
// because the registry mutates an in-memory `Ref` and never yields.
|
|
43
|
+
const handle = Effect.runSync(registry.register(definition, refinement, {
|
|
44
|
+
onFrame: (frame) => Effect.sync(() => {
|
|
45
|
+
emit.single(frame);
|
|
46
|
+
}),
|
|
47
|
+
onClose: (cause) => Effect.sync(() => {
|
|
48
|
+
emit.fail(cause);
|
|
49
|
+
}),
|
|
50
|
+
}));
|
|
51
|
+
// Per P3 issue #613: use `Effect.suspend` to defer running `unregister`
|
|
52
|
+
// to whenever the Stream's runtime invokes the finalizer Effect. This
|
|
53
|
+
// is future-proof if `unregister` ever grows yielded effects (e.g.
|
|
54
|
+
// flushing a queue); the `Effect.sync(() => Effect.runSync(handle.unregister))`
|
|
55
|
+
// form would force-eager-evaluate as sync.
|
|
56
|
+
return Effect.suspend(() => handle.unregister);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Broad-union escape hatch. The only intended in-tree consumer is
|
|
61
|
+
* `MoltZapService.connect`, which uses this to fan every inbound
|
|
62
|
+
* notification through `MoltZapService.handleNotification`. Payload
|
|
63
|
+
* narrowing is intentionally lost; callers wanting typed payloads use
|
|
64
|
+
* `subscribe(def, refinement?)`.
|
|
65
|
+
*
|
|
66
|
+
* The `subscribeAll` Stream uses a synthetic "match every definition"
|
|
67
|
+
* filter — implemented by the registry treating a `null` definition
|
|
68
|
+
* pointer as "match all", but for the Stream API we instead register
|
|
69
|
+
* one subscription per inbound frame's definition. Simpler: model
|
|
70
|
+
* "match all" by passing an in-band sentinel.
|
|
71
|
+
*
|
|
72
|
+
* Implementation: the registry has no native "match all" subscription
|
|
73
|
+
* shape (intentional — `subscribe<D>` is per-definition). To preserve
|
|
74
|
+
* the registry's typed dispatch surface, `subscribeAll` constructs a
|
|
75
|
+
* Stream that taps the registry via a per-arrival path: registering
|
|
76
|
+
* once with a sentinel definition would require a registry-level "match
|
|
77
|
+
* any" capability we deliberately avoid (would complicate the typed
|
|
78
|
+
* dispatch in subscribers.ts). Instead, the dispatcher feeds every
|
|
79
|
+
* frame to `subscribeAll`'s emit via a dedicated `subscribeAllRef`
|
|
80
|
+
* callback list maintained alongside `subsRef`.
|
|
81
|
+
*
|
|
82
|
+
* Architect-mandated code shape: per spec Goal #2 / plan §5.3, the
|
|
83
|
+
* surface returns a Stream value. Implementation passes the literal
|
|
84
|
+
* `AnyNotificationDefinition` sentinel via the registry's broad-union
|
|
85
|
+
* channel — see `notification/stream.ts → subscribeAllStream` below
|
|
86
|
+
* which registers via a new `SubscriberRegistry.registerAll` helper.
|
|
87
|
+
*
|
|
88
|
+
* To keep the registry minimal we route `subscribeAll` through a thin
|
|
89
|
+
* wrapper that the registry exposes as `registerAll(callbacks)` —
|
|
90
|
+
* identical lifecycle to `register(def, …)` but with no definition
|
|
91
|
+
* match (the dispatcher hits these callbacks for every inbound frame).
|
|
92
|
+
*/
|
|
93
|
+
export function subscribeAll(registry, refinement) {
|
|
94
|
+
return Stream.async((emit) => {
|
|
95
|
+
const handle = Effect.runSync(registry.registerAll(refinement, {
|
|
96
|
+
onFrame: (frame) => Effect.sync(() => {
|
|
97
|
+
emit.single(frame);
|
|
98
|
+
}),
|
|
99
|
+
onClose: (cause) => Effect.sync(() => {
|
|
100
|
+
emit.fail(cause);
|
|
101
|
+
}),
|
|
102
|
+
}));
|
|
103
|
+
return Effect.suspend(() => handle.unregister);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/notification/stream.ts"],"names":[],"mappings":"AAAA,wVAAwV;AAExV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAKN,MAAM,mBAAmB,CAAC;AA6B3B,MAAM,UAAU,SAAS,CACvB,QAA4B,EAC5B,UAAa,EACb,UAAyD;IAEzD,OAAO,MAAM,CAAC,KAAK,CAA4C,CAAC,IAAI,EAAE,EAAE;QACtE,qEAAqE;QACrE,oEAAoE;QACpE,uIAAuI;QACvI,EAAE;QACF,uEAAuE;QACvE,oEAAoE;QACpE,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAC3B,QAAQ,CAAC,QAAQ,CAAC,UAAU,EAAE,UAAU,EAAE;YACxC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CACjB,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;gBACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC,CAAC;YACJ,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CACjB,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;gBACf,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACnB,CAAC,CAAC;SACL,CAAC,CACH,CAAC;QACF,wEAAwE;QACxE,sEAAsE;QACtE,mEAAmE;QACnE,gFAAgF;QAChF,2CAA2C;QAC3C,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,UAAU,YAAY,CAC1B,QAA4B,EAC5B,UAEY;IAMZ,OAAO,MAAM,CAAC,KAAK,CAGjB,CAAC,IAAI,EAAE,EAAE;QACT,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAC3B,QAAQ,CAAC,WAAW,CAAC,UAAU,EAAE;YAC/B,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CACjB,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;gBACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC,CAAC;YACJ,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CACjB,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;gBACf,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACnB,CAAC,CAAC;SACL,CAAC,CACH,CAAC;QACF,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file Local-service IPC primitives and runtime helpers for the client CLI.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Spec B (#596) deleted the three-field `SubscriptionFilter` re-export — the
|
|
5
|
+
* notification consumption surface is now Stream-based via
|
|
6
|
+
* `MoltZapWsClient.subscribe(def, refinement?)`.
|
|
7
7
|
*/
|
|
8
8
|
export { LocalServiceCommands } from "./local-service-commands.js";
|
|
9
9
|
export type { LocalServiceCommand } from "./local-service-commands.js";
|
|
10
|
-
export type { SubscriptionFilter } from "./subscribers.js";
|
|
11
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,YAAY,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,YAAY,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC"}
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file Local-service IPC primitives and runtime helpers for the client CLI.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Spec B (#596) deleted the three-field `SubscriptionFilter` re-export — the
|
|
5
|
+
* notification consumption surface is now Stream-based via
|
|
6
|
+
* `MoltZapWsClient.subscribe(def, refinement?)`.
|
|
7
7
|
*/
|
|
8
8
|
export { LocalServiceCommands } from "./local-service-commands.js";
|
|
9
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Effect, Scope } from "effect";
|
|
2
|
+
export declare function composeServiceTeardown(serviceScope: Scope.CloseableScope | null, client: {
|
|
3
|
+
readonly close: () => Effect.Effect<void, never>;
|
|
4
|
+
} | null): Effect.Effect<void, never>;
|
|
5
|
+
//# sourceMappingURL=service-teardown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-teardown.d.ts","sourceRoot":"","sources":["../../src/runtime/service-teardown.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAQ,KAAK,EAAE,MAAM,QAAQ,CAAC;AA8B7C,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,KAAK,CAAC,cAAc,GAAG,IAAI,EACzC,MAAM,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;CAAE,GAAG,IAAI,GAClE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAQ5B"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/* eslint-disable agent-code-guard/no-conditional-chaining -- composeServiceTeardown IS the resolution site for nullable scope+client inputs; the `closeXxxIfPresent` helpers each narrow their one nullable arg to a concrete `Effect`, so by the time `zipRight` runs every value is fully resolved. The rule fires on the parameter declarations (regarding them as "carriers"), but pushing the null-narrowing to every caller of `MoltZapService.close` would duplicate the conditional shape across the package. */
|
|
2
|
+
import { Effect, Exit, Scope } from "effect";
|
|
3
|
+
/**
|
|
4
|
+
* Build the close-order teardown Effect for `MoltZapService.close`:
|
|
5
|
+
* close the service-owned scope (which interrupts the
|
|
6
|
+
* `subscribeAll → Stream.runForEach` fan-out fiber) BEFORE invoking the
|
|
7
|
+
* ws-client's `close()`. Exposed as a top-level pure helper so the
|
|
8
|
+
* ordering can be exercised by a regression test without spinning up
|
|
9
|
+
* the full service. Codex r0 caught the original two-`runFork` race
|
|
10
|
+
* here; the regression test in `service.test.ts` pins the structural
|
|
11
|
+
* fix (P2-4 r1 cleanup).
|
|
12
|
+
*
|
|
13
|
+
* The chain is single-fork by construction (`Effect.zipRight`): the
|
|
14
|
+
* ws-client `close()` does not begin until the scope finalizer chain
|
|
15
|
+
* has completed.
|
|
16
|
+
*/
|
|
17
|
+
function closeScopeIfPresent(serviceScope) {
|
|
18
|
+
if (serviceScope === null)
|
|
19
|
+
return Effect.void;
|
|
20
|
+
return Scope.close(serviceScope, Exit.void);
|
|
21
|
+
}
|
|
22
|
+
function closeClientIfPresent(client) {
|
|
23
|
+
if (client === null)
|
|
24
|
+
return Effect.void;
|
|
25
|
+
return client.close();
|
|
26
|
+
}
|
|
27
|
+
export function composeServiceTeardown(serviceScope, client) {
|
|
28
|
+
// Resolve each optional input to a concrete Effect via the dedicated
|
|
29
|
+
// `closeXxxIfPresent` helpers, then sequence with `zipRight`. Single-fork
|
|
30
|
+
// sequencing: ws-client `close()` does not begin until the scope
|
|
31
|
+
// finalizer chain has completed.
|
|
32
|
+
const closeScope = closeScopeIfPresent(serviceScope);
|
|
33
|
+
const closeClient = closeClientIfPresent(client);
|
|
34
|
+
return closeScope.pipe(Effect.zipRight(closeClient));
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=service-teardown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-teardown.js","sourceRoot":"","sources":["../../src/runtime/service-teardown.ts"],"names":[],"mappings":"AAAA,yfAAyf;AACzf,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAE7C;;;;;;;;;;;;;GAaG;AACH,SAAS,mBAAmB,CAC1B,YAAyC;IAEzC,IAAI,YAAY,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC;IAC9C,OAAO,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;AAC9C,CAAC;AAED,SAAS,oBAAoB,CAC3B,MAAmE;IAEnE,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC;IACxC,OAAO,MAAM,CAAC,KAAK,EAAE,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,YAAyC,EACzC,MAAmE;IAEnE,qEAAqE;IACrE,0EAA0E;IAC1E,iEAAiE;IACjE,iCAAiC;IACjC,MAAM,UAAU,GAAG,mBAAmB,CAAC,YAAY,CAAC,CAAC;IACrD,MAAM,WAAW,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;IACjD,OAAO,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-teardown.test.d.ts","sourceRoot":"","sources":["../../src/runtime/service-teardown.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec B (#596) close-ordering regression test (P2-4 r1 cleanup).
|
|
3
|
+
*
|
|
4
|
+
* `MoltZapService.close` MUST close the service-owned scope (which holds
|
|
5
|
+
* the `subscribeAll → Stream.runForEach` fan-out fiber) BEFORE invoking
|
|
6
|
+
* the ws-client's `close()`. Codex r0 caught the original two-`runFork`
|
|
7
|
+
* race; the structural fix lives in `composeServiceTeardown` and these
|
|
8
|
+
* tests pin the ordering so a future refactor can't regress.
|
|
9
|
+
*
|
|
10
|
+
* `composeServiceTeardown` is a pure Effect producer: it composes the
|
|
11
|
+
* close chain but does NOT run it. The tests await the returned Effect
|
|
12
|
+
* via `yield*`, which means by the time we read `events` the chain has
|
|
13
|
+
* fully settled — no `Effect.runFork` race in the test harness either.
|
|
14
|
+
*/
|
|
15
|
+
/* eslint-disable agent-code-guard/finalizer-requires-scope -- scope is the test artifact closed explicitly via composeServiceTeardown(scope, ...); no enclosing Effect.scoped frame applies here by design */
|
|
16
|
+
import { describe, expect, it } from "vitest";
|
|
17
|
+
import { Effect, Scope } from "effect";
|
|
18
|
+
import { composeServiceTeardown } from "./service-teardown.js";
|
|
19
|
+
function makeRecorder() {
|
|
20
|
+
const events = [];
|
|
21
|
+
const fakeClient = {
|
|
22
|
+
close: () => Effect.sync(() => {
|
|
23
|
+
events.push("client");
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
return { events, fakeClient };
|
|
27
|
+
}
|
|
28
|
+
function makeScopeRecorder(events) {
|
|
29
|
+
// Build a `Scope.CloseableScope` whose finalizer pushes "scope" — mirrors
|
|
30
|
+
// the runtime shape (the fan-out fiber is installed as a finalizer on
|
|
31
|
+
// `serviceScope` via `Effect.forkIn` in `MoltZapService.connect`). The
|
|
32
|
+
// scope is the artifact under test and is closed explicitly via
|
|
33
|
+
// `composeServiceTeardown`; no enclosing `Effect.scoped` is needed.
|
|
34
|
+
return Effect.gen(function* () {
|
|
35
|
+
const scope = yield* Scope.make();
|
|
36
|
+
yield* Scope.addFinalizer(scope, Effect.sync(() => {
|
|
37
|
+
events.push("scope");
|
|
38
|
+
}));
|
|
39
|
+
return scope;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function teardownOrderingCloses() {
|
|
43
|
+
return Effect.runPromise(Effect.gen(function* () {
|
|
44
|
+
const { events, fakeClient } = makeRecorder();
|
|
45
|
+
const scope = yield* makeScopeRecorder(events);
|
|
46
|
+
yield* composeServiceTeardown(scope, fakeClient);
|
|
47
|
+
expect(events).toEqual(["scope", "client"]);
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
function teardownNullScopeRunsClient() {
|
|
51
|
+
return Effect.runPromise(Effect.gen(function* () {
|
|
52
|
+
const { events, fakeClient } = makeRecorder();
|
|
53
|
+
yield* composeServiceTeardown(null, fakeClient);
|
|
54
|
+
expect(events).toEqual(["client"]);
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
function teardownNullClientClosesScope() {
|
|
58
|
+
return Effect.runPromise(Effect.gen(function* () {
|
|
59
|
+
const events = [];
|
|
60
|
+
const scope = yield* makeScopeRecorder(events);
|
|
61
|
+
yield* composeServiceTeardown(scope, null);
|
|
62
|
+
expect(events).toEqual(["scope"]);
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
function teardownBothNullIsNoop() {
|
|
66
|
+
return Effect.runPromise(Effect.gen(function* () {
|
|
67
|
+
yield* composeServiceTeardown(null, null);
|
|
68
|
+
// The Effect completed without failing — that IS the assertion. The
|
|
69
|
+
// explicit expect anchors the test name; no value to compare beyond
|
|
70
|
+
// "no throw."
|
|
71
|
+
expect(true).toBe(true);
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
describe("composeServiceTeardown — Spec B close ordering (P2-4)", () => {
|
|
75
|
+
it("closes service-owned scope BEFORE invoking ws-client.close()", teardownOrderingCloses);
|
|
76
|
+
it("tolerates null scope (calls only client.close())", teardownNullScopeRunsClient);
|
|
77
|
+
it("tolerates null client (closes only the scope)", teardownNullClientClosesScope);
|
|
78
|
+
it("no-op when both inputs are null", teardownBothNullIsNoop);
|
|
79
|
+
});
|
|
80
|
+
//# sourceMappingURL=service-teardown.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-teardown.test.js","sourceRoot":"","sources":["../../src/runtime/service-teardown.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,166 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-subscription notification registry for `MoltZapWsClient`.
|
|
3
3
|
*
|
|
4
|
-
* Responsibility: own the list of live
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `SubscriptionId`) re-export from the package barrel.
|
|
4
|
+
* Responsibility: own the list of live subscriptions and fan each inbound
|
|
5
|
+
* JSON-RPC notification out to every subscription whose definition (and
|
|
6
|
+
* optional typed refinement predicate) matches. Implements spec #596
|
|
7
|
+
* (notification consumption consolidation) via the AD1 path-(a) Stream.async
|
|
8
|
+
* design (architect plan §3, §5.5).
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
10
|
+
* Storage shape (post-Spec B):
|
|
11
|
+
* - Subscriptions are records of typed `{onFrame, onClose}` callbacks
|
|
12
|
+
* (NOT queues / Take items / Stream.fromQueue). `notification/stream.ts`
|
|
13
|
+
* owns Stream construction via `Stream.async`; this module owns the
|
|
14
|
+
* register/dispatch/closeAll lifecycle.
|
|
15
|
+
* - The dispatch path snapshots `subsRef` at iteration start (AD1
|
|
16
|
+
* snapshot-semantic contract from spec #222 §5.3 OQ-3 / spec #596
|
|
17
|
+
* "Stream lifecycle contract" row).
|
|
18
|
+
* - `closeAll` invokes each live sub's `onClose(new NotConnectedError(...))`
|
|
19
|
+
* before clearing `subsRef` — deterministic typed-failure delivery
|
|
20
|
+
* replaces the deleted `failAllNotificationWaiters` semantic.
|
|
19
21
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `closeAll` are `Effect<T, never>`.
|
|
22
|
+
* Filter grammar (post-Spec B):
|
|
23
|
+
* - Subscription matches a frame iff `sub.definition === frame.definition`.
|
|
24
|
+
* - Optional `refinement` is a typed predicate over the frame's params
|
|
25
|
+
* (erased to the union type `ErasedNotificationRefinement` at the
|
|
26
|
+
* storage boundary; consumers receive the typed-narrowed shape via
|
|
27
|
+
* `Stream.async`'s typed `emit.single` callback inside
|
|
28
|
+
* `notification/stream.ts`).
|
|
29
|
+
* - The three-field `SubscriptionFilter` grammar is deleted (spec #596
|
|
30
|
+
* Goal #3 + §"Acceptance criteria" delete sweep). Multi-definition
|
|
31
|
+
* fan-out becomes `Stream.mergeAll([subscribe(d1), subscribe(d2)])`.
|
|
31
32
|
*/
|
|
32
33
|
import { Brand, Effect } from "effect";
|
|
33
|
-
import type
|
|
34
|
-
interface FilterableNotificationFrame {
|
|
35
|
-
readonly method: string;
|
|
36
|
-
readonly params?: unknown;
|
|
37
|
-
}
|
|
34
|
+
import { NotConnectedError, type AnyNotificationDefinition, type DecodedNotification, type NotificationParamsOf } from "@moltzap/protocol";
|
|
38
35
|
/** Branded identifier for a subscription handle. Minted by `register`. */
|
|
39
|
-
|
|
36
|
+
type SubscriptionId = string & Brand.Brand<"SubscriptionId">;
|
|
40
37
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* OQ-2 resolution (A): exactly these three fields, no free-form
|
|
46
|
-
* predicate, no schema-derived matcher. Matches the existing
|
|
47
|
-
* `RealClientEventFilter` contract at
|
|
48
|
-
* `packages/protocol/src/testing/conformance/client/runner.ts:104-116`
|
|
49
|
-
* one-for-one.
|
|
50
|
-
*
|
|
51
|
-
* - `emissionTag` — exact match against the canonical payload key
|
|
52
|
-
* `frame.params.__emissionTag`. (The adapter reads the same key at
|
|
53
|
-
* `packages/client/src/test-utils/conformance-adapter.ts:77-79`;
|
|
54
|
-
* `emitTaggedEventDefault` writes it at `runner.ts:343-357`. The
|
|
55
|
-
* `__emissionId` string in the `runner.ts:108` doc comment is a
|
|
56
|
-
* known doc-bug — architect files a follow-up issue against
|
|
57
|
-
* protocol to correct the comment; it is not the canonical name.)
|
|
58
|
-
* - `conversationId` — exact match against `frame.params.conversationId`
|
|
59
|
-
* when set on the notification payload.
|
|
60
|
-
* - `notificationNamePrefix` — `frame.method.startsWith(prefix)`.
|
|
38
|
+
* Per-subscription callback delivered each matching frame. Implemented in
|
|
39
|
+
* `notification/stream.ts` as `(frame) => Effect.sync(() => emit.single(frame))`.
|
|
40
|
+
* Returning `Effect<void, never>` keeps the dispatch path total — the
|
|
41
|
+
* Stream-side `emit.single` cannot fail synchronously.
|
|
61
42
|
*/
|
|
62
|
-
|
|
63
|
-
readonly emissionTag?: string;
|
|
64
|
-
readonly conversationId?: string;
|
|
65
|
-
readonly notificationNamePrefix?: string;
|
|
66
|
-
}
|
|
43
|
+
type SubscriberFrameCallback = (frame: DecodedNotification<AnyNotificationDefinition>) => Effect.Effect<void, never>;
|
|
67
44
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* `
|
|
71
|
-
*
|
|
72
|
-
* `unsubscribe` is `Effect<void, never>`: it is idempotent and total.
|
|
73
|
-
* Calling `unsubscribe` a second time, or after `closeAll`, is a no-op.
|
|
45
|
+
* Per-subscription callback invoked exactly once when the client transitions
|
|
46
|
+
* to its terminal closed state. Implemented in `notification/stream.ts` as
|
|
47
|
+
* `(cause) => Effect.sync(() => emit.fail(cause))`.
|
|
74
48
|
*/
|
|
75
|
-
|
|
76
|
-
readonly id: SubscriptionId;
|
|
77
|
-
readonly unsubscribe: Effect.Effect<void, never>;
|
|
78
|
-
}
|
|
49
|
+
type SubscriberCloseCallback = (cause: NotConnectedError) => Effect.Effect<void, never>;
|
|
79
50
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* The frame is a known-descriptor `DecodedNotification` (post-S9
|
|
85
|
-
* fail-close: malformed / unknown-method frames are rejected before
|
|
86
|
-
* dispatch). Handlers receive validated `params` typed as
|
|
87
|
-
* `NotificationParamsOf<D>` for the matching definition.
|
|
88
|
-
*
|
|
89
|
-
* Returning an `Effect` (not a plain `void`) lets handlers compose with
|
|
90
|
-
* Effect-native downstream code without an extra runSync shim. The
|
|
91
|
-
* registry awaits each handler's effect before moving to the next
|
|
92
|
-
* subscription for this frame — fairness over throughput, so a slow
|
|
93
|
-
* handler on subscription A does not reorder frames seen by
|
|
94
|
-
* subscription B across frames.
|
|
51
|
+
* Handle returned by `register`. `unregister` is `Effect<void, never>`: it
|
|
52
|
+
* is idempotent and total. Calling `unregister` a second time, or after
|
|
53
|
+
* `closeAll`, is a no-op.
|
|
95
54
|
*/
|
|
96
|
-
|
|
55
|
+
interface SubscriptionHandle {
|
|
56
|
+
readonly id: SubscriptionId;
|
|
57
|
+
readonly unregister: Effect.Effect<void, never>;
|
|
58
|
+
}
|
|
97
59
|
/**
|
|
98
60
|
* Subscriber registry. One instance per `MoltZapWsClient`, created at
|
|
99
|
-
* construction time and owned by the client.
|
|
100
|
-
*
|
|
101
|
-
* `
|
|
61
|
+
* construction time and owned by the client.
|
|
62
|
+
*
|
|
63
|
+
* `register<D>` accepts typed `onFrame` / `onClose` callbacks for the
|
|
64
|
+
* specific definition `D`; internally the registry erases them to the
|
|
65
|
+
* `Subscriber{Frame,Close}Callback` union shape so a single iteration
|
|
66
|
+
* can dispatch over heterogeneous subscriptions without per-`D`
|
|
67
|
+
* dispatch tables.
|
|
102
68
|
*/
|
|
103
69
|
export interface SubscriberRegistry {
|
|
70
|
+
readonly register: <D extends AnyNotificationDefinition>(definition: D, refinement: ((params: NotificationParamsOf<D>) => boolean) | undefined, callbacks: {
|
|
71
|
+
readonly onFrame: (frame: DecodedNotification<D>) => Effect.Effect<void, never>;
|
|
72
|
+
readonly onClose: SubscriberCloseCallback;
|
|
73
|
+
}) => Effect.Effect<SubscriptionHandle, never>;
|
|
104
74
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
75
|
+
* `subscribeAll` surface (spec #596 Goal #2). Registers a broad-union
|
|
76
|
+
* subscription that fires for every inbound frame regardless of
|
|
77
|
+
* definition. The optional `refinement` predicate runs against the
|
|
78
|
+
* full `DecodedNotification<AnyNotificationDefinition>` shape.
|
|
79
|
+
*
|
|
80
|
+
* Stored in a sibling `subsAllRef` list — kept off `subsRef` so the
|
|
81
|
+
* per-definition dispatch loop in `dispatch` stays a simple
|
|
82
|
+
* `definition ===` check without a per-sub broad-union escape branch.
|
|
110
83
|
*/
|
|
111
|
-
readonly
|
|
84
|
+
readonly registerAll: (refinement: ((notification: DecodedNotification<AnyNotificationDefinition>) => boolean) | undefined, callbacks: {
|
|
85
|
+
readonly onFrame: SubscriberFrameCallback;
|
|
86
|
+
readonly onClose: SubscriberCloseCallback;
|
|
87
|
+
}) => Effect.Effect<SubscriptionHandle, never>;
|
|
112
88
|
/**
|
|
113
|
-
* Fan an inbound notification out to every matching subscription. Called
|
|
114
|
-
* `MoltZapWsClient.
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* unsubscribe-during-dispatch observes next-frame semantics (OQ-3 A).
|
|
118
|
-
*
|
|
119
|
-
* Frames are pre-validation (`DecodedNotification<AnyNotificationDefinition>` union) — the
|
|
120
|
-
* type-system contract that subscribers see RAW frames, not the lifted
|
|
121
|
-
* `DecodedNotification<D>` shape that typed handlers see.
|
|
122
|
-
*
|
|
123
|
-
* Dispatch order: registration order, iterated sequentially; slow
|
|
124
|
-
* handlers block later subscriptions for this frame but never
|
|
125
|
-
* reorder frame N relative to frame N+1.
|
|
89
|
+
* Fan an inbound notification out to every matching subscription. Called
|
|
90
|
+
* from `MoltZapWsClient.handleDecodedNotification`. Snapshot semantic:
|
|
91
|
+
* unsubscribes that commit mid-dispatch observe NEXT-frame semantics
|
|
92
|
+
* (AD1 path-(a) contract).
|
|
126
93
|
*/
|
|
127
94
|
readonly dispatch: (frame: DecodedNotification<AnyNotificationDefinition>) => Effect.Effect<void, never>;
|
|
128
95
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
96
|
+
* Terminal close. Invokes every live sub's `onClose` callback (which
|
|
97
|
+
* `emit.fail(cause)`s the corresponding consumer Stream), then clears
|
|
98
|
+
* both `subsRef` and `subsAllRef`. Idempotent.
|
|
131
99
|
*/
|
|
132
100
|
readonly closeAll: Effect.Effect<void, never>;
|
|
133
101
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Construct an empty registry. Called once from the `MoltZapWsClient`
|
|
136
|
-
* constructor.
|
|
137
|
-
*
|
|
138
|
-
* Implementation notes:
|
|
139
|
-
* - Live subscriptions are stored in a `Ref<ReadonlyArray<…>>` keyed
|
|
140
|
-
* by registration order. Append-on-register, filter-on-unsubscribe
|
|
141
|
-
* keeps the dispatch path O(N) with N = live subscription count.
|
|
142
|
-
* Bigger structures aren't justified at the expected sub count
|
|
143
|
-
* (≤ ~10 per fixture).
|
|
144
|
-
* - `dispatch` snapshots the array at start (OQ-3 A). An
|
|
145
|
-
* `unsubscribe` mid-dispatch mutates the Ref but the in-flight
|
|
146
|
-
* iteration walks the snapshot.
|
|
147
|
-
* - Handler exceptions are caught with `Effect.catchAllDefect` after
|
|
148
|
-
* the handler Effect. Sync `throw` from a `(frame) => …` body
|
|
149
|
-
* surfaces as a defect; we log + swallow to match the pre-deletion
|
|
150
|
-
* `onNotification` contract.
|
|
151
|
-
*/
|
|
152
102
|
export declare function makeSubscriberRegistry(): Effect.Effect<SubscriberRegistry, never>;
|
|
153
|
-
/**
|
|
154
|
-
* Pure filter-match predicate. Exposed for unit testing so executable
|
|
155
|
-
* client-side divergence proofs can force known-bad subscription
|
|
156
|
-
* behavior without bypassing the production dispatch path.
|
|
157
|
-
*
|
|
158
|
-
* Returns `true` iff `frame` matches every set field on `filter`:
|
|
159
|
-
* - `filter.emissionTag === frame.params.__emissionTag` (strict ===)
|
|
160
|
-
* - `filter.conversationId === frame.params.conversationId` (strict ===)
|
|
161
|
-
* - `frame.method.startsWith(filter.notificationNamePrefix)`
|
|
162
|
-
* Unset filter fields are wildcards.
|
|
163
|
-
*/
|
|
164
|
-
export declare function matchesFilter(filter: SubscriptionFilter, frame: FilterableNotificationFrame): boolean;
|
|
165
103
|
export {};
|
|
166
104
|
//# sourceMappingURL=subscribers.d.ts.map
|