@voltro/plugin-broadcast 0.54.0 → 0.56.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/CHANGELOG.md +639 -2
- package/dist/index.d.ts +92 -11
- package/dist/index.js +162 -73
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -24,20 +24,22 @@ export declare interface AttachBroadcastBusOptions {
|
|
|
24
24
|
/** Channel override (tests). Default `voltro:changes`. */
|
|
25
25
|
readonly channel?: string;
|
|
26
26
|
/**
|
|
27
|
-
* Called when this replica
|
|
27
|
+
* Called when this replica has a hole in what it received.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* and always correct. `voltro dev` and `voltro serve` wire this to the
|
|
34
|
-
* dispatcher's refresh.
|
|
29
|
+
* Two causes, one shape (see `BroadcastGap`), because there is one recovery:
|
|
30
|
+
* re-run every live query. A live query is idempotent, so re-running all of
|
|
31
|
+
* them is always safe and always complete — which is the only recovery
|
|
32
|
+
* available, since pub/sub keeps no log and there is nothing to replay.
|
|
35
33
|
*
|
|
36
|
-
*
|
|
34
|
+
* - a peer's serial jumped: the count is EXACT, not an estimate.
|
|
35
|
+
* - this replica was not subscribed for a while: no origin, no count, and
|
|
36
|
+
* the same instruction.
|
|
37
|
+
*
|
|
38
|
+
* Absent ⇒ the hole is still DETECTED and logged; only the recovery is
|
|
37
39
|
* missing. That is deliberate: a bus used without a dispatcher (a test, an
|
|
38
40
|
* embedder) should still say what it lost.
|
|
39
41
|
*/
|
|
40
|
-
readonly onGap?: (
|
|
42
|
+
readonly onGap?: (gap: BroadcastGap) => void;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
/**
|
|
@@ -89,6 +91,28 @@ export declare interface BroadcastEnvelope {
|
|
|
89
91
|
* than as a gap.
|
|
90
92
|
*/
|
|
91
93
|
readonly n?: number;
|
|
94
|
+
/**
|
|
95
|
+
* Which PROCESS of that origin published this — a nonce minted once per bus
|
|
96
|
+
* attach, meaningless except for being different after a restart.
|
|
97
|
+
*
|
|
98
|
+
* `origin` is a replica's NAME, and a name can outlive the process wearing
|
|
99
|
+
* it: a StatefulSet pod keeps `POD_NAME` across a restart, and
|
|
100
|
+
* `VOLTRO_REPLICA_ID` is stable by definition. The serial, however, restarts
|
|
101
|
+
* at 1 — so a receiver holding a watermark of 500 sees the new process's
|
|
102
|
+
* 1, 2, 3… as "not newer than what I have", never advances, and reports no
|
|
103
|
+
* gap for the next 500 changes. Gap detection for that peer is simply off,
|
|
104
|
+
* silently, and precisely after the event that most deserves a refresh.
|
|
105
|
+
*
|
|
106
|
+
* The epoch turns that into a fact the receiver can read: a different epoch
|
|
107
|
+
* under a known origin means a NEW process, so reset the watermark. It is
|
|
108
|
+
* NOT reported as a gap — a restart is not evidence that this replica missed
|
|
109
|
+
* anything, and a gap is a claim about loss.
|
|
110
|
+
*
|
|
111
|
+
* Optional for the same reason `n` is: a message from an older replica
|
|
112
|
+
* mid-rolling-deploy carries none, and "cannot tell" must not read as
|
|
113
|
+
* "restarted".
|
|
114
|
+
*/
|
|
115
|
+
readonly epoch?: string;
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
/**
|
|
@@ -115,6 +139,24 @@ declare const BroadcastError_base: Schema.TaggedErrorClass<BroadcastError, "Broa
|
|
|
115
139
|
cause: Schema.optional<typeof Schema.Unknown>;
|
|
116
140
|
}>;
|
|
117
141
|
|
|
142
|
+
/**
|
|
143
|
+
* A proven — or presumed — hole in what this replica received.
|
|
144
|
+
*
|
|
145
|
+
* ONE shape for both causes, because the recovery is one thing: re-run every
|
|
146
|
+
* live query. Two callbacks would have been two things for `voltro dev` and
|
|
147
|
+
* `voltro serve` to each wire, and this repo has the history to say how that
|
|
148
|
+
* ends.
|
|
149
|
+
*/
|
|
150
|
+
export declare interface BroadcastGap {
|
|
151
|
+
/** What happened, for the refresh's log line. */
|
|
152
|
+
readonly reason: string;
|
|
153
|
+
/** The peer whose serial jumped — absent when the hole is this replica's own
|
|
154
|
+
* (it was not subscribed). */
|
|
155
|
+
readonly origin?: string;
|
|
156
|
+
/** EXACT count of missed messages, when it is known. Never an estimate. */
|
|
157
|
+
readonly missed?: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
118
160
|
/**
|
|
119
161
|
* A broadcast plugin instance also exposes the resolved provider so the
|
|
120
162
|
* CLI's serve pipeline can attach the bus to the live DataStore. The
|
|
@@ -189,6 +231,22 @@ export declare interface BroadcastProvider {
|
|
|
189
231
|
readonly publish: (channel: string, payload: string) => Effect.Effect<void, BroadcastError>;
|
|
190
232
|
readonly subscribe: (channel: string, handler: (payload: string) => void) => Effect.Effect<() => void, BroadcastError>;
|
|
191
233
|
readonly close: () => Effect.Effect<void>;
|
|
234
|
+
/**
|
|
235
|
+
* Subscribe to the TRANSPORT's own connection lifecycle, when the backend
|
|
236
|
+
* has one to report.
|
|
237
|
+
*
|
|
238
|
+
* A broker outage is not visible in `publish`/`subscribe`: a driver that
|
|
239
|
+
* reconnects on its own hands back a working transport and says nothing, and
|
|
240
|
+
* every message published while it was down is gone. The bus's serial
|
|
241
|
+
* accounting catches that on the next message from a peer — but only if a
|
|
242
|
+
* peer publishes again. On a quiet table, "no peer published again" and "we
|
|
243
|
+
* are up to date" look identical, and one of them is stale forever.
|
|
244
|
+
*
|
|
245
|
+
* Optional: the memory provider has no connection to lose, and a provider a
|
|
246
|
+
* user brings themselves need not implement it. Absent means the bus falls
|
|
247
|
+
* back to serial-only detection, which is where it was before this existed.
|
|
248
|
+
*/
|
|
249
|
+
readonly onTransportEvent?: (listener: (event: BroadcastTransportEvent) => void) => () => void;
|
|
192
250
|
}
|
|
193
251
|
|
|
194
252
|
/** Provider id understood by `resolveBroadcastProvider`. */
|
|
@@ -201,6 +259,12 @@ export declare interface BroadcastStore {
|
|
|
201
259
|
injectExternalChange: (event: ChangeEvent) => void;
|
|
202
260
|
}
|
|
203
261
|
|
|
262
|
+
export declare interface BroadcastTransportEvent {
|
|
263
|
+
readonly kind: 'disconnected' | 'reconnected';
|
|
264
|
+
/** Driver detail for the log line — never parsed. */
|
|
265
|
+
readonly detail?: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
204
268
|
/**
|
|
205
269
|
* One framework channel inside a namespace.
|
|
206
270
|
*
|
|
@@ -268,6 +332,12 @@ declare interface NatsLike {
|
|
|
268
332
|
publish: (subject: string, data: Uint8Array) => void;
|
|
269
333
|
subscribe: (subject: string) => NatsSubscription;
|
|
270
334
|
drain: () => Promise<void>;
|
|
335
|
+
/** nats.js' connection events. Read for two reasons: to report a reconnect as
|
|
336
|
+
* the hole it is, and to notice a connection that gave up. */
|
|
337
|
+
status: () => AsyncIterable<{
|
|
338
|
+
type: string;
|
|
339
|
+
data?: unknown;
|
|
340
|
+
}>;
|
|
271
341
|
}
|
|
272
342
|
|
|
273
343
|
export declare const natsProvider: (options: NatsProviderOptions) => BroadcastProvider;
|
|
@@ -291,8 +361,13 @@ declare interface RedisLike {
|
|
|
291
361
|
publish: (channel: string, message: string) => Promise<number>;
|
|
292
362
|
subscribe: (channel: string) => Promise<unknown>;
|
|
293
363
|
unsubscribe: (channel: string) => Promise<unknown>;
|
|
294
|
-
on
|
|
295
|
-
|
|
364
|
+
on(event: 'message', listener: (channel: string, message: string) => void): void;
|
|
365
|
+
/** ioredis' connection lifecycle. The subscriber re-subscribes its channels
|
|
366
|
+
* itself on `ready`; what it does NOT do is tell anyone that the messages
|
|
367
|
+
* sent while it was away are gone. That is what these are read for. */
|
|
368
|
+
on(event: 'ready' | 'reconnecting' | 'end' | 'close', listener: () => void): void;
|
|
369
|
+
off(event: 'message', listener: (channel: string, message: string) => void): void;
|
|
370
|
+
off(event: 'ready' | 'reconnecting' | 'end' | 'close', listener: () => void): void;
|
|
296
371
|
duplicate: () => RedisLike;
|
|
297
372
|
quit: () => Promise<unknown>;
|
|
298
373
|
}
|
|
@@ -349,4 +424,10 @@ export declare const resolveBroadcastProvider: (options: ResolveBroadcastOptions
|
|
|
349
424
|
crossReplica: boolean;
|
|
350
425
|
};
|
|
351
426
|
|
|
427
|
+
/** Retry cadence for a subscribe that could not land: 500ms doubling to a 10s
|
|
428
|
+
* ceiling, forever. A bus that gives up subscribing is a replica that is
|
|
429
|
+
* permanently deaf and says so once, which is the state this retry exists to
|
|
430
|
+
* make impossible. */
|
|
431
|
+
export declare const subscribeRetryDelayMs: (attempt: number) => number;
|
|
432
|
+
|
|
352
433
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -34,13 +34,25 @@ var r = "voltro", i = (e) => e.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-"
|
|
|
34
34
|
message: t,
|
|
35
35
|
transient: !1,
|
|
36
36
|
...n === void 0 ? {} : { cause: n }
|
|
37
|
-
}), p =
|
|
38
|
-
let
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
},
|
|
43
|
-
|
|
37
|
+
}), p = () => {
|
|
38
|
+
let e = /* @__PURE__ */ new Set();
|
|
39
|
+
return {
|
|
40
|
+
subscribe: (t) => (e.add(t), () => {
|
|
41
|
+
e.delete(t);
|
|
42
|
+
}),
|
|
43
|
+
emit: (t) => {
|
|
44
|
+
for (let n of e) try {
|
|
45
|
+
n(t);
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}, m = /* @__PURE__ */ new Map(), h = (e) => {
|
|
50
|
+
let t = m.get(e);
|
|
51
|
+
return t || (t = { handlers: /* @__PURE__ */ new Map() }, m.set(e, t)), t;
|
|
52
|
+
}, g = (e) => {
|
|
53
|
+
m.get(e)?.handlers.clear();
|
|
54
|
+
}, _ = (t = `default-${Math.random().toString(36).slice(2)}`) => {
|
|
55
|
+
let n = h(t);
|
|
44
56
|
return {
|
|
45
57
|
name: "memory",
|
|
46
58
|
publish: (t, r) => e.sync(() => {
|
|
@@ -57,8 +69,8 @@ var r = "voltro", i = (e) => e.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-"
|
|
|
57
69
|
}),
|
|
58
70
|
close: () => e.void
|
|
59
71
|
};
|
|
60
|
-
},
|
|
61
|
-
let n = null, r = () => n || (n = (async () => {
|
|
72
|
+
}, v = (t) => {
|
|
73
|
+
let n = null, r = p(), i = !0, a = () => n || (n = (async () => {
|
|
62
74
|
let e;
|
|
63
75
|
if (t.client) e = t.client;
|
|
64
76
|
else {
|
|
@@ -71,27 +83,37 @@ var r = "voltro", i = (e) => e.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-"
|
|
|
71
83
|
e = new n(t.url);
|
|
72
84
|
}
|
|
73
85
|
let n = e.duplicate();
|
|
74
|
-
return {
|
|
86
|
+
return n.on("reconnecting", () => {
|
|
87
|
+
i && (i = !1, r.emit({
|
|
88
|
+
kind: "disconnected",
|
|
89
|
+
detail: "ioredis is re-dialling"
|
|
90
|
+
}));
|
|
91
|
+
}), n.on("ready", () => {
|
|
92
|
+
i || (i = !0, r.emit({ kind: "reconnected" }));
|
|
93
|
+
}), {
|
|
75
94
|
pub: e,
|
|
76
95
|
sub: n
|
|
77
96
|
};
|
|
78
|
-
})()
|
|
97
|
+
})().catch((e) => {
|
|
98
|
+
throw n = null, e;
|
|
99
|
+
}), n);
|
|
79
100
|
return {
|
|
80
101
|
name: "redis",
|
|
102
|
+
onTransportEvent: r.subscribe,
|
|
81
103
|
publish: (t, n) => e.tryPromise({
|
|
82
104
|
try: async () => {
|
|
83
|
-
let { pub: e } = await
|
|
105
|
+
let { pub: e } = await a();
|
|
84
106
|
await e.publish(t, n);
|
|
85
107
|
},
|
|
86
108
|
catch: (e) => d("redis", `publish failed: ${String(e?.message ?? e)}`, e)
|
|
87
109
|
}),
|
|
88
110
|
subscribe: (t, n) => e.tryPromise({
|
|
89
111
|
try: async () => {
|
|
90
|
-
let { sub: e } = await
|
|
112
|
+
let { sub: e } = await a(), r = (e, r) => {
|
|
91
113
|
e === t && n(r);
|
|
92
114
|
};
|
|
93
|
-
return e.on("message",
|
|
94
|
-
e.off("message",
|
|
115
|
+
return e.on("message", r), await e.subscribe(t), () => {
|
|
116
|
+
e.off("message", r), e.unsubscribe(t);
|
|
95
117
|
};
|
|
96
118
|
},
|
|
97
119
|
catch: (e) => d("redis", `subscribe failed: ${String(e?.message ?? e)}`, e)
|
|
@@ -103,25 +125,50 @@ var r = "voltro", i = (e) => e.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-"
|
|
|
103
125
|
} catch {}
|
|
104
126
|
})
|
|
105
127
|
};
|
|
106
|
-
},
|
|
107
|
-
let n = null, r = ()
|
|
128
|
+
}, y = new TextEncoder(), b = new TextDecoder(), x = (t) => {
|
|
129
|
+
let n = null, r = p(), i = !0, a = (e) => {
|
|
130
|
+
(async () => {
|
|
131
|
+
try {
|
|
132
|
+
for await (let t of e.status()) t.type === "disconnect" && i ? (i = !1, r.emit({
|
|
133
|
+
kind: "disconnected",
|
|
134
|
+
detail: "nats disconnected"
|
|
135
|
+
})) : t.type === "reconnect" && !i && (i = !0, r.emit({ kind: "reconnected" }));
|
|
136
|
+
} catch {}
|
|
137
|
+
})();
|
|
138
|
+
}, o = () => import("@nats-io/transport-node").catch((e) => {
|
|
108
139
|
throw f("nats", "the 'nats' broadcast provider requires the '@nats-io/transport-node' optional dependency. Install it to enable it.", e);
|
|
109
|
-
}),
|
|
140
|
+
}), s = () => n || (n = (async () => {
|
|
141
|
+
if (t.connection) return t.connection;
|
|
142
|
+
let e = await (await o()).connect({
|
|
143
|
+
servers: t.url,
|
|
144
|
+
maxReconnectAttempts: -1,
|
|
145
|
+
waitOnFirstConnect: !0,
|
|
146
|
+
reconnectTimeWait: 2e3
|
|
147
|
+
});
|
|
148
|
+
return a(e), e;
|
|
149
|
+
})().catch((e) => {
|
|
150
|
+
throw n = null, e;
|
|
151
|
+
}), n);
|
|
110
152
|
return {
|
|
111
153
|
name: "nats",
|
|
154
|
+
onTransportEvent: r.subscribe,
|
|
112
155
|
publish: (t, n) => e.tryPromise({
|
|
113
156
|
try: async () => {
|
|
114
|
-
(await
|
|
157
|
+
(await s()).publish(t, y.encode(n));
|
|
115
158
|
},
|
|
116
159
|
catch: (e) => d("nats", `publish failed: ${String(e?.message ?? e)}`, e)
|
|
117
160
|
}),
|
|
118
161
|
subscribe: (t, n) => e.tryPromise({
|
|
119
162
|
try: async () => {
|
|
120
|
-
let e = (await
|
|
163
|
+
let e = (await s()).subscribe(t), a = !1;
|
|
121
164
|
return (async () => {
|
|
122
|
-
for await (let t of e) n(
|
|
165
|
+
for await (let t of e) n(b.decode(t.data));
|
|
166
|
+
!a && i && (i = !1, r.emit({
|
|
167
|
+
kind: "disconnected",
|
|
168
|
+
detail: `the nats subscription to '${t}' ended while the connection was up`
|
|
169
|
+
}));
|
|
123
170
|
})(), () => {
|
|
124
|
-
e.unsubscribe();
|
|
171
|
+
a = !0, e.unsubscribe();
|
|
125
172
|
};
|
|
126
173
|
},
|
|
127
174
|
catch: (e) => d("nats", `subscribe failed: ${String(e?.message ?? e)}`, e)
|
|
@@ -133,7 +180,7 @@ var r = "voltro", i = (e) => e.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-"
|
|
|
133
180
|
} catch {}
|
|
134
181
|
})
|
|
135
182
|
};
|
|
136
|
-
},
|
|
183
|
+
}, S = (e, t) => t[`${e.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_REDIS_URL`], C = (e, t) => {
|
|
137
184
|
if (e.provider && typeof e.provider == "object") {
|
|
138
185
|
let t = e.provider;
|
|
139
186
|
return {
|
|
@@ -142,7 +189,7 @@ var r = "voltro", i = (e) => e.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-"
|
|
|
142
189
|
crossReplica: t.name !== "memory"
|
|
143
190
|
};
|
|
144
191
|
}
|
|
145
|
-
let n = e.url ?? t.BROADCAST_URL ?? null, r =
|
|
192
|
+
let n = e.url ?? t.BROADCAST_URL ?? null, r = S(e.connection ?? "broadcast", t), i = (e.provider ?? t.BROADCAST_PROVIDER)?.toLowerCase() ?? (n?.startsWith("nats://") ? "nats" : n?.startsWith("redis://") || n?.startsWith("rediss://") || r || t.REDIS_URL ? "redis" : void 0), a = (e.provider ?? t.BROADCAST_PROVIDER)?.toLowerCase();
|
|
146
193
|
if (a !== void 0 && a !== "redis" && a !== "nats" && a !== "memory") throw Error(`broadcastPlugin: unknown provider "${a}".\n Valid: 'redis', 'nats', 'memory'.
|
|
147
194
|
A name we do not recognise used to fall through to the in-process bus, which boots fine on one replica and drops cross-replica traffic on the second.`);
|
|
148
195
|
if (n !== null && n !== "" && !/^(redis|rediss|nats):\/\//.test(n)) throw Error(`broadcastPlugin: url "${n}" names no provider we support.\n Expected a redis://, rediss:// or nats:// url.`);
|
|
@@ -152,13 +199,13 @@ var r = "voltro", i = (e) => e.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-"
|
|
|
152
199
|
if (!e) {
|
|
153
200
|
if (a === "redis") throw Error("broadcastPlugin: provider \"redis\" was requested but no url resolves.\n Set BROADCAST_URL, BROADCAST_REDIS_URL or REDIS_URL, or pass `url`.\n Falling back to the in-process bus here would answer a stated requirement with a silent downgrade.");
|
|
154
201
|
return {
|
|
155
|
-
provider:
|
|
202
|
+
provider: _(),
|
|
156
203
|
url: null,
|
|
157
204
|
crossReplica: !1
|
|
158
205
|
};
|
|
159
206
|
}
|
|
160
207
|
return {
|
|
161
|
-
provider:
|
|
208
|
+
provider: v({ url: e }),
|
|
162
209
|
url: e,
|
|
163
210
|
crossReplica: !0
|
|
164
211
|
};
|
|
@@ -166,22 +213,22 @@ var r = "voltro", i = (e) => e.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-"
|
|
|
166
213
|
case "nats": {
|
|
167
214
|
let e = n ?? t.NATS_URL;
|
|
168
215
|
return e ? {
|
|
169
|
-
provider:
|
|
216
|
+
provider: x({ url: e }),
|
|
170
217
|
url: e,
|
|
171
218
|
crossReplica: !0
|
|
172
219
|
} : {
|
|
173
|
-
provider:
|
|
220
|
+
provider: _(),
|
|
174
221
|
url: null,
|
|
175
222
|
crossReplica: !1
|
|
176
223
|
};
|
|
177
224
|
}
|
|
178
225
|
default: return {
|
|
179
|
-
provider:
|
|
226
|
+
provider: _(),
|
|
180
227
|
url: null,
|
|
181
228
|
crossReplica: !1
|
|
182
229
|
};
|
|
183
230
|
}
|
|
184
|
-
},
|
|
231
|
+
}, w = ["network:outbound:*"], T = [
|
|
185
232
|
{
|
|
186
233
|
name: "BROADCAST_URL",
|
|
187
234
|
required: !1,
|
|
@@ -217,8 +264,8 @@ var r = "voltro", i = (e) => e.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-"
|
|
|
217
264
|
description: "Fallback broker URL used when the resolved provider is nats and no BROADCAST_URL is set (comma-separated for a cluster).",
|
|
218
265
|
example: "nats://localhost:4222"
|
|
219
266
|
}
|
|
220
|
-
],
|
|
221
|
-
let r =
|
|
267
|
+
], E = (t = {}) => {
|
|
268
|
+
let r = C({
|
|
222
269
|
...t.provider === void 0 ? {} : { provider: t.provider },
|
|
223
270
|
...t.url === void 0 ? {} : { url: t.url },
|
|
224
271
|
...t.connection === void 0 ? {} : { connection: t.connection }
|
|
@@ -227,8 +274,8 @@ var r = "voltro", i = (e) => e.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-"
|
|
|
227
274
|
...n({
|
|
228
275
|
name: t.name ? `@voltro/plugin-broadcast#${t.name}` : "@voltro/plugin-broadcast",
|
|
229
276
|
description: "Cross-replica reactivity — fans out app-mutation ChangeEvents to every replica over a pub/sub bus (Redis / NATS). ADDITIVE to inline emit: local reactivity survives a broker outage. Closes the single-instance gap for every non-postgres dialect.",
|
|
230
|
-
permissions:
|
|
231
|
-
declaredEnv:
|
|
277
|
+
permissions: w,
|
|
278
|
+
declaredEnv: T,
|
|
232
279
|
onActivate: (t) => e.sync(() => {
|
|
233
280
|
r.crossReplica ? t.logger.info("broadcast active", {
|
|
234
281
|
provider: r.provider.name,
|
|
@@ -244,57 +291,101 @@ var r = "voltro", i = (e) => e.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-"
|
|
|
244
291
|
namespace: t.namespace
|
|
245
292
|
}
|
|
246
293
|
};
|
|
247
|
-
},
|
|
248
|
-
for (let t of e) if (
|
|
294
|
+
}, D = (e) => {
|
|
295
|
+
for (let t of e) if (O(t)) return t;
|
|
249
296
|
return null;
|
|
250
|
-
},
|
|
297
|
+
}, O = (e) => typeof e.broadcast == "object" && e.broadcast !== null && typeof e.broadcast.provider == "object", k = ({ origin: e, ...t }) => t, A = (e) => Math.min(500 * 2 ** Math.min(Math.max(e, 1) - 1, 5), 1e4), j = {
|
|
251
298
|
info: () => {},
|
|
252
299
|
warn: () => {},
|
|
253
300
|
debug: () => {}
|
|
254
|
-
},
|
|
255
|
-
let { store: n, provider: r, replicaId: i } = t, a = t.logger ??
|
|
256
|
-
|
|
301
|
+
}, M = async (t) => {
|
|
302
|
+
let { store: n, provider: r, replicaId: i } = t, a = t.logger ?? j, o = t.channel ?? u, s = !1, c, l = !1, d = 0, f = /* @__PURE__ */ new Map(), p = `${Date.now().toString(36)}.${Math.random().toString(36).slice(2, 10)}`, m = (e) => {
|
|
303
|
+
a.warn(`broadcast: ${e.reason} — refreshing live queries, which is safe because a query is idempotent`, {
|
|
304
|
+
...e.origin === void 0 ? {} : { origin: e.origin },
|
|
305
|
+
...e.missed === void 0 ? {} : { missed: e.missed }
|
|
306
|
+
});
|
|
307
|
+
try {
|
|
308
|
+
t.onGap?.(e);
|
|
309
|
+
} catch (e) {
|
|
310
|
+
a.warn("broadcast: gap recovery threw", { err: e });
|
|
311
|
+
}
|
|
312
|
+
}, h = (e) => {
|
|
313
|
+
let t;
|
|
257
314
|
try {
|
|
258
|
-
|
|
315
|
+
t = JSON.parse(e);
|
|
259
316
|
} catch {
|
|
260
317
|
a.warn("broadcast: bad payload (not JSON)", { channel: o });
|
|
261
318
|
return;
|
|
262
319
|
}
|
|
263
|
-
if (
|
|
264
|
-
if (typeof
|
|
265
|
-
let e = f.get(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
(e === void 0 || r.n > e) && f.set(r.origin, r.n);
|
|
320
|
+
if (t.origin === i || !t.event || typeof t.event != "object") return;
|
|
321
|
+
if (typeof t.n == "number" && Number.isFinite(t.n)) {
|
|
322
|
+
let e = t.epoch ?? "", n = f.get(t.origin);
|
|
323
|
+
n === void 0 ? f.set(t.origin, {
|
|
324
|
+
epoch: e,
|
|
325
|
+
n: t.n
|
|
326
|
+
}) : n.epoch === e ? (t.n > n.n + 1 && m({
|
|
327
|
+
reason: `missed ${t.n - n.n - 1} change(s) from ${t.origin}`,
|
|
328
|
+
origin: t.origin,
|
|
329
|
+
missed: t.n - n.n - 1
|
|
330
|
+
}), t.n > n.n && f.set(t.origin, {
|
|
331
|
+
epoch: e,
|
|
332
|
+
n: t.n
|
|
333
|
+
})) : f.set(t.origin, {
|
|
334
|
+
epoch: e,
|
|
335
|
+
n: t.n
|
|
336
|
+
});
|
|
281
337
|
}
|
|
282
|
-
let
|
|
283
|
-
s = !0, c =
|
|
338
|
+
let r = k(t.event);
|
|
339
|
+
s = !0, c = r;
|
|
284
340
|
try {
|
|
285
|
-
n.injectExternalChange(
|
|
341
|
+
n.injectExternalChange(r);
|
|
286
342
|
} catch (e) {
|
|
287
343
|
a.warn("broadcast: inject failed", { err: e });
|
|
288
344
|
} finally {
|
|
289
345
|
s = !1, c = void 0;
|
|
290
346
|
}
|
|
291
|
-
}
|
|
292
|
-
|
|
347
|
+
}, g, _ = !1, v = 0, y = async () => {
|
|
348
|
+
try {
|
|
349
|
+
return g = await e.runPromise(r.subscribe(o, h)), !0;
|
|
350
|
+
} catch (e) {
|
|
351
|
+
return v += 1, v === 1 && a.warn("broadcast: could not subscribe — this replica will not receive other replicas' changes yet. Retrying in the background; local reactivity is unaffected.", {
|
|
352
|
+
provider: r.name,
|
|
353
|
+
channel: o,
|
|
354
|
+
err: e
|
|
355
|
+
}), !1;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
await y() || (async () => {
|
|
359
|
+
let e = Date.now();
|
|
360
|
+
for (; !_ && g === void 0;) {
|
|
361
|
+
if (await new Promise((e) => setTimeout(e, A(v))), _) return;
|
|
362
|
+
if (await y()) {
|
|
363
|
+
a.info("broadcast: subscribed", {
|
|
364
|
+
provider: r.name,
|
|
365
|
+
channel: o,
|
|
366
|
+
attempts: v
|
|
367
|
+
}), m({ reason: `this replica was not subscribed to '${o}' for ${Math.round((Date.now() - e) / 1e3)}s` });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
})();
|
|
372
|
+
let b = r.onTransportEvent?.((e) => {
|
|
373
|
+
if (e.kind === "disconnected") {
|
|
374
|
+
a.warn("broadcast: transport disconnected — cross-replica fan-out is down until it re-dials", {
|
|
375
|
+
provider: r.name,
|
|
376
|
+
...e.detail === void 0 ? {} : { detail: e.detail }
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
m({ reason: `the ${r.name} transport reconnected after an outage` });
|
|
381
|
+
}), x = n.onChange((t) => {
|
|
382
|
+
if (t.origin === "injected" || s && t === c) return;
|
|
293
383
|
d += 1;
|
|
294
384
|
let n = {
|
|
295
385
|
origin: i,
|
|
296
386
|
event: t,
|
|
297
|
-
n: d
|
|
387
|
+
n: d,
|
|
388
|
+
epoch: p
|
|
298
389
|
};
|
|
299
390
|
e.runPromise(r.publish(o, JSON.stringify(n)).pipe(e.tap(() => e.sync(() => {
|
|
300
391
|
l = !1;
|
|
@@ -306,21 +397,19 @@ var r = "voltro", i = (e) => e.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-"
|
|
|
306
397
|
}));
|
|
307
398
|
}))));
|
|
308
399
|
});
|
|
309
|
-
a.info("broadcast bus attached", {
|
|
400
|
+
return a.info("broadcast bus attached", {
|
|
310
401
|
provider: r.name,
|
|
311
402
|
channel: o,
|
|
312
403
|
replicaId: i
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (!h) {
|
|
317
|
-
h = !0, m();
|
|
404
|
+
}), { close: async () => {
|
|
405
|
+
if (!_) {
|
|
406
|
+
_ = !0, x(), b?.();
|
|
318
407
|
try {
|
|
319
|
-
|
|
408
|
+
g?.();
|
|
320
409
|
} catch {}
|
|
321
410
|
await e.runPromise(r.close());
|
|
322
411
|
}
|
|
323
412
|
} };
|
|
324
413
|
};
|
|
325
414
|
//#endregion
|
|
326
|
-
export { u as BROADCAST_CHANNEL, l as BroadcastError, r as DEFAULT_BROADCAST_NAMESPACE,
|
|
415
|
+
export { u as BROADCAST_CHANNEL, l as BroadcastError, r as DEFAULT_BROADCAST_NAMESPACE, M as attachBroadcastBus, E as broadcastPlugin, s as channelFor, a as describeNamespaceResolution, c as eventChannelFor, D as getBroadcastPlugin, _ as memoryProvider, x as natsProvider, v as redisProvider, g as resetMemoryBus, o as resolveBroadcastNamespace, C as resolveBroadcastProvider, A as subscribeRetryDelayMs };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-broadcast",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.0",
|
|
4
4
|
"description": "Cross-replica reactivity — a pub/sub message bus (Redis / NATS / memory) that fans out app-mutation ChangeEvents to every replica behind a load balancer. ADDITIVE to inline emit: local reactivity survives a broker outage; cross-replica degrades gracefully. Closes the single-instance gap for every non-postgres dialect (postgres keeps native LISTEN/NOTIFY).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"node": ">=24.0.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@voltro/database": "0.
|
|
37
|
-
"@voltro/protocol": "0.
|
|
36
|
+
"@voltro/database": "0.56.0",
|
|
37
|
+
"@voltro/protocol": "0.56.0"
|
|
38
38
|
},
|
|
39
39
|
"optionalDependencies": {
|
|
40
40
|
"@nats-io/transport-node": "^3.4.0",
|