@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.
@@ -0,0 +1,166 @@
1
+ import { BroadcastError as e } from "./types.js";
2
+ import { Effect as t } from "effect";
3
+ //#region src/providers.ts
4
+ var n = (t, n, r) => new e({
5
+ provider: t,
6
+ message: n,
7
+ transient: !0,
8
+ ...r === void 0 ? {} : { cause: r }
9
+ }), r = (t, n, r) => new e({
10
+ provider: t,
11
+ message: n,
12
+ transient: !1,
13
+ ...r === void 0 ? {} : { cause: r }
14
+ }), i = () => {
15
+ let e = /* @__PURE__ */ new Set();
16
+ return {
17
+ subscribe: (t) => (e.add(t), () => {
18
+ e.delete(t);
19
+ }),
20
+ emit: (t) => {
21
+ for (let n of e) try {
22
+ n(t);
23
+ } catch {}
24
+ }
25
+ };
26
+ }, a = /* @__PURE__ */ new Map(), o = (e) => {
27
+ let t = a.get(e);
28
+ return t || (t = { handlers: /* @__PURE__ */ new Map() }, a.set(e, t)), t;
29
+ }, s = (e) => {
30
+ a.delete(e);
31
+ }, c = (e = `default-${Math.random().toString(36).slice(2)}`) => {
32
+ let n = /* @__PURE__ */ new Set();
33
+ return {
34
+ name: "memory",
35
+ publish: (n, r) => t.sync(() => {
36
+ let t = a.get(e)?.handlers.get(n);
37
+ if (t) for (let e of t) try {
38
+ e(r);
39
+ } catch {}
40
+ }),
41
+ subscribe: (r, i) => t.sync(() => {
42
+ let t = o(e), s = t.handlers.get(r) ?? /* @__PURE__ */ new Set(), c = (e) => i(e);
43
+ s.add(c), t.handlers.set(r, s);
44
+ let l = () => {
45
+ n.delete(l), s.delete(c), s.size === 0 && t.handlers.get(r) === s && t.handlers.delete(r), t.handlers.size === 0 && a.get(e) === t && a.delete(e);
46
+ };
47
+ return n.add(l), l;
48
+ }),
49
+ close: () => t.sync(() => {
50
+ for (let e of n) e();
51
+ })
52
+ };
53
+ }, l = (e) => {
54
+ let a = null, o = i(), s = !0, c = () => a || (a = (async () => {
55
+ let t;
56
+ if (e.client) t = e.client;
57
+ else {
58
+ let n;
59
+ try {
60
+ n = (await import("ioredis")).default;
61
+ } catch (e) {
62
+ throw r("redis", "the 'redis' broadcast provider requires the 'ioredis' optional dependency. Install it to enable it.", e);
63
+ }
64
+ t = new n(e.url);
65
+ }
66
+ let n = t.duplicate();
67
+ return t.on("error", () => {}), n.on("error", () => {}), n.on("reconnecting", () => {
68
+ s && (s = !1, o.emit({
69
+ kind: "disconnected",
70
+ detail: "ioredis is re-dialling"
71
+ }));
72
+ }), n.on("ready", () => {
73
+ s || (s = !0, o.emit({ kind: "reconnected" }));
74
+ }), {
75
+ pub: t,
76
+ sub: n
77
+ };
78
+ })().catch((e) => {
79
+ throw a = null, e;
80
+ }), a);
81
+ return {
82
+ name: "redis",
83
+ onTransportEvent: o.subscribe,
84
+ publish: (e, r) => t.tryPromise({
85
+ try: async () => {
86
+ let { pub: t } = await c();
87
+ await t.publish(e, r);
88
+ },
89
+ catch: (e) => n("redis", `publish failed: ${String(e?.message ?? e)}`, e)
90
+ }),
91
+ subscribe: (e, r) => t.tryPromise({
92
+ try: async () => {
93
+ let { sub: t } = await c(), n = (t, n) => {
94
+ t === e && r(n);
95
+ };
96
+ return t.on("message", n), await t.subscribe(e), () => {
97
+ t.off("message", n), t.unsubscribe(e).catch(() => void 0);
98
+ };
99
+ },
100
+ catch: (e) => n("redis", `subscribe failed: ${String(e?.message ?? e)}`, e)
101
+ }),
102
+ close: () => t.promise(async () => {
103
+ if (a) try {
104
+ let { pub: t, sub: n } = await a;
105
+ await n.quit(), e.client || await t.quit();
106
+ } catch {}
107
+ })
108
+ };
109
+ }, u = new TextEncoder(), d = new TextDecoder(), f = (e) => {
110
+ let a = null, o = i(), s = !0, c = (e) => {
111
+ (async () => {
112
+ try {
113
+ for await (let t of e.status()) t.type === "disconnect" && s ? (s = !1, o.emit({
114
+ kind: "disconnected",
115
+ detail: "nats disconnected"
116
+ })) : t.type === "reconnect" && !s && (s = !0, o.emit({ kind: "reconnected" }));
117
+ } catch {}
118
+ })();
119
+ }, l = () => import("@nats-io/transport-node").catch((e) => {
120
+ throw r("nats", "the 'nats' broadcast provider requires the '@nats-io/transport-node' optional dependency. Install it to enable it.", e);
121
+ }), f = () => a || (a = (async () => {
122
+ if (e.connection) return e.connection;
123
+ let t = await (await l()).connect({
124
+ servers: e.url,
125
+ maxReconnectAttempts: -1,
126
+ waitOnFirstConnect: !0,
127
+ reconnectTimeWait: 2e3
128
+ });
129
+ return c(t), t;
130
+ })().catch((e) => {
131
+ throw a = null, e;
132
+ }), a);
133
+ return {
134
+ name: "nats",
135
+ onTransportEvent: o.subscribe,
136
+ publish: (e, r) => t.tryPromise({
137
+ try: async () => {
138
+ (await f()).publish(e, u.encode(r));
139
+ },
140
+ catch: (e) => n("nats", `publish failed: ${String(e?.message ?? e)}`, e)
141
+ }),
142
+ subscribe: (e, r) => t.tryPromise({
143
+ try: async () => {
144
+ let t = (await f()).subscribe(e), n = !1;
145
+ return (async () => {
146
+ for await (let e of t) r(d.decode(e.data));
147
+ !n && s && (s = !1, o.emit({
148
+ kind: "disconnected",
149
+ detail: `the nats subscription to '${e}' ended while the connection was up`
150
+ }));
151
+ })(), () => {
152
+ n = !0, t.unsubscribe();
153
+ };
154
+ },
155
+ catch: (e) => n("nats", `subscribe failed: ${String(e?.message ?? e)}`, e)
156
+ }),
157
+ close: () => t.promise(async () => {
158
+ if (a) try {
159
+ let t = await a;
160
+ e.connection || await t.drain();
161
+ } catch {}
162
+ })
163
+ };
164
+ };
165
+ //#endregion
166
+ export { c as memoryProvider, f as natsProvider, l as redisProvider, s as resetMemoryBus };
@@ -0,0 +1,64 @@
1
+ import { Effect as e } from "effect";
2
+ //#region src/publishQueue.ts
3
+ var t = (t, n, r, i, a = {}, o = () => {}) => {
4
+ let s = a.maxMessages ?? 1024, c = a.maxBytes ?? 8388608, l = a.timeoutMs ?? 5e3, u = a.shutdownTimeoutMs ?? 1e3;
5
+ for (let [e, t] of Object.entries({
6
+ maxMessages: s,
7
+ maxBytes: c,
8
+ timeoutMs: l,
9
+ shutdownTimeoutMs: u
10
+ })) if (!Number.isSafeInteger(t) || t < 1) throw Error(`broadcast ${e} must be a positive safe integer`);
11
+ let d = [], f = 0, p = 0, m = !1, h = 0, g = !1, _ = !1, v, y, b = !1, x, S = /* @__PURE__ */ new Set(), C = () => {
12
+ for (let e of S) e();
13
+ S.clear();
14
+ }, w = (e) => {
15
+ m || i(e), m = !0;
16
+ }, T = () => {
17
+ if (g || b || v !== void 0) return;
18
+ let i = d[f];
19
+ if (!i && !m) {
20
+ C();
21
+ return;
22
+ }
23
+ let a = h, s = i?.payload ?? r(a);
24
+ i && (d[f] = void 0, f++, f * 2 >= d.length && (d = d.slice(f), f = 0)), x = new AbortController(), b = !0;
25
+ let c = setTimeout(() => {
26
+ w("publish timed out; peers need a resync");
27
+ }, l);
28
+ y = e.runPromise(t.publish(n, s), { signal: x.signal }).then(() => {
29
+ g || o(), !i && h === a && (m = !1);
30
+ }, () => {
31
+ g || (w("publish failed — cross-replica fan-out degraded, local reactivity unaffected; peers need a resync"), v = setTimeout(() => {
32
+ v = void 0, T();
33
+ }, 500));
34
+ }).finally(() => {
35
+ clearTimeout(c), i && (p -= i.bytes), y = void 0, b = !1, x = void 0, g ? C() : T();
36
+ });
37
+ };
38
+ return {
39
+ enqueue: (e, t) => {
40
+ if (_ || g) return;
41
+ if (h = t, m || d.length - f + +!!b >= s) {
42
+ w("publish queue full; peers need a resync"), T();
43
+ return;
44
+ }
45
+ let n = e(), r = Buffer.byteLength(n, "utf8");
46
+ p + r > c ? (w("publish byte budget exceeded; peers need a resync"), T()) : (p += r, d.push({
47
+ payload: n,
48
+ bytes: r
49
+ }), T());
50
+ },
51
+ close: async () => {
52
+ _ = !0, v !== void 0 && (clearTimeout(v), v = void 0), T();
53
+ let e;
54
+ (y || d.length > f || m) && await new Promise((t) => {
55
+ let n = () => {
56
+ S.delete(n), t();
57
+ };
58
+ S.add(n), e = setTimeout(n, u);
59
+ }), e !== void 0 && clearTimeout(e), g = !0, (y || d.length > f || m) && i("shutdown deadline reached; unsent changes require peer resync"), d = [], f = 0, v !== void 0 && clearTimeout(v), x?.abort(), y && await Promise.race([y, new Promise((e) => setImmediate(e))]), p = 0, C();
60
+ }
61
+ };
62
+ };
63
+ //#endregion
64
+ export { t as createPublishQueue };
@@ -0,0 +1,53 @@
1
+ import { memoryProvider as e, natsProvider as t, redisProvider as n } from "./providers.js";
2
+ //#region src/resolve.ts
3
+ var r = (e, t) => t[`${e.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_REDIS_URL`], i = (i, a) => {
4
+ if (i.provider && typeof i.provider == "object") {
5
+ let e = i.provider;
6
+ return {
7
+ provider: e,
8
+ url: i.url ?? null,
9
+ crossReplica: e.name !== "memory"
10
+ };
11
+ }
12
+ let o = i.url ?? a.BROADCAST_URL ?? null, s = r(i.connection ?? "broadcast", a), c = (i.provider ?? a.BROADCAST_PROVIDER)?.toLowerCase() ?? (o?.startsWith("nats://") ? "nats" : o?.startsWith("redis://") || o?.startsWith("rediss://") || s || a.REDIS_URL ? "redis" : void 0), l = (i.provider ?? a.BROADCAST_PROVIDER)?.toLowerCase();
13
+ if (l !== void 0 && l !== "redis" && l !== "nats" && l !== "memory") throw Error(`broadcastPlugin: unknown provider "${l}".\n Valid: 'redis', 'nats', 'memory'.
14
+ 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.`);
15
+ if (o !== null && o !== "" && !/^(redis|rediss|nats):\/\//.test(o)) throw Error(`broadcastPlugin: url "${o}" names no provider we support.\n Expected a redis://, rediss:// or nats:// url.`);
16
+ switch (c) {
17
+ case "redis": {
18
+ let t = o ?? s ?? a.REDIS_URL;
19
+ if (!t) {
20
+ if (l === "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.");
21
+ return {
22
+ provider: e(),
23
+ url: null,
24
+ crossReplica: !1
25
+ };
26
+ }
27
+ return {
28
+ provider: n({ url: t }),
29
+ url: t,
30
+ crossReplica: !0
31
+ };
32
+ }
33
+ case "nats": {
34
+ let n = o ?? a.NATS_URL;
35
+ return n ? {
36
+ provider: t({ url: n }),
37
+ url: n,
38
+ crossReplica: !0
39
+ } : {
40
+ provider: e(),
41
+ url: null,
42
+ crossReplica: !1
43
+ };
44
+ }
45
+ default: return {
46
+ provider: e(),
47
+ url: null,
48
+ crossReplica: !1
49
+ };
50
+ }
51
+ };
52
+ //#endregion
53
+ export { i as resolveBroadcastProvider };
package/dist/types.js ADDED
@@ -0,0 +1,11 @@
1
+ import { DEFAULT_BROADCAST_NAMESPACE as e, channelFor as t } from "./namespace.js";
2
+ import { Schema as n } from "effect";
3
+ //#region src/types.ts
4
+ var r = class extends n.TaggedError()("BroadcastError", {
5
+ provider: n.String,
6
+ message: n.String,
7
+ transient: n.Boolean,
8
+ cause: n.optional(n.Unknown)
9
+ }) {}, i = t(e, "changes");
10
+ //#endregion
11
+ export { i as BROADCAST_CHANNEL, r as BroadcastError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-broadcast",
3
- "version": "0.84.0",
3
+ "version": "0.86.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,15 +33,15 @@
33
33
  "node": ">=24.0.0"
34
34
  },
35
35
  "dependencies": {
36
- "@voltro/database": "0.84.0",
37
- "@voltro/protocol": "0.84.0"
36
+ "@voltro/database": "0.86.0",
37
+ "@voltro/protocol": "0.86.0"
38
38
  },
39
39
  "optionalDependencies": {
40
40
  "@nats-io/transport-node": "^3.4.0",
41
41
  "ioredis": "^5.11.1"
42
42
  },
43
43
  "peerDependencies": {
44
- "effect": "^3.22.2"
44
+ "effect": "4.0.1"
45
45
  },
46
46
  "publishConfig": {
47
47
  "access": "public"