@voltro/plugin-broadcast 0.84.0 → 0.86.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.
@@ -1,70 +1,14 @@
1
1
  # Third-Party Notices — @voltro/plugin-broadcast
2
2
 
3
- This package depends on the open-source software listed below. Each is the
3
+ This package uses the open-source software listed below. Each is the
4
4
  property of its respective copyright holders and is used under the terms of
5
5
  its license. This file is provided for attribution; it grants no rights in
6
6
  @voltro/plugin-broadcast itself, which is proprietary (see LICENSE).
7
7
 
8
- Generated from the resolved runtime dependency closure (21 packages).
8
+ Generated from the resolved runtime dependency closure (18 packages).
9
9
 
10
10
  ---
11
11
 
12
- ## @effect/experimental@0.61.1
13
-
14
- License: MIT
15
-
16
- ```
17
- MIT License
18
-
19
- Copyright (c) 2023-present The Contributors
20
-
21
- Permission is hereby granted, free of charge, to any person obtaining a copy
22
- of this software and associated documentation files (the "Software"), to deal
23
- in the Software without restriction, including without limitation the rights
24
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
25
- copies of the Software, and to permit persons to whom the Software is
26
- furnished to do so, subject to the following conditions:
27
-
28
- The above copyright notice and this permission notice shall be included in all
29
- copies or substantial portions of the Software.
30
-
31
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37
- SOFTWARE.
38
- ```
39
-
40
- ## @effect/sql@0.52.1
41
-
42
- License: MIT
43
-
44
- ```
45
- MIT License
46
-
47
- Copyright (c) 2023-present The Contributors
48
-
49
- Permission is hereby granted, free of charge, to any person obtaining a copy
50
- of this software and associated documentation files (the "Software"), to deal
51
- in the Software without restriction, including without limitation the rights
52
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
53
- copies of the Software, and to permit persons to whom the Software is
54
- furnished to do so, subject to the following conditions:
55
-
56
- The above copyright notice and this permission notice shall be included in all
57
- copies or substantial portions of the Software.
58
-
59
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
60
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
61
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
62
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
63
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
64
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
65
- SOFTWARE.
66
- ```
67
-
68
12
  ## @ioredis/commands@1.10.0
69
13
 
70
14
  License: MIT
@@ -1236,34 +1180,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1236
1180
  SOFTWARE.
1237
1181
  ```
1238
1182
 
1239
- ## layerr@3.0.0
1240
-
1241
- License: MIT
1242
-
1243
- ```
1244
- MIT License
1245
-
1246
- Copyright (c) 2020 Perry Mitchell
1247
-
1248
- Permission is hereby granted, free of charge, to any person obtaining a copy
1249
- of this software and associated documentation files (the "Software"), to deal
1250
- in the Software without restriction, including without limitation the rights
1251
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1252
- copies of the Software, and to permit persons to whom the Software is
1253
- furnished to do so, subject to the following conditions:
1254
-
1255
- The above copyright notice and this permission notice shall be included in all
1256
- copies or substantial portions of the Software.
1257
-
1258
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1259
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1260
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1261
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1262
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1263
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1264
- SOFTWARE.
1265
- ```
1266
-
1267
1183
  ## ms@2.1.3
1268
1184
 
1269
1185
  License: MIT
@@ -1616,14 +1532,14 @@ Apache License
1616
1532
  limitations under the License.
1617
1533
  ```
1618
1534
 
1619
- ## ulidx@2.4.1
1535
+ ## ulid@3.0.2
1620
1536
 
1621
1537
  License: MIT
1622
1538
 
1623
1539
  ```
1624
- MIT License
1540
+ The MIT License (MIT)
1625
1541
 
1626
- Copyright (c) 2021 Perry Mitchell
1542
+ Copyright (c) 2017 Alizain Feerasta
1627
1543
 
1628
1544
  Permission is hereby granted, free of charge, to any person obtaining a copy
1629
1545
  of this software and associated documentation files (the "Software"), to deal
package/dist/bus.js ADDED
@@ -0,0 +1,181 @@
1
+ import { BROADCAST_CHANNEL as e } from "./types.js";
2
+ import { createPublishQueue as t } from "./publishQueue.js";
3
+ import { Effect as n, Metric as r } from "effect";
4
+ //#region src/bus.ts
5
+ var i = ({ origin: e, ...t }) => t, a = (e) => Math.min(500 * 2 ** Math.min(Math.max(e, 1) - 1, 5), 1e4), o = {
6
+ info: () => {},
7
+ warn: () => {},
8
+ debug: () => {}
9
+ }, s = async (s) => {
10
+ let { store: c, provider: l, replicaId: u } = s, d = s.logger ?? o, f = s.channel ?? e, p = !1, m, h = 0, g = /* @__PURE__ */ new Map(), _ = `${Date.now().toString(36)}.${Math.random().toString(36).slice(2, 10)}`, v = r.gauge("voltro_broadcast_connected").pipe(r.withAttributes({ provider: l.name })), y = r.counter("voltro_broadcast_gaps_total").pipe(r.withAttributes({ provider: l.name })), b = r.counter("voltro_broadcast_missed_total").pipe(r.withAttributes({ provider: l.name })), x = r.counter("voltro_broadcast_reconnects_total").pipe(r.withAttributes({ provider: l.name })), S = {
11
+ connected: !1,
12
+ lastReceivedAt: null,
13
+ lastPublishedAt: null,
14
+ reconnects: 0,
15
+ gaps: 0,
16
+ missed: 0
17
+ }, C = (e) => {
18
+ S.connected = e, n.runSync(r.update(v, +!!e));
19
+ };
20
+ C(!1);
21
+ let w = (e) => {
22
+ S.gaps += 1, n.runSync(r.update(y, 1)), e.missed !== void 0 && e.missed > 0 && (S.missed += e.missed, n.runSync(r.update(b, e.missed))), d.warn(`broadcast: ${e.reason} — refreshing live queries, which is safe because a query is idempotent`, {
23
+ ...e.origin === void 0 ? {} : { origin: e.origin },
24
+ ...e.missed === void 0 ? {} : { missed: e.missed }
25
+ });
26
+ try {
27
+ s.onGap?.(e);
28
+ } catch (e) {
29
+ d.warn("broadcast: gap recovery threw", { err: e });
30
+ }
31
+ }, T = (e) => {
32
+ if (O) return;
33
+ S.lastReceivedAt = Date.now();
34
+ let t;
35
+ try {
36
+ t = JSON.parse(e);
37
+ } catch {
38
+ d.warn("broadcast: bad payload (not JSON)", { channel: f });
39
+ return;
40
+ }
41
+ if (t.origin === u || !t.event || typeof t.event != "object") return;
42
+ let n = !1;
43
+ if (typeof t.n == "number" && Number.isFinite(t.n)) {
44
+ let e = t.epoch ?? "", r = g.get(t.origin);
45
+ if (r === void 0) g.set(t.origin, {
46
+ epoch: e,
47
+ n: t.n
48
+ });
49
+ else if (r.epoch !== e) g.set(t.origin, {
50
+ epoch: e,
51
+ n: t.n
52
+ });
53
+ else {
54
+ let i = t.n - r.n - (t.resync === !0 ? 0 : 1);
55
+ i > 0 && (n = !0, w({
56
+ reason: `missed ${i} change(s) from ${t.origin}`,
57
+ origin: t.origin,
58
+ missed: i
59
+ })), t.n > r.n && g.set(t.origin, {
60
+ epoch: e,
61
+ n: t.n
62
+ });
63
+ }
64
+ }
65
+ if (t.resync === !0) {
66
+ n || w({
67
+ reason: `the ${t.origin} publisher lost changes and requested a resync`,
68
+ origin: t.origin
69
+ });
70
+ return;
71
+ }
72
+ let r = i(t.event);
73
+ p = !0, m = r;
74
+ try {
75
+ c.injectExternalChange(r);
76
+ } catch (e) {
77
+ d.warn("broadcast: inject failed", { err: e });
78
+ } finally {
79
+ p = !1, m = void 0;
80
+ }
81
+ }, E = t(l, f, (e) => JSON.stringify({
82
+ origin: u,
83
+ epoch: _,
84
+ n: e,
85
+ resync: !0,
86
+ event: {
87
+ table: "",
88
+ op: "update",
89
+ old: null,
90
+ new: null
91
+ }
92
+ }), (e) => {
93
+ d.warn(`broadcast: ${e}`, {
94
+ provider: l.name,
95
+ channel: f
96
+ });
97
+ }, s.publishQueue, () => {
98
+ S.lastPublishedAt = Date.now();
99
+ }), D, O = !1, k = 0, A = async () => {
100
+ try {
101
+ return D = await n.runPromise(l.subscribe(f, T)), O ? (D(), D = void 0, !1) : (C(!0), !0);
102
+ } catch (e) {
103
+ return k += 1, k === 1 && d.warn("broadcast: could not subscribe — this replica will not receive other replicas' changes yet. Retrying in the background; local reactivity is unaffected.", {
104
+ provider: l.name,
105
+ channel: f,
106
+ err: e
107
+ }), !1;
108
+ }
109
+ };
110
+ await A() || (async () => {
111
+ let e = Date.now();
112
+ for (; !O && D === void 0;) {
113
+ if (await new Promise((e) => setTimeout(e, a(k))), O) return;
114
+ if (await A()) {
115
+ d.info("broadcast: subscribed", {
116
+ provider: l.name,
117
+ channel: f,
118
+ attempts: k
119
+ }), w({ reason: `this replica was not subscribed to '${f}' for ${Math.round((Date.now() - e) / 1e3)}s` });
120
+ return;
121
+ }
122
+ }
123
+ })();
124
+ let j = l.onTransportEvent?.((e) => {
125
+ O || (e.kind === "disconnected" ? (C(!1), d.warn("broadcast: transport disconnected — cross-replica fan-out is down until it re-dials", {
126
+ provider: l.name,
127
+ ...e.detail === void 0 ? {} : { detail: e.detail }
128
+ })) : (C(!0), S.reconnects += 1, n.runSync(r.update(x, 1)), d.warn("broadcast transport reconnected", {
129
+ provider: l.name,
130
+ channel: f,
131
+ replicaId: u,
132
+ reconnects: S.reconnects,
133
+ lastReceivedAt: S.lastReceivedAt
134
+ }), w({ reason: `the ${l.name} transport reconnected after an outage` })));
135
+ }), M = c.onChange((e) => {
136
+ if (e.origin === "injected" || p && e === m) return;
137
+ h += 1;
138
+ let t = {
139
+ origin: u,
140
+ event: e,
141
+ n: h,
142
+ epoch: _
143
+ };
144
+ E.enqueue(() => JSON.stringify(t), h);
145
+ });
146
+ d.info("broadcast bus attached", {
147
+ provider: l.name,
148
+ channel: f,
149
+ replicaId: u
150
+ });
151
+ let N;
152
+ return {
153
+ health: () => ({
154
+ provider: l.name,
155
+ channel: f,
156
+ replicaId: u,
157
+ ...S
158
+ }),
159
+ close: () => (N ??= (async () => {
160
+ O = !0, M(), j?.(), C(!1);
161
+ try {
162
+ D?.();
163
+ } catch {}
164
+ await E.close();
165
+ let e = new AbortController(), t, r = n.runPromise(l.close(), { signal: e.signal }).catch((e) => {
166
+ d.warn("broadcast: provider close failed", { error: e });
167
+ });
168
+ try {
169
+ await Promise.race([r, new Promise((n) => {
170
+ t = setTimeout(() => {
171
+ d.warn("broadcast: provider shutdown timed out"), e.abort(), n();
172
+ }, s.publishQueue?.shutdownTimeoutMs ?? 1e3);
173
+ })]);
174
+ } finally {
175
+ t !== void 0 && clearTimeout(t);
176
+ }
177
+ })(), N)
178
+ };
179
+ };
180
+ //#endregion
181
+ export { s as attachBroadcastBus, a as subscribeRetryDelayMs, i as withoutWireOrigin };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { ChangeEvent } from '@voltro/database';
2
2
  import { Effect } from 'effect';
3
3
  import { Schema } from 'effect';
4
4
  import { VoltroPlugin } from '@voltro/protocol';
5
+ import { YieldableError } from 'effect/Cause';
5
6
 
6
7
  /**
7
8
  * Wire a store to a broadcast provider. Publishes local changes outward
@@ -40,6 +41,9 @@ export declare interface AttachBroadcastBusOptions {
40
41
  * embedder) should still say what it lost.
41
42
  */
42
43
  readonly onGap?: (gap: BroadcastGap) => void;
44
+ /** Bounded outbound buffering and broker/shutdown deadlines. Defaults:
45
+ * 1024 messages, 8 MiB, 5s publish, 1s shutdown. Overflow signals peer resync. */
46
+ readonly publishQueue?: BroadcastPublishQueueOptions;
43
47
  }
44
48
 
45
49
  /**
@@ -76,6 +80,9 @@ export declare type BroadcastChannelKind = 'changes' | 'events' | 'members' | 'p
76
80
  * into every OTHER replica's store emitter.
77
81
  */
78
82
  export declare interface BroadcastEnvelope {
83
+ /** Control delivery: refresh live queries rather than inject `event`.
84
+ * Sent after outbound loss, including when no later mutation occurs. */
85
+ readonly resync?: true;
79
86
  readonly origin: string;
80
87
  readonly event: ChangeEvent;
81
88
  /**
@@ -132,14 +139,12 @@ export declare interface BroadcastEnvelope {
132
139
  export declare class BroadcastError extends BroadcastError_base {
133
140
  }
134
141
 
135
- declare const BroadcastError_base: Schema.TaggedErrorClass<BroadcastError, "BroadcastError", {
136
- readonly _tag: Schema.tag<"BroadcastError">;
137
- } & {
138
- provider: typeof Schema.String;
139
- message: typeof Schema.String;
140
- transient: typeof Schema.Boolean;
141
- cause: Schema.optional<typeof Schema.Unknown>;
142
- }>;
142
+ declare const BroadcastError_base: Schema.Class<BroadcastError, Schema.TaggedStruct<"BroadcastError", {
143
+ readonly provider: Schema.String;
144
+ readonly message: Schema.String;
145
+ readonly transient: Schema.Boolean;
146
+ readonly cause: Schema.optional<Schema.Unknown>;
147
+ }>, YieldableError>;
143
148
 
144
149
  /**
145
150
  * A proven — or presumed — hole in what this replica received.
@@ -274,6 +279,13 @@ export declare interface BroadcastProvider {
274
279
  /** Provider id understood by `resolveBroadcastProvider`. */
275
280
  export declare type BroadcastProviderName = 'redis' | 'nats' | 'memory';
276
281
 
282
+ export declare interface BroadcastPublishQueueOptions {
283
+ readonly maxMessages?: number;
284
+ readonly maxBytes?: number;
285
+ readonly timeoutMs?: number;
286
+ readonly shutdownTimeoutMs?: number;
287
+ }
288
+
277
289
  /** The minimal store surface the bus drives — a subset of `DataStore` so
278
290
  * the bus stays decoupled from the concrete store classes. */
279
291
  export declare interface BroadcastStore {