@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.
Files changed (60) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +4 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/notification/__tests__/filter-equivalence.test.d.ts +2 -0
  6. package/dist/notification/__tests__/filter-equivalence.test.d.ts.map +1 -0
  7. package/dist/notification/__tests__/filter-equivalence.test.js +123 -0
  8. package/dist/notification/__tests__/filter-equivalence.test.js.map +1 -0
  9. package/dist/notification/__tests__/snapshot-semantics.test.d.ts +2 -0
  10. package/dist/notification/__tests__/snapshot-semantics.test.d.ts.map +1 -0
  11. package/dist/notification/__tests__/snapshot-semantics.test.js +261 -0
  12. package/dist/notification/__tests__/snapshot-semantics.test.js.map +1 -0
  13. package/dist/notification/__tests__/snapshot-semantics.types-check.d.ts +3 -0
  14. package/dist/notification/__tests__/snapshot-semantics.types-check.d.ts.map +1 -0
  15. package/dist/notification/__tests__/snapshot-semantics.types-check.js +62 -0
  16. package/dist/notification/__tests__/snapshot-semantics.types-check.js.map +1 -0
  17. package/dist/notification/errors.d.ts +27 -0
  18. package/dist/notification/errors.d.ts.map +1 -0
  19. package/dist/notification/errors.js +24 -0
  20. package/dist/notification/errors.js.map +1 -0
  21. package/dist/notification/stream.d.ts +84 -0
  22. package/dist/notification/stream.d.ts.map +1 -0
  23. package/dist/notification/stream.js +106 -0
  24. package/dist/notification/stream.js.map +1 -0
  25. package/dist/runtime/index.d.ts +3 -4
  26. package/dist/runtime/index.d.ts.map +1 -1
  27. package/dist/runtime/index.js +3 -3
  28. package/dist/runtime/service-teardown.d.ts +5 -0
  29. package/dist/runtime/service-teardown.d.ts.map +1 -0
  30. package/dist/runtime/service-teardown.js +36 -0
  31. package/dist/runtime/service-teardown.js.map +1 -0
  32. package/dist/runtime/service-teardown.test.d.ts +2 -0
  33. package/dist/runtime/service-teardown.test.d.ts.map +1 -0
  34. package/dist/runtime/service-teardown.test.js +80 -0
  35. package/dist/runtime/service-teardown.test.js.map +1 -0
  36. package/dist/runtime/subscribers.d.ts +74 -136
  37. package/dist/runtime/subscribers.d.ts.map +1 -1
  38. package/dist/runtime/subscribers.js +187 -93
  39. package/dist/runtime/subscribers.js.map +1 -1
  40. package/dist/service.d.ts +10 -0
  41. package/dist/service.d.ts.map +1 -1
  42. package/dist/service.js +45 -12
  43. package/dist/service.js.map +1 -1
  44. package/dist/test-utils/conformance-adapter.d.ts +6 -16
  45. package/dist/test-utils/conformance-adapter.d.ts.map +1 -1
  46. package/dist/test-utils/conformance-adapter.js +100 -59
  47. package/dist/test-utils/conformance-adapter.js.map +1 -1
  48. package/dist/ws-client-test-support.d.ts +1 -1
  49. package/dist/ws-client-test-support.d.ts.map +1 -1
  50. package/dist/ws-client-test-support.js +24 -3
  51. package/dist/ws-client-test-support.js.map +1 -1
  52. package/dist/ws-client.d.ts +52 -54
  53. package/dist/ws-client.d.ts.map +1 -1
  54. package/dist/ws-client.js +40 -168
  55. package/dist/ws-client.js.map +1 -1
  56. package/package.json +3 -3
  57. package/dist/runtime/subscribers.test.d.ts +0 -2
  58. package/dist/runtime/subscribers.test.d.ts.map +0 -1
  59. package/dist/runtime/subscribers.test.js +0 -193
  60. 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"}
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * @file Local-service IPC primitives and runtime helpers for the client CLI.
3
3
  *
4
- * The public surface is intentionally narrow: command definitions for the
5
- * local daemon socket and the subscription filter type consumed by status and
6
- * conformance adapters.
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;AACvE,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,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"}
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @file Local-service IPC primitives and runtime helpers for the client CLI.
3
3
  *
4
- * The public surface is intentionally narrow: command definitions for the
5
- * local daemon socket and the subscription filter type consumed by status and
6
- * conformance adapters.
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=service-teardown.test.d.ts.map
@@ -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 `subscribe()` handles and fan each
5
- * inbound JSON-RPC notification out to every subscription whose filter matches.
6
- * Implements spec #222 §5.3 (C4 + the `RealClientEventSubscriber.subscribe`
7
- * filter stub). Lives as an internal collaborator of `MoltZapWsClient`;
8
- * the public types (`SubscriptionFilter`, `NotificationSubscription`,
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
- * Dispatch ordering (Invariant 6 — notifications delivered in arrival order):
12
- * 1. Inbound frames are handed to the registry in arrival order.
13
- * 2. Within a single frame, subscriptions are notified in registration
14
- * order.
15
- * 3. There is no separate legacy `onNotification` fanout — spec #222 OQ-4 is
16
- * resolved by DELETING `MoltZapWsClientOptions.onNotification`. Callers
17
- * that want every notification register `subscribe({}, handler)` after
18
- * construction and before `connect()`.
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
- * Unsubscribe semantics (OQ-3 A): `unsubscribe` takes effect on the next
21
- * frame. The registry snapshots its live-subscription list at the start
22
- * of each `dispatch` call; in-flight dispatch of frame N is not
23
- * interrupted by an unsubscribe during frame N. Frame N+1 observes the
24
- * unsubscribed state.
25
- *
26
- * Error channel: handlers are invoked inside a defect-catcher; a throw is
27
- * logged through Effect logging and swallowed (matching the prior
28
- * `onNotification` contract at `ws-client.ts:650-655` pre-deletion). The
29
- * registry itself has no typed error surface — `register`, `dispatch`, and
30
- * `closeAll` are `Effect&lt;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 { AnyNotificationDefinition, DecodedNotification } from "@moltzap/protocol";
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
- export type SubscriptionId = string & Brand.Brand<"SubscriptionId">;
36
+ type SubscriptionId = string & Brand.Brand<"SubscriptionId">;
40
37
  /**
41
- * Filter grammar for `subscribe`. A notification is delivered to a subscription
42
- * iff it matches **every** field that is set on the filter. Unset fields
43
- * are wildcards; the empty filter `{}` matches every notification.
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
- export interface SubscriptionFilter {
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
- * Handle returned by `register` / `MoltZapWsClient.subscribe`. Caller
69
- * holds the handle for its subscription's lifetime and runs
70
- * `unsubscribe` to stop delivery.
71
- *
72
- * `unsubscribe` is `Effect&lt;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
- export interface NotificationSubscription {
76
- readonly id: SubscriptionId;
77
- readonly unsubscribe: Effect.Effect<void, never>;
78
- }
49
+ type SubscriberCloseCallback = (cause: NotConnectedError) => Effect.Effect<void, never>;
79
50
  /**
80
- * Per-subscription handler signature. Runs inside the registry's
81
- * dispatch fiber. Must not throw — throws are caught by the registry, logged,
82
- * and swallowed.
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&lt;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
- export type SubscriberHandler = (frame: DecodedNotification<AnyNotificationDefinition>) => Effect.Effect<void, never>;
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. Not exported from the
100
- * package barrel — consumers reach the registry only through
101
- * `MoltZapWsClient.subscribe`.
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
- * Add a subscription. Returns the handle immediately; delivery starts
106
- * with the next frame passed to `dispatch`. Does not await any
107
- * connection state — subscribe is legal pre-connect (spec §5.3 +
108
- * Assumption 1 deletion: post-delete of `onNotification`, subscribe is the
109
- * only pre-connect notification hook).
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 register: (filter: SubscriptionFilter, handler: SubscriberHandler) => Effect.Effect<NotificationSubscription, never>;
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 by
114
- * `MoltZapWsClient.handleIncoming` at the existing notification-dispatch
115
- * point (`ws-client.ts:649-685`). Implementation snapshots the
116
- * live-subscription list at the start of dispatch so
117
- * unsubscribe-during-dispatch observes next-frame semantics (OQ-3 A).
118
- *
119
- * Frames are pre-validation (`DecodedNotification&lt;AnyNotificationDefinition>` union) — the
120
- * type-system contract that subscribers see RAW frames, not the lifted
121
- * `DecodedNotification&lt;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
- * Drop every live subscription. Called from `MoltZapWsClient.close`
130
- * so handlers stop firing once the client is torn down. Idempotent.
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&lt;ReadonlyArray&lt;…>>` 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