@macula-io/ts 0.13.5 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -235,6 +235,16 @@ fixed) is in [CHANGELOG.md](CHANGELOG.md).
235
235
  is why `keepAdvertisedDirect()` is a standalone function rather than a
236
236
  `Session` method.
237
237
 
238
+ - **Pool** — `Pool.connect(seeds, controlIdentity, opts)` holds live
239
+ connections to every configured seed concurrently (not
240
+ dial-one-then-fallback-on-failure), each independently monitored and
241
+ respawned with backoff on disconnect; `publish()`/`call()` fan out
242
+ over live links, `subscribe()` re-establishes automatically on
243
+ reconnect. Ports `macula/src/client/macula_client.erl`'s pool design;
244
+ see `pool.ts`'s own module doc for why it's a set of role-scoped
245
+ `Session`s per seed rather than one, given the single-reader
246
+ constraint below.
247
+
238
248
  Every item above is live-verified against the real production fleet
239
249
  (`station-de-frankfurt.macula.io`), including negative/error paths and,
240
250
  where applicable, the actual packaged npm tarball rather than only the
@@ -257,21 +267,23 @@ arbitrary payload" function (see above for why), a `station_endpoint`
257
267
  record builder (macula-go has none either — stations publish those
258
268
  themselves, not clients), and `Pinned`/`Insecure` trust modes (`WebPKI`
259
269
  only so far). Multiple concurrent `subscribe()` topics on one `Session`
260
- also isn't supported yet one `subscribe()` (like one `serve()`) per
261
- `Session` at a time; open a second `Session` for a second topic. Each of
262
- these is a separate, later slice of work built on top of a working
263
- `Session`.
270
+ still isn't supported at the `Session` level itself — one `subscribe()`
271
+ (like one `serve()`) per `Session` at a time; open a second `Session`
272
+ for a second topic (`Pool`, above, does exactly this internally to give
273
+ each tracked topic its own session). Each of these is a separate, later
274
+ slice of work built on top of a working `Session`.
264
275
 
265
276
  ## Testing
266
277
 
267
278
  ```bash
268
279
  npx vitest run # default suite, no network
269
- npm run test:live # MACULA_TS_LIVE=1 vitest run src/session.live.test.ts src/rpc.live.test.ts src/dht.live.test.ts src/pubsub.live.test.ts src/content.live.test.ts src/ucan.live.test.ts src/directdial.live.test.ts
280
+ npm run test:live # MACULA_TS_LIVE=1 vitest run src/session.live.test.ts src/rpc.live.test.ts src/dht.live.test.ts src/pubsub.live.test.ts src/content.live.test.ts src/ucan.live.test.ts src/directdial.live.test.ts src/pool.live.test.ts
270
281
  ```
271
282
 
272
283
  `src/session.live.test.ts`, `src/rpc.live.test.ts`, `src/dht.live.test.ts`,
273
284
  `src/pubsub.live.test.ts`, `src/content.live.test.ts`,
274
- `src/ucan.live.test.ts`, and `src/directdial.live.test.ts` hit the real
285
+ `src/ucan.live.test.ts`, `src/directdial.live.test.ts`, and
286
+ `src/pool.live.test.ts` hit the real
275
287
  production fleet and are **not** part of default `npm test`/CI — opt in
276
288
  explicitly, gated behind `MACULA_TS_LIVE`. Same convention as macula-go's
277
289
  `live` build tag, macula-rust's `#[ignore]`, and macula-dotnet's
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { Identity } from "./identity.js";
2
2
  export { Session, type CallOptions } from "./session.js";
3
+ export { Pool, NoHealthyStationError, type Seed, type PoolOptions, type PoolStatus } from "./pool.js";
3
4
  export { MaculaCallError, type JsonValue, type Bolt4ErrorInfo } from "./rpc.js";
4
5
  export { DhtRecordType, DHT_DEFAULT_TTL_MS, type DhtRecord } from "./dht.js";
5
6
  export type { PublishOptions, PubsubEvent } from "./pubsub.js";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { Identity } from "./identity.js";
2
2
  export { Session } from "./session.js";
3
+ export { Pool, NoHealthyStationError } from "./pool.js";
3
4
  export { MaculaCallError } from "./rpc.js";
4
5
  export { DhtRecordType, DHT_DEFAULT_TTL_MS } from "./dht.js";
5
6
  export { ContentNotFoundError } from "./content.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,OAAO,EAAoB,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,eAAe,EAAuC,MAAM,UAAU,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAkB,MAAM,UAAU,CAAC;AAE7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,IAAI,EAAiE,MAAM,WAAW,CAAC;AAChG,OAAO,EACL,oBAAoB,EACpB,kCAAkC,GAInC,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,OAAO,EAAoB,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,qBAAqB,EAAgD,MAAM,WAAW,CAAC;AACtG,OAAO,EAAE,eAAe,EAAuC,MAAM,UAAU,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAkB,MAAM,UAAU,CAAC;AAE7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,IAAI,EAAiE,MAAM,WAAW,CAAC;AAChG,OAAO,EACL,oBAAoB,EACpB,kCAAkC,GAInC,MAAM,iBAAiB,CAAC"}
package/dist/pool.d.ts ADDED
@@ -0,0 +1,123 @@
1
+ import { Identity } from "./identity.js";
2
+ import type { PubsubEvent } from "./pubsub.js";
3
+ import { type JsonValue } from "./rpc.js";
4
+ /** One configured connection target. */
5
+ export interface Seed {
6
+ host: string;
7
+ port: number;
8
+ }
9
+ export interface PoolOptions {
10
+ /** How many currently-live control links a publish() fans out to
11
+ * before resolving -- partial success counts as success, matching
12
+ * macula_client:publish/5's own documented semantics. Default 1.
13
+ * NOT YET FULLY AT PARITY above 1: the Erlang reference stamps ONE
14
+ * seq across every replica specifically so a receiver's own
15
+ * (publisher, seq, topic) dedup collapses the redundant copies into
16
+ * one event. This SDK's Session.publish() has no caller-supplied-seq
17
+ * parameter to do the same (each call mints its own fresh seq), so
18
+ * setting this above 1 would deliver N distinct, non-deduplicable
19
+ * copies to every subscriber -- clamped to 1 until that's fixed. */
20
+ replicationFactor?: number;
21
+ /** How long a recorded (realm, publisher, seq, topic) key is treated
22
+ * as a duplicate before it ages out. Default 60_000, matching the
23
+ * Erlang reference's own `dedup_window_ms` default. */
24
+ dedupWindowMs?: number;
25
+ /** How often the dedup table is swept for expired entries. Default
26
+ * 30_000, matching the Erlang reference's own `dedup_sweep_ms`. */
27
+ dedupSweepMs?: number;
28
+ /** How often a live control link is health-checked (see
29
+ * #armHealthCheck's own doc for why publish() alone can't be trusted
30
+ * to ever notice a dead connection). Default 10_000. */
31
+ healthCheckIntervalMs?: number;
32
+ }
33
+ export interface PoolStatus {
34
+ /** Seeds whose control link is currently connected. */
35
+ healthyLinks: number;
36
+ /** Seeds whose control link is not currently connected (connecting
37
+ * or backing off). Every configured seed is exactly one or the
38
+ * other -- a link backs off and retries forever, it is never given
39
+ * up on and dropped from the pool, same as the Erlang reference. */
40
+ failedLinks: number;
41
+ }
42
+ /** Thrown by publish()/call() when the pool has zero live control links
43
+ * to try -- a distinct, identifiable condition from any one link's own
44
+ * transient error, matching macula_client:publish/5's own `{error,
45
+ * {transient, no_healthy_station}}`. */
46
+ export declare class NoHealthyStationError extends Error {
47
+ constructor();
48
+ }
49
+ /**
50
+ * A resilient multi-station client: live connections to every configured
51
+ * seed held concurrently, each independently monitored and respawned
52
+ * with backoff on disconnect, every tracked subscription re-established
53
+ * automatically when its own link reconnects. See this module's own
54
+ * header doc for the full design, why a "link" is a small role-scoped
55
+ * session set rather than one Session, and its one deliberate deviation
56
+ * from the Erlang reference (topic-scoped dedup).
57
+ */
58
+ export declare class Pool {
59
+ #private;
60
+ private constructor();
61
+ /** Dials the control role against every seed concurrently under
62
+ * `controlIdentity` (the caller's own identity -- publish()/call()
63
+ * are attributed to it on the wire) and resolves once every seed's
64
+ * FIRST connect attempt has settled (success or a logged failure
65
+ * headed into backoff) -- matches tapRoom()'s own "await the first
66
+ * attempt, not the eventual outcome" reasoning (macula-mcp's
67
+ * lobby_observer.ts): a caller that publishes/calls immediately
68
+ * after connect() must not race links still mid-handshake. */
69
+ static connect(seeds: Seed[], controlIdentity: Identity, opts?: PoolOptions): Promise<Pool>;
70
+ /** Publishes to `replicationFactor` currently-live control links
71
+ * (default 1); partial success counts as success. A link whose
72
+ * publish attempt fails is marked for respawn immediately -- this is
73
+ * this v1's only liveness signal for the control role, since it
74
+ * cannot also carry a liveness-only subscribe (see this module's own
75
+ * header doc). Throws NoHealthyStationError if zero links are live. */
76
+ publish(realm: string | undefined, topic: string, payload: JsonValue, opts?: {
77
+ ttlMs?: number;
78
+ }): Promise<void>;
79
+ /** Calls `procedure` against the pool's live control links in order
80
+ * until one succeeds or all have been tried. Throws
81
+ * NoHealthyStationError if zero links are live.
82
+ *
83
+ * A `MaculaCallError` (a real BOLT#4 response -- e.g.
84
+ * unknown_next_peer, unauthorized) does NOT mark its link for
85
+ * respawn: the connection plainly worked, it answered. Found in
86
+ * review 2026-09-05: treating every thrown error as evidence of a
87
+ * dead connection meant a single genuinely-answered error (a
88
+ * procedure nobody serves, a bad realm, a gated call this identity
89
+ * isn't authorized for) tore down and reconnected EVERY live control
90
+ * link in turn as call() moved through them re-trying the same
91
+ * doomed call -- and for a non-idempotent provider handler, actually
92
+ * re-invoked it on each one. Only an error that never got a
93
+ * wire-level answer at all (the same distinction call()'s own doc on
94
+ * Session draws) is treated as a dead link. Each link's own `session`
95
+ * reference is re-checked before scheduling a reconnect, in case a
96
+ * concurrent operation already superseded it between this loop
97
+ * starting and this iteration running. */
98
+ call(realm: string | undefined, procedure: string, payload: JsonValue, opts?: {
99
+ deadlineMs?: number;
100
+ }): Promise<JsonValue>;
101
+ /** Subscribes `handler` to `(realm, topic)`: opens one subscribe-only
102
+ * session against every configured seed, replayed automatically on
103
+ * every future respawn. Mints a dedicated identity for this topic by
104
+ * default (disposed on unsubscribe); pass `identity` to supply the
105
+ * pool's own instead (e.g. for a stable, caller-controlled identity
106
+ * across restarts, matching macula-mcp's own observeRoomIdentityPath
107
+ * pattern) -- the pool never disposes an identity it didn't mint.
108
+ * Returns an unsubscribe function. */
109
+ subscribe(realm: string | undefined, topic: string, handler: (evt: PubsubEvent) => void, identity?: Identity): Promise<() => Promise<void>>;
110
+ /** Live/backing-off CONTROL link counts (publish/call reachability).
111
+ * Every configured seed is exactly one or the other. Per-topic
112
+ * subscription link health is not reflected here -- inspect a
113
+ * specific subscription's own behavior (events arriving or not)
114
+ * instead; exposing N independent per-topic health vectors here
115
+ * would not simplify what a caller actually needs to know. */
116
+ status(): PoolStatus;
117
+ /** Closes every control and subscription link and disposes every
118
+ * identity this pool owns (the caller-supplied control identity
119
+ * included). Awaits each link's in-flight connect/reconnect first,
120
+ * so a still-connecting link never has its identity yanked out from
121
+ * under it. */
122
+ close(): Promise<void>;
123
+ }
package/dist/pool.js ADDED
@@ -0,0 +1,538 @@
1
+ // Resilient multi-station client -- ports macula/src/client/macula_client.erl's
2
+ // pool design (N live links, monitor + respawn-with-backoff, subscription
3
+ // replay on reconnect) into this SDK. `Session` (session.ts) and
4
+ // `connectWithFallback`/`withSession` (this SDK's consumers, e.g.
5
+ // macula-mcp's macula_ts_client.ts) are both dial-one-then-use primitives --
6
+ // correct for a one-shot operation, wrong for anything that needs to stay
7
+ // reachable across a station's own outage. A Pool holds live connections to
8
+ // every configured seed concurrently, not one-then-fallback-on-first-failure;
9
+ // a seed that never connects and a link whose connection later dies are the
10
+ // same condition (backoff, retry forever) rather than two different code
11
+ // paths.
12
+ //
13
+ // == Why this isn't "one Session per seed" ==
14
+ //
15
+ // Erlang's macula_client can put N tracked (Realm, Topic) subscriptions
16
+ // (plus Call, plus Advertise) on ONE gen_server-owned link, because a BEAM
17
+ // mailbox demuxes every message type for that one process regardless of how
18
+ // many concerns it's juggling. This SDK's Session has no equivalent: its
19
+ // underlying control stream supports exactly one concurrent reader --
20
+ // `subscribe()` throws outright if called twice on the same Session
21
+ // ("a second concurrent subscribe() on the same Session races; open a
22
+ // second Session instead"), and `call()`/`serve()` are mutually exclusive
23
+ // with an active subscribe on the same Session for the identical reason
24
+ // (macula-go's FrameStream does a raw sequential read into a shared buffer,
25
+ // no per-caller demux). Confirmed live 2026-09-04 while porting the Go side
26
+ // of this same design: this is a wire-level constraint, not a TS-specific
27
+ // gap -- macula-mcp already works around it today by giving lobby_observer.ts
28
+ // a dedicated identity+Session PER ROOM TOPIC rather than multiplexing one
29
+ // connection.
30
+ //
31
+ // So a "link" to one station here is a small SET of role-scoped sessions,
32
+ // not one session:
33
+ // - one "control" session (this pool's own caller-supplied identity),
34
+ // used for publish() and call() and NEVER subscribed on -- publish() is
35
+ // write-only (explicitly unguarded by the exclusivity rule) and call()'s
36
+ // own internal queue already serialises concurrent calls safely, so one
37
+ // control session per station is enough regardless of call volume.
38
+ // - one additional session PER CURRENTLY-TRACKED TOPIC, under an identity
39
+ // derived from that topic (shared across every station's copy of that
40
+ // topic's session -- reused across stations is safe, since a station's
41
+ // own per-identity dedupe only kicks a SECOND connection to THAT SAME
42
+ // station, never across different stations). Each does exactly one
43
+ // subscribe() and nothing else.
44
+ // Every one of these sessions is independently monitored and respawned with
45
+ // backoff; a topic session dying just re-subscribes its own one topic on
46
+ // respawn (no "replay N subscriptions onto a survivor" step needed, since
47
+ // each session only ever carried one). A control link can't also carry a
48
+ // liveness-only subscribe without reintroducing the exclusivity problem it
49
+ // exists to avoid, and a failed call() genuinely does surface a dead
50
+ // connection -- but publish() does not: found live 2026-09-04 that a
51
+ // publish() attempt on a session the station had already kicked can still
52
+ // locally "succeed" (the frame is handed off before the QUIC stack notices
53
+ // its own connection is gone), so a publish-only workload could go
54
+ // arbitrarily long without ever discovering a dead control link. Every
55
+ // live control link is therefore also health-checked on a timer
56
+ // (#armHealthCheck's own doc has the mechanism) -- a periodic call() to a
57
+ // procedure name guaranteed never to be advertised, whose failure is
58
+ // inspected to tell "a real BOLT#4 unknown_next_peer came back" (alive)
59
+ // apart from "this never reached anyone at all" (dead, reconnect).
60
+ //
61
+ // == Deliberate deviation from the Erlang reference ==
62
+ //
63
+ // macula_client's own inbound-event dedup keys on (Realm, Publisher, Seq)
64
+ // alone, with no Topic. That is the exact shape of a real bug found and
65
+ // fixed on the station side 2026-09-04 (macula-station's event_dedup): an
66
+ // identity's own auto-published facts and its application-level publishes
67
+ // can share one seq-counter space, and two different topics publishing the
68
+ // same seq collide under a topic-blind key. This pool's own dedup includes
69
+ // Topic from the start.
70
+ import { Identity } from "./identity.js";
71
+ import { MaculaCallError } from "./rpc.js";
72
+ import { Session } from "./session.js";
73
+ /** Thrown by publish()/call() when the pool has zero live control links
74
+ * to try -- a distinct, identifiable condition from any one link's own
75
+ * transient error, matching macula_client:publish/5's own `{error,
76
+ * {transient, no_healthy_station}}`. */
77
+ export class NoHealthyStationError extends Error {
78
+ constructor() {
79
+ super("macula-ts: pool has no currently-live links");
80
+ this.name = "NoHealthyStationError";
81
+ }
82
+ }
83
+ const RECONNECT_BASE_MS = 1_000;
84
+ const RECONNECT_MAX_MS = 30_000;
85
+ /** How often a LIVE control link is health-checked (see #armHealthCheck's
86
+ * own doc for why publish() alone can't be trusted to ever notice a dead
87
+ * connection). Topic-role links need no equivalent -- their own
88
+ * subscribe() already gives them a real, immediate onClosed signal. */
89
+ const HEALTH_CHECK_INTERVAL_MS = 10_000;
90
+ function subKey(realm, topic) {
91
+ return `${realm ?? ""}\0${topic}`;
92
+ }
93
+ function dedupKey(realm, publisher, seq, topic) {
94
+ return `${realm ?? ""}\0${Buffer.from(publisher).toString("hex")}\0${seq}\0${topic}`;
95
+ }
96
+ /**
97
+ * A resilient multi-station client: live connections to every configured
98
+ * seed held concurrently, each independently monitored and respawned
99
+ * with backoff on disconnect, every tracked subscription re-established
100
+ * automatically when its own link reconnects. See this module's own
101
+ * header doc for the full design, why a "link" is a small role-scoped
102
+ * session set rather than one Session, and its one deliberate deviation
103
+ * from the Erlang reference (topic-scoped dedup).
104
+ */
105
+ export class Pool {
106
+ #controlIdentity;
107
+ #controlLinks;
108
+ #seeds;
109
+ #subscriptions = new Map();
110
+ #dedup = new Map();
111
+ #sweepTimer;
112
+ #replicationFactor;
113
+ #dedupWindowMs;
114
+ #healthCheckIntervalMs;
115
+ #closed = false;
116
+ constructor(controlIdentity, seeds, opts) {
117
+ this.#controlIdentity = controlIdentity;
118
+ this.#seeds = seeds;
119
+ const requestedReplication = opts.replicationFactor ?? 1;
120
+ if (requestedReplication !== 1) {
121
+ console.error(`macula-ts pool: replicationFactor ${requestedReplication} is not yet supported (each replica would publish ` +
122
+ "a distinct seq, so a receiver's dedup can't collapse them into one event) -- clamping to 1.");
123
+ }
124
+ this.#replicationFactor = 1;
125
+ this.#dedupWindowMs = opts.dedupWindowMs ?? 60_000;
126
+ this.#healthCheckIntervalMs = opts.healthCheckIntervalMs ?? HEALTH_CHECK_INTERVAL_MS;
127
+ this.#controlLinks = seeds.map((seed) => this.#newRoleLink(seed, controlIdentity, undefined));
128
+ const sweepMs = opts.dedupSweepMs ?? 30_000;
129
+ this.#sweepTimer = setInterval(() => this.#sweepDedup(), sweepMs);
130
+ this.#sweepTimer.unref?.();
131
+ }
132
+ #newRoleLink(seed, identity, onConnected) {
133
+ return {
134
+ seed,
135
+ identity,
136
+ session: undefined,
137
+ status: "connecting",
138
+ reconnectAttempt: 0,
139
+ retryTimer: undefined,
140
+ healthCheckTimer: undefined,
141
+ closing: false,
142
+ inFlight: undefined,
143
+ onConnected,
144
+ closedDuringConnect: false,
145
+ };
146
+ }
147
+ /** Dials the control role against every seed concurrently under
148
+ * `controlIdentity` (the caller's own identity -- publish()/call()
149
+ * are attributed to it on the wire) and resolves once every seed's
150
+ * FIRST connect attempt has settled (success or a logged failure
151
+ * headed into backoff) -- matches tapRoom()'s own "await the first
152
+ * attempt, not the eventual outcome" reasoning (macula-mcp's
153
+ * lobby_observer.ts): a caller that publishes/calls immediately
154
+ * after connect() must not race links still mid-handshake. */
155
+ static async connect(seeds, controlIdentity, opts = {}) {
156
+ if (seeds.length === 0)
157
+ throw new Error("macula-ts: Pool.connect() needs at least one seed");
158
+ const seen = new Set();
159
+ for (const seed of seeds) {
160
+ const seedKey = `${seed.host}:${seed.port}`;
161
+ if (seen.has(seedKey))
162
+ throw new Error(`macula-ts: Pool.connect() given duplicate seed ${seedKey} -- each seed must be a distinct station`);
163
+ seen.add(seedKey);
164
+ }
165
+ const pool = new Pool(controlIdentity, seeds, opts);
166
+ await Promise.all(pool.#controlLinks.map((link) => pool.#attach(link)));
167
+ return pool;
168
+ }
169
+ async #attach(link) {
170
+ const attempt = this.#attachOnce(link);
171
+ link.inFlight = attempt.finally(() => {
172
+ if (link.inFlight === attempt)
173
+ link.inFlight = undefined;
174
+ });
175
+ await link.inFlight;
176
+ }
177
+ /** (Re)connects one role link and, for a topic role, re-subscribes it.
178
+ * Never throws -- a failure logs and schedules a backoff retry, the
179
+ * same self-healing shape every persistent connection in this
180
+ * ecosystem already uses. */
181
+ async #attachOnce(link) {
182
+ if (link.closing)
183
+ return;
184
+ // Defensive: a redundant call on an already-live link must never
185
+ // overwrite its session with a second connection under the same
186
+ // identity to the same station (exactly the collision this whole
187
+ // design exists to avoid). #scheduleReconnect's own idempotency
188
+ // guard is what should prevent this from ever being reachable in
189
+ // practice; this is the belt-and-suspenders backstop.
190
+ if (link.session)
191
+ return;
192
+ link.status = "connecting";
193
+ let session;
194
+ try {
195
+ session = await Session.connect(link.seed.host, link.seed.port, link.identity);
196
+ }
197
+ catch (err) {
198
+ console.error(`macula-ts pool: connect to ${link.seed.host}:${link.seed.port} failed:`, err);
199
+ this.#scheduleReconnect(link);
200
+ return;
201
+ }
202
+ if (link.closing) {
203
+ await session.close(link.identity).catch(() => { });
204
+ return;
205
+ }
206
+ if (link.onConnected) {
207
+ link.closedDuringConnect = false;
208
+ try {
209
+ await link.onConnected(link, session);
210
+ }
211
+ catch (err) {
212
+ console.error(`macula-ts pool: subscribe on ${link.seed.host}:${link.seed.port} failed:`, err);
213
+ await session.close(link.identity).catch(() => { });
214
+ this.#scheduleReconnect(link);
215
+ return;
216
+ }
217
+ if (link.closedDuringConnect) {
218
+ console.error(`macula-ts pool: ${link.seed.host}:${link.seed.port}'s subscription closed before it could be marked live -- retrying`);
219
+ await session.close(link.identity).catch(() => { });
220
+ this.#scheduleReconnect(link);
221
+ return;
222
+ }
223
+ }
224
+ link.session = session;
225
+ link.status = "live";
226
+ link.reconnectAttempt = 0;
227
+ if (!link.onConnected)
228
+ this.#armHealthCheck(link);
229
+ }
230
+ /** Control-role links only (topic roles have their own subscribe()
231
+ * onClosed and never reach here -- `!link.onConnected` is what
232
+ * distinguishes them). publish() is fire-and-forget: found live
233
+ * 2026-09-04 that a publish() attempt on a session the station had
234
+ * already kicked can still locally "succeed" (the frame is handed off
235
+ * before the QUIC stack notices its own connection is gone), so a
236
+ * publish-only caller could go arbitrarily long without this pool
237
+ * ever discovering a dead control link. Every HEALTH_CHECK_INTERVAL_MS
238
+ * while the link is live, this calls a procedure name that is
239
+ * guaranteed never to be advertised (a fresh UUID-shaped string) and
240
+ * inspects the failure: a clean MaculaCallError means a real BOLT#4
241
+ * response came back over the wire (unknown_next_peer, as expected) --
242
+ * the connection is genuinely alive, nothing to do. Any OTHER
243
+ * thrown error means the call never got a wire-level answer at all --
244
+ * the same signal call()'s own doc uses to distinguish a real BOLT#4
245
+ * answer from "this never reached anyone" -- so it's treated exactly
246
+ * like a real operation failure and schedules a reconnect. */
247
+ #armHealthCheck(link) {
248
+ if (link.healthCheckTimer)
249
+ clearInterval(link.healthCheckTimer);
250
+ const timer = setInterval(() => {
251
+ const session = link.session;
252
+ if (!session)
253
+ return;
254
+ const probe = `_pool.healthcheck.${Math.random().toString(36).slice(2)}`;
255
+ session.call(probe, null, { deadlineMs: this.#healthCheckIntervalMs }).then(() => {
256
+ // A real provider somehow answering a random UUID-shaped
257
+ // procedure name is astronomically unlikely and not itself a
258
+ // problem either way -- the connection is clearly alive.
259
+ }, (err) => {
260
+ if (err instanceof MaculaCallError)
261
+ return; // alive: a real wire answer came back
262
+ if (link.session !== session)
263
+ return; // already superseded by a reconnect
264
+ console.error(`macula-ts pool: health check on ${link.seed.host}:${link.seed.port} failed:`, err);
265
+ this.#scheduleReconnect(link);
266
+ });
267
+ }, this.#healthCheckIntervalMs);
268
+ timer.unref?.();
269
+ link.healthCheckTimer = timer;
270
+ }
271
+ /** Marks `link` for backoff and schedules its next reconnect attempt.
272
+ * Closes whatever session it currently holds first (fire-and-forget,
273
+ * not awaited -- the retry timer must not wait on a graceful close
274
+ * round trip) so a still-alive session from an OPERATION failure
275
+ * (publish()/call() throwing for a reason that doesn't mean the
276
+ * connection itself already died, unlike a subscribe onClosed) is
277
+ * never simply orphaned. Found live 2026-09-04: without this, a
278
+ * publish/call failure left the old session's native handle both
279
+ * leaked AND, worse, still open under this link's identity -- the
280
+ * NEXT connect attempt for this SAME station under that SAME
281
+ * identity then races the still-live old one for the station's own
282
+ * per-identity dedupe kick, with no guarantee which one loses. The
283
+ * close is fired here, before scheduling, so it has the full backoff
284
+ * delay (at least RECONNECT_BASE_MS) to land before a new connect
285
+ * attempt begins.
286
+ *
287
+ * Idempotent per backoff episode: a no-op once `link.status` is
288
+ * already "backoff". Found live in review 2026-09-05: concurrent
289
+ * operations queued on one session (e.g. several call()s serialised
290
+ * by Session's own internal queue) can ALL fail once that session
291
+ * dies, and each one's catch handler calls this -- without this
292
+ * guard, every failure past the first would re-close an
293
+ * already-`undefined` `link.session` (harmless) but ALSO arm a
294
+ * second, third, ... retryTimer, each bumping reconnectAttempt
295
+ * independently, leaving multiple redundant reconnects racing each
296
+ * other. Deliberately NOT guarded against "connecting" -- this is
297
+ * also called from `#attachOnce`'s own failure paths, while status
298
+ * is still "connecting" from the top of that same function, and
299
+ * that path must proceed normally. */
300
+ #scheduleReconnect(link) {
301
+ if (link.closing)
302
+ return;
303
+ if (link.status === "backoff")
304
+ return;
305
+ if (link.healthCheckTimer) {
306
+ clearInterval(link.healthCheckTimer);
307
+ link.healthCheckTimer = undefined;
308
+ }
309
+ const stale = link.session;
310
+ link.status = "backoff";
311
+ link.session = undefined;
312
+ if (stale)
313
+ void stale.close(link.identity).catch(() => { });
314
+ const delay = Math.min(RECONNECT_MAX_MS, RECONNECT_BASE_MS * 2 ** link.reconnectAttempt);
315
+ link.reconnectAttempt += 1;
316
+ const timer = setTimeout(() => {
317
+ const attempt = this.#attachOnce(link);
318
+ link.inFlight = attempt.finally(() => {
319
+ if (link.inFlight === attempt)
320
+ link.inFlight = undefined;
321
+ });
322
+ }, delay);
323
+ // Deliberately NOT unref()'d, unlike healthCheckTimer/sweepTimer --
324
+ // this pool's own documented contract is "retry forever" for a
325
+ // link in backoff. Found live in review 2026-09-05: a pure
326
+ // subscribe()-only process (no other keep-alive handle: no HTTP
327
+ // listener, no stdio transport) would otherwise exit cleanly the
328
+ // moment its last live connection drops, silently breaking that
329
+ // promise instead of actually retrying through the outage.
330
+ link.retryTimer = timer;
331
+ }
332
+ #sweepDedup() {
333
+ const cutoff = Date.now() - this.#dedupWindowMs;
334
+ for (const [key, at] of this.#dedup) {
335
+ if (at < cutoff)
336
+ this.#dedup.delete(key);
337
+ }
338
+ }
339
+ #liveControlLinks() {
340
+ return this.#controlLinks.filter((l) => l.status === "live" && l.session);
341
+ }
342
+ /** Refuses an `identity` that would double-connect to the same
343
+ * station as the control role or another already-tracked
344
+ * subscription -- the station kicks the OLDER of the two, this pool
345
+ * reconnects it, which gets IT kicked in turn: a perpetual ping-pong
346
+ * with no error naming the cause. Compares raw node-id bytes, not
347
+ * object identity, so two separately-loaded Identity instances over
348
+ * the SAME underlying keypair are still caught. */
349
+ #assertIdentityNotAlreadyUsed(identity) {
350
+ const nodeIdHex = Buffer.from(identity.nodeId).toString("hex");
351
+ if (nodeIdHex === Buffer.from(this.#controlIdentity.nodeId).toString("hex")) {
352
+ throw new Error("macula-ts pool: this identity is already the pool's own control identity -- reusing it for a subscription would double-connect one identity to one station");
353
+ }
354
+ for (const sub of this.#subscriptions.values()) {
355
+ if (Buffer.from(sub.identity.nodeId).toString("hex") === nodeIdHex) {
356
+ throw new Error(`macula-ts pool: this identity is already used by the "${sub.topic}" subscription -- reusing it here would double-connect one identity to one station`);
357
+ }
358
+ }
359
+ }
360
+ /** Publishes to `replicationFactor` currently-live control links
361
+ * (default 1); partial success counts as success. A link whose
362
+ * publish attempt fails is marked for respawn immediately -- this is
363
+ * this v1's only liveness signal for the control role, since it
364
+ * cannot also carry a liveness-only subscribe (see this module's own
365
+ * header doc). Throws NoHealthyStationError if zero links are live. */
366
+ async publish(realm, topic, payload, opts = {}) {
367
+ if (this.#closed)
368
+ throw new Error("macula-ts pool: used after close()");
369
+ const targets = this.#liveControlLinks().slice(0, this.#replicationFactor);
370
+ if (targets.length === 0)
371
+ throw new NoHealthyStationError();
372
+ const results = await Promise.allSettled(targets.map(async (link) => {
373
+ const session = link.session;
374
+ if (link.status !== "live" || !session)
375
+ throw new NoHealthyStationError(); // superseded since targets was captured
376
+ try {
377
+ await session.publish(topic, payload, { realm, ttlMs: opts.ttlMs });
378
+ }
379
+ catch (err) {
380
+ if (link.session === session)
381
+ this.#scheduleReconnect(link);
382
+ throw err;
383
+ }
384
+ }));
385
+ if (!results.some((r) => r.status === "fulfilled")) {
386
+ const first = results.find((r) => r.status === "rejected");
387
+ throw first ? first.reason : new Error("macula-ts pool: publish failed on every targeted link");
388
+ }
389
+ }
390
+ /** Calls `procedure` against the pool's live control links in order
391
+ * until one succeeds or all have been tried. Throws
392
+ * NoHealthyStationError if zero links are live.
393
+ *
394
+ * A `MaculaCallError` (a real BOLT#4 response -- e.g.
395
+ * unknown_next_peer, unauthorized) does NOT mark its link for
396
+ * respawn: the connection plainly worked, it answered. Found in
397
+ * review 2026-09-05: treating every thrown error as evidence of a
398
+ * dead connection meant a single genuinely-answered error (a
399
+ * procedure nobody serves, a bad realm, a gated call this identity
400
+ * isn't authorized for) tore down and reconnected EVERY live control
401
+ * link in turn as call() moved through them re-trying the same
402
+ * doomed call -- and for a non-idempotent provider handler, actually
403
+ * re-invoked it on each one. Only an error that never got a
404
+ * wire-level answer at all (the same distinction call()'s own doc on
405
+ * Session draws) is treated as a dead link. Each link's own `session`
406
+ * reference is re-checked before scheduling a reconnect, in case a
407
+ * concurrent operation already superseded it between this loop
408
+ * starting and this iteration running. */
409
+ async call(realm, procedure, payload, opts = {}) {
410
+ if (this.#closed)
411
+ throw new Error("macula-ts pool: used after close()");
412
+ const targets = this.#liveControlLinks();
413
+ if (targets.length === 0)
414
+ throw new NoHealthyStationError();
415
+ let lastErr;
416
+ for (const link of targets) {
417
+ const session = link.session;
418
+ if (link.status !== "live" || !session)
419
+ continue; // superseded since `targets` was captured
420
+ try {
421
+ return await session.call(procedure, payload, { realm, deadlineMs: opts.deadlineMs });
422
+ }
423
+ catch (err) {
424
+ lastErr = err;
425
+ if (!(err instanceof MaculaCallError) && link.session === session)
426
+ this.#scheduleReconnect(link);
427
+ }
428
+ }
429
+ throw lastErr;
430
+ }
431
+ /** Subscribes `handler` to `(realm, topic)`: opens one subscribe-only
432
+ * session against every configured seed, replayed automatically on
433
+ * every future respawn. Mints a dedicated identity for this topic by
434
+ * default (disposed on unsubscribe); pass `identity` to supply the
435
+ * pool's own instead (e.g. for a stable, caller-controlled identity
436
+ * across restarts, matching macula-mcp's own observeRoomIdentityPath
437
+ * pattern) -- the pool never disposes an identity it didn't mint.
438
+ * Returns an unsubscribe function. */
439
+ async subscribe(realm, topic, handler, identity) {
440
+ if (this.#closed)
441
+ throw new Error("macula-ts pool: used after close()");
442
+ const key = subKey(realm, topic);
443
+ if (this.#subscriptions.has(key))
444
+ throw new Error(`macula-ts pool: already subscribed to ${topic}${realm ? ` (realm ${realm})` : ""}`);
445
+ if (identity)
446
+ this.#assertIdentityNotAlreadyUsed(identity);
447
+ const ownsIdentity = identity === undefined;
448
+ const subIdentity = identity ?? Identity.generate();
449
+ const sub = { realm, topic, handler, identity: subIdentity, ownsIdentity, links: [] };
450
+ const onConnected = async (link, session) => {
451
+ await session.subscribe(topic, (evt) => {
452
+ const dkey = dedupKey(realm, evt.publisher, evt.seq, topic);
453
+ if (this.#dedup.has(dkey))
454
+ return;
455
+ this.#dedup.set(dkey, Date.now());
456
+ sub.handler(evt);
457
+ }, {
458
+ realm,
459
+ onClosed: (err) => {
460
+ if (link.session === session) {
461
+ console.error(`macula-ts pool: subscription to ${topic} on ${link.seed.host}:${link.seed.port} dropped (${err.message}) -- reconnecting`);
462
+ this.#scheduleReconnect(link);
463
+ }
464
+ else {
465
+ // subscribe-start hasn't resolved on this side yet (or
466
+ // this link has already moved on) -- flag it so
467
+ // #attachOnce notices once `onConnected` itself returns,
468
+ // rather than marking a link live with a subscription
469
+ // that already silently died. See closedDuringConnect's
470
+ // own doc.
471
+ link.closedDuringConnect = true;
472
+ }
473
+ },
474
+ });
475
+ };
476
+ sub.links = this.#seeds.map((seed) => this.#newRoleLink(seed, subIdentity, onConnected));
477
+ this.#subscriptions.set(key, sub);
478
+ await Promise.all(sub.links.map((link) => this.#attach(link)));
479
+ return async () => {
480
+ const tracked = this.#subscriptions.get(key);
481
+ if (!tracked)
482
+ return;
483
+ this.#subscriptions.delete(key);
484
+ await Promise.all(tracked.links.map(async (link) => {
485
+ link.closing = true;
486
+ if (link.retryTimer)
487
+ clearTimeout(link.retryTimer);
488
+ if (link.inFlight)
489
+ await link.inFlight.catch(() => { });
490
+ if (link.session)
491
+ await link.session.close(link.identity).catch(() => { });
492
+ }));
493
+ if (tracked.ownsIdentity)
494
+ tracked.identity.dispose();
495
+ };
496
+ }
497
+ /** Live/backing-off CONTROL link counts (publish/call reachability).
498
+ * Every configured seed is exactly one or the other. Per-topic
499
+ * subscription link health is not reflected here -- inspect a
500
+ * specific subscription's own behavior (events arriving or not)
501
+ * instead; exposing N independent per-topic health vectors here
502
+ * would not simplify what a caller actually needs to know. */
503
+ status() {
504
+ const healthy = this.#liveControlLinks().length;
505
+ return { healthyLinks: healthy, failedLinks: this.#controlLinks.length - healthy };
506
+ }
507
+ /** Closes every control and subscription link and disposes every
508
+ * identity this pool owns (the caller-supplied control identity
509
+ * included). Awaits each link's in-flight connect/reconnect first,
510
+ * so a still-connecting link never has its identity yanked out from
511
+ * under it. */
512
+ async close() {
513
+ if (this.#closed)
514
+ return;
515
+ this.#closed = true;
516
+ clearInterval(this.#sweepTimer);
517
+ const allLinks = [...this.#controlLinks, ...[...this.#subscriptions.values()].flatMap((s) => s.links)];
518
+ await Promise.all(allLinks.map(async (link) => {
519
+ link.closing = true;
520
+ if (link.retryTimer)
521
+ clearTimeout(link.retryTimer);
522
+ if (link.healthCheckTimer)
523
+ clearInterval(link.healthCheckTimer);
524
+ if (link.inFlight)
525
+ await link.inFlight.catch(() => { });
526
+ if (link.session)
527
+ await link.session.close(link.identity).catch(() => { });
528
+ link.session = undefined;
529
+ link.status = "backoff"; // reflects reality for status()/#liveControlLinks() if either is called after close()
530
+ }));
531
+ for (const sub of this.#subscriptions.values())
532
+ if (sub.ownsIdentity)
533
+ sub.identity.dispose();
534
+ this.#subscriptions.clear();
535
+ this.#controlIdentity.dispose();
536
+ }
537
+ }
538
+ //# sourceMappingURL=pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.js","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@macula-io/ts",
3
- "version": "0.13.5",
3
+ "version": "0.14.0",
4
4
  "description": "TypeScript SDK for the Macula mesh (FFI over macula-go)",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -46,7 +46,7 @@
46
46
  "build:prebuilds": "npm run build:go && prebuildify --napi --strip && npm pkg delete scripts.install",
47
47
  "build": "npm run build:addon:dev && tsc",
48
48
  "test": "vitest run",
49
- "test:live": "MACULA_TS_LIVE=1 vitest run src/session.live.test.ts src/rpc.live.test.ts src/dht.live.test.ts src/pubsub.live.test.ts src/content.live.test.ts src/ucan.live.test.ts src/directdial.live.test.ts",
49
+ "test:live": "MACULA_TS_LIVE=1 vitest run src/session.live.test.ts src/rpc.live.test.ts src/dht.live.test.ts src/pubsub.live.test.ts src/content.live.test.ts src/ucan.live.test.ts src/directdial.live.test.ts src/pool.live.test.ts",
50
50
  "typecheck": "tsc --noEmit"
51
51
  },
52
52
  "dependencies": {