@voltro/plugin-presence 0.87.0 → 0.88.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 +169 -6
- package/THIRD-PARTY-NOTICES.md +6 -1349
- package/dist/errors.js +1 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +67 -53
- package/dist/rpc.js +1 -2
- package/dist/tracker.js +1 -2
- package/dist/trackerBus.js +45 -19
- package/dist/web.js +1 -2
- package/package.json +5 -5
package/dist/errors.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Schema as e } from "effect";
|
|
2
|
-
|
|
2
|
+
// src/errors.ts
|
|
3
3
|
var t = class extends e.TaggedError()("PresenceKeyMissing", { channel: e.String }) {}, n = class extends e.TaggedError()("PresenceStoreUnavailable", {}) {};
|
|
4
|
-
//#endregion
|
|
5
4
|
export { t as PresenceKeyMissing, n as PresenceStoreUnavailable };
|
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,9 @@ export declare interface AttachPresenceBusOptions {
|
|
|
35
35
|
*/
|
|
36
36
|
readonly onRosterChanged?: (() => void) | undefined;
|
|
37
37
|
readonly now?: () => number;
|
|
38
|
+
/** Called when binding settles: acquired, local-only, or failed. Pending
|
|
39
|
+
* acquisition and a closed handle never report a usable transport. */
|
|
40
|
+
readonly onTransportSettled?: (connected: boolean, cause?: unknown) => void;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
/**
|
|
@@ -51,7 +54,7 @@ export declare const PRESENCE_BROADCAST_CHANNEL = "voltro:presence";
|
|
|
51
54
|
export declare interface PresenceBusHandle {
|
|
52
55
|
readonly tracker: PresenceTracker;
|
|
53
56
|
/**
|
|
54
|
-
* Whether
|
|
57
|
+
* Whether the transport's subscription has been acquired and is still owned.
|
|
55
58
|
*
|
|
56
59
|
* Exposed so the plugin can WARN at boot. Without one every replica keeps a
|
|
57
60
|
* correct roster of its own clients and nothing errors — which is why the
|
package/dist/index.js
CHANGED
|
@@ -4,63 +4,74 @@ import { PresenceKeyMissing as r, PresenceStoreUnavailable as i } from "./errors
|
|
|
4
4
|
import { BASE_NAME as a, heartbeatDescriptor as o, leaveDescriptor as s, listDescriptor as c, presenceRoster as l, presenceRpcClientImports as u } from "./rpc.js";
|
|
5
5
|
import { Effect as d } from "effect";
|
|
6
6
|
import { definePlugin as f, pluginInstanceName as p, publishReactivity as m } from "@voltro/protocol";
|
|
7
|
-
|
|
7
|
+
// src/index.ts
|
|
8
8
|
var h = a, g = (e = {}) => {
|
|
9
9
|
if (e.timeoutMs !== void 0 && e.timeoutMs < 1e3) throw Error(`presencePlugin: \`timeoutMs\` is ${e.timeoutMs}, which expires every member before it can heartbeat again.
|
|
10
10
|
A member is online for \`timeoutMs\` after its last heartbeat, and \`usePresence\` beats every 15s by default — so this must be LARGER than your client's
|
|
11
11
|
\`heartbeatMs\`, comfortably. The default pair is 30s server / 15s client.
|
|
12
12
|
There is no "never expire" spelling: omit \`timeoutMs\` for the default.`);
|
|
13
|
-
let t
|
|
13
|
+
let t = e.timeoutMs ?? 3e4, a = Math.max(1e3, Math.floor(e.sweepIntervalMs ?? t / 3)), g = p({
|
|
14
14
|
base: h,
|
|
15
15
|
alias: e.alias,
|
|
16
16
|
instance: e.name
|
|
17
|
-
}), v, y
|
|
17
|
+
}), _, v, y = () => {
|
|
18
|
+
let e = v ?? _;
|
|
19
|
+
_ = void 0, e !== void 0 && (v = e, e.detach(), v = void 0);
|
|
20
|
+
}, b, x = !1, S, C = 0, w = !1, T = !1, E, D, O, k = () => {
|
|
21
|
+
let e = D;
|
|
22
|
+
if (x) {
|
|
23
|
+
if (e === void 0) {
|
|
24
|
+
let e = _ === void 0 ? "awaiting-binding" : T ? "connecting" : void 0;
|
|
25
|
+
e !== void 0 && E !== `${C}:${e}` && (E = `${C}:${e}`, S?.info("presence transport", { state: e }));
|
|
26
|
+
} else e.generation === C && e !== O && (O = e, S?.info("presence transport", { crossInstance: e.connected }), !e.connected && (e.hasProvider ? S?.warn("presence: could not subscribe — this instance will only see its own members", { cause: e.cause }) : S?.warn("presence: no broadcast provider — each replica sees only the clients CONNECTED TO IT. Correct for a single instance; on more than one, every screen shows a fraction of the room and nothing reports it. Add @voltro/plugin-broadcast (redis:// or nats://) to share the roster.")));
|
|
27
|
+
}
|
|
28
|
+
}, A = (e) => {
|
|
18
29
|
let t = e === void 0 || e.includes("presence");
|
|
19
|
-
if (
|
|
20
|
-
return
|
|
21
|
-
},
|
|
22
|
-
|
|
23
|
-
let e =
|
|
30
|
+
if (b !== void 0 && b !== t) throw Error("Presence bind and activation workloads must agree");
|
|
31
|
+
return b = t, t;
|
|
32
|
+
}, j, M, N, P = () => {
|
|
33
|
+
N === void 0 && _ !== void 0 && (N = setInterval(() => {
|
|
34
|
+
let e = _;
|
|
24
35
|
if (e === void 0) return;
|
|
25
|
-
let
|
|
26
|
-
if (
|
|
27
|
-
for (let
|
|
28
|
-
|
|
36
|
+
let n = e.tracker.sweep(t);
|
|
37
|
+
if (n.length !== 0) {
|
|
38
|
+
for (let t of n) e.announce(t);
|
|
39
|
+
L();
|
|
29
40
|
}
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
m(
|
|
33
|
-
},
|
|
41
|
+
}, a), N.unref?.());
|
|
42
|
+
}, F = (e, t) => typeof t == "string" && t.length > 0 ? t : e.request.subject?.id ?? null, I = (e) => e.request.subject?.tenantId ?? null, L = () => {
|
|
43
|
+
m(j, l);
|
|
44
|
+
}, R = [
|
|
34
45
|
{
|
|
35
46
|
...o,
|
|
36
47
|
description: "Register/refresh the caller's presence in a channel.",
|
|
37
48
|
execute: (t, n) => d.gen(function* () {
|
|
38
49
|
let a = t;
|
|
39
|
-
if (!
|
|
40
|
-
let o =
|
|
50
|
+
if (!_) return yield* d.fail(new i({}));
|
|
51
|
+
let o = F(n, a.key);
|
|
41
52
|
if (o === null) return yield* d.fail(new r({ channel: a.channel }));
|
|
42
|
-
let s = e.resolveMember !== void 0 &&
|
|
53
|
+
let s = e.resolveMember !== void 0 && M !== void 0 ? yield* d.promise(async () => e.resolveMember({
|
|
43
54
|
subject: n.request.subject,
|
|
44
55
|
channel: a.channel,
|
|
45
56
|
key: o,
|
|
46
|
-
store:
|
|
57
|
+
store: M
|
|
47
58
|
})) : void 0, c = e.resolveMember !== void 0 && e.identityFields !== void 0 ? Object.fromEntries(Object.entries(a.meta ?? {}).filter(([t]) => !e.identityFields.includes(t))) : a.meta, l = s === void 0 ? c : {
|
|
48
59
|
...c ?? {},
|
|
49
60
|
...s
|
|
50
|
-
}, u =
|
|
61
|
+
}, u = _.tracker.track({
|
|
51
62
|
channel: a.channel,
|
|
52
|
-
tenantId:
|
|
63
|
+
tenantId: I(n),
|
|
53
64
|
key: o,
|
|
54
65
|
...l === void 0 ? {} : { meta: l }
|
|
55
66
|
});
|
|
56
|
-
return
|
|
67
|
+
return _.announce(u.delta), u.changed && L(), { ok: !0 };
|
|
57
68
|
})
|
|
58
69
|
},
|
|
59
70
|
{
|
|
60
71
|
...c,
|
|
61
72
|
description: "The online roster of a channel (heartbeat-fresh members). Push-driven — updates live.",
|
|
62
73
|
execute: (e, t) => d.gen(function* () {
|
|
63
|
-
return
|
|
74
|
+
return _ ? _.tracker.roster(e.channel, I(t)).map((e) => ({
|
|
64
75
|
key: e.key,
|
|
65
76
|
meta: e.meta
|
|
66
77
|
})) : yield* d.fail(new i({}));
|
|
@@ -71,53 +82,56 @@ var h = a, g = (e = {}) => {
|
|
|
71
82
|
description: "Remove the caller's presence from a channel.",
|
|
72
83
|
execute: (e, t) => d.gen(function* () {
|
|
73
84
|
let n = e;
|
|
74
|
-
if (!
|
|
75
|
-
let a =
|
|
85
|
+
if (!_) return yield* d.fail(new i({}));
|
|
86
|
+
let a = F(t, n.key);
|
|
76
87
|
if (a === null) return yield* d.fail(new r({ channel: n.channel }));
|
|
77
|
-
let o =
|
|
88
|
+
let o = _.tracker.untrack({
|
|
78
89
|
channel: n.channel,
|
|
79
|
-
tenantId:
|
|
90
|
+
tenantId: I(t),
|
|
80
91
|
key: a
|
|
81
92
|
});
|
|
82
|
-
return
|
|
93
|
+
return _.announce(o.delta), o.changed && L(), { ok: !0 };
|
|
83
94
|
})
|
|
84
95
|
}
|
|
85
96
|
];
|
|
86
97
|
return f({
|
|
87
|
-
name:
|
|
98
|
+
name: g,
|
|
88
99
|
baseName: h,
|
|
89
100
|
workloads: ["rpc", "presence"],
|
|
90
101
|
description: "Ephemeral realtime presence — heartbeat roster per channel.",
|
|
91
|
-
routes:
|
|
102
|
+
routes: R,
|
|
92
103
|
rpcClientDescriptors: u,
|
|
93
104
|
bindDataStore: (e, t) => {
|
|
94
|
-
if (!
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
...
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
if (w || !A(t?.workloads)) return;
|
|
106
|
+
let r = ++C;
|
|
107
|
+
D = void 0, O = void 0, E = void 0, y(), j = e, M = e;
|
|
108
|
+
let i = t;
|
|
109
|
+
T = i?.broadcast !== void 0, _ = n({
|
|
110
|
+
instanceId: i?.instanceId ?? `presence-${process.pid}`,
|
|
111
|
+
membership: i?.membership ?? { onChange: () => () => {} },
|
|
112
|
+
...i?.broadcast === void 0 ? {} : { provider: i.broadcast },
|
|
113
|
+
...i?.broadcastChannels?.presence === void 0 ? {} : { channel: i.broadcastChannels.presence },
|
|
114
|
+
onRosterChanged: L,
|
|
115
|
+
logger: { warn: (e, t) => S?.warn(e, t) },
|
|
116
|
+
onTransportSettled: (e, t) => {
|
|
117
|
+
r === C && (T = !1, D = {
|
|
118
|
+
generation: r,
|
|
119
|
+
connected: e,
|
|
120
|
+
hasProvider: i?.broadcast !== void 0,
|
|
121
|
+
cause: t
|
|
122
|
+
}, k());
|
|
123
|
+
}
|
|
124
|
+
}), x && (P(), k());
|
|
104
125
|
},
|
|
105
126
|
onActivate: (e) => d.sync(() => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
});
|
|
111
|
-
let n = Number(process.env.VOLTRO_PRESENCE_BROKER_GRACE_MS ?? 5e3), r = setTimeout(() => {
|
|
112
|
-
let t = v?.hasTransport === !0;
|
|
113
|
-
e.logger.info("presence transport", { crossInstance: t }), !t && e.logger.warn("presence: no broadcast provider — each replica sees only the clients CONNECTED TO IT. Correct for a single instance; on more than one, every screen shows a fraction of the room and nothing reports it. Add @voltro/plugin-broadcast (redis:// or nats://) to share the roster.");
|
|
114
|
-
}, n);
|
|
115
|
-
r.unref?.(), t = r;
|
|
127
|
+
A(e.workloads) && (x || (w = !1, S = e.logger, x = !0, P(), e.logger.info("presence active", {
|
|
128
|
+
timeoutMs: t,
|
|
129
|
+
sweepEveryMs: a
|
|
130
|
+
}), k()));
|
|
116
131
|
}),
|
|
117
132
|
onDeactivate: () => d.sync(() => {
|
|
118
|
-
|
|
133
|
+
x = !1, w = !0, C += 1, T = !1, E = void 0, D = void 0, O = void 0, N !== void 0 && (clearInterval(N), N = void 0), j = void 0, M = void 0, y();
|
|
119
134
|
})
|
|
120
135
|
});
|
|
121
136
|
};
|
|
122
|
-
//#endregion
|
|
123
137
|
export { t as PRESENCE_BROADCAST_CHANNEL, r as PresenceKeyMissing, i as PresenceStoreUnavailable, e as PresenceTracker, n as attachPresenceBus, g as presencePlugin, l as presenceRoster };
|
package/dist/rpc.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PresenceKeyMissing as e, PresenceStoreUnavailable as t } from "./errors.js";
|
|
2
2
|
import { Schema as n } from "effect";
|
|
3
3
|
import { defineMutation as r, defineQuery as i, reactivityChannel as a } from "@voltro/protocol";
|
|
4
|
-
|
|
4
|
+
// src/rpc.ts
|
|
5
5
|
var o = "@voltro/plugin-presence", s = a("presence"), c = n.Struct({
|
|
6
6
|
key: n.String,
|
|
7
7
|
meta: n.NullOr(n.Record(n.String, n.Unknown))
|
|
@@ -57,5 +57,4 @@ var o = "@voltro/plugin-presence", s = a("presence"), c = n.Struct({
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
];
|
|
60
|
-
//#endregion
|
|
61
60
|
export { o as BASE_NAME, l as heartbeatDescriptor, d as leaveDescriptor, u as listDescriptor, s as presenceRoster, f as presenceRpcClientImports };
|
package/dist/tracker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/tracker.ts
|
|
2
2
|
var e = (e, t) => `${e ?? "~"}::${t}`, t = (e) => e === void 0 ? "undefined" : typeof e != "object" || !e ? JSON.stringify(e) ?? "undefined" : Array.isArray(e) ? `[${e.map(t).join(",")}]` : `{${Object.entries(e).sort(([e], [t]) => e < t ? -1 : +(e > t)).map(([e, n]) => `${JSON.stringify(e)}:${t(n)}`).join(",")}}`, n = (e, n) => e === n || e !== null && n !== null && t(e) === t(n), r = class {
|
|
3
3
|
partitions = /* @__PURE__ */ new Map();
|
|
4
4
|
selfId;
|
|
@@ -130,5 +130,4 @@ var e = (e, t) => `${e ?? "~"}::${t}`, t = (e) => e === void 0 ? "undefined" : t
|
|
|
130
130
|
return this.owners().reduce((e, t) => e + t.members, 0);
|
|
131
131
|
}
|
|
132
132
|
};
|
|
133
|
-
//#endregion
|
|
134
133
|
export { r as PresenceTracker };
|
package/dist/trackerBus.js
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import { PresenceTracker as e } from "./tracker.js";
|
|
2
2
|
import { Effect as t } from "effect";
|
|
3
|
-
|
|
3
|
+
// src/trackerBus.ts
|
|
4
4
|
var n = "voltro:presence", r = (e) => typeof e == "object" && !!e && typeof e.owner == "string" && typeof e.delta == "object" && e.delta !== null, i = (e) => typeof e == "object" && !!e && typeof e.owner == "string" && Array.isArray(e.sync), a = (n) => {
|
|
5
5
|
let a = new e({
|
|
6
6
|
instanceId: n.instanceId,
|
|
7
7
|
...n.now === void 0 ? {} : { now: n.now }
|
|
8
|
-
}), o = n.logger, s = () => {
|
|
8
|
+
}), o = n.logger, s = !1, c = !1, l = (e, t) => {
|
|
9
|
+
if (!s) try {
|
|
10
|
+
n.onTransportSettled?.(e, t);
|
|
11
|
+
} catch (e) {
|
|
12
|
+
o?.warn("presence: transport observer failed", { cause: e });
|
|
13
|
+
}
|
|
14
|
+
}, u = () => {
|
|
9
15
|
try {
|
|
10
16
|
n.onRosterChanged?.();
|
|
11
17
|
} catch {}
|
|
12
|
-
},
|
|
18
|
+
}, d = n.membership.onChange((e) => {
|
|
19
|
+
if (s) return;
|
|
13
20
|
if (e.kind === "joined") {
|
|
14
|
-
|
|
21
|
+
h();
|
|
15
22
|
return;
|
|
16
23
|
}
|
|
17
24
|
let t = a.dropOwner(e.instanceId);
|
|
@@ -19,17 +26,18 @@ var n = "voltro:presence", r = (e) => typeof e == "object" && !!e && typeof e.ow
|
|
|
19
26
|
instanceId: e.instanceId,
|
|
20
27
|
members: t,
|
|
21
28
|
reason: e.kind
|
|
22
|
-
}),
|
|
23
|
-
}),
|
|
24
|
-
|
|
25
|
-
},
|
|
29
|
+
}), u()), e.kind === "restarted" && h();
|
|
30
|
+
}), f = n.provider, p = n.channel ?? "voltro:presence", m = (e) => {
|
|
31
|
+
s || f === void 0 || t.runPromise(f.publish(p, JSON.stringify(e))).catch(() => {});
|
|
32
|
+
}, h = () => {
|
|
26
33
|
let e = a.ownSnapshot();
|
|
27
|
-
e.length !== 0 &&
|
|
34
|
+
e.length !== 0 && m({
|
|
28
35
|
owner: n.instanceId,
|
|
29
36
|
sync: e
|
|
30
37
|
});
|
|
31
|
-
},
|
|
32
|
-
return
|
|
38
|
+
}, g;
|
|
39
|
+
return f === void 0 ? queueMicrotask(() => l(!1)) : t.runPromise(f.subscribe(p, (e) => {
|
|
40
|
+
if (s) return;
|
|
33
41
|
let t;
|
|
34
42
|
try {
|
|
35
43
|
t = JSON.parse(e);
|
|
@@ -39,25 +47,43 @@ var n = "voltro:presence", r = (e) => typeof e == "object" && !!e && typeof e.ow
|
|
|
39
47
|
if (i(t)) {
|
|
40
48
|
let e = !1;
|
|
41
49
|
for (let n of t.sync) e = a.apply(t.owner, n) || e;
|
|
42
|
-
e &&
|
|
43
|
-
} else r(t) && a.apply(t.owner, t.delta) &&
|
|
50
|
+
e && u();
|
|
51
|
+
} else r(t) && a.apply(t.owner, t.delta) && u();
|
|
44
52
|
})).then((e) => {
|
|
45
|
-
|
|
53
|
+
if (g = e, s) try {
|
|
54
|
+
e(), g = void 0;
|
|
55
|
+
} catch (e) {
|
|
56
|
+
o?.warn("presence: could not release a closed subscription", { cause: e });
|
|
57
|
+
}
|
|
58
|
+
else c = !0, l(!0);
|
|
46
59
|
}, (e) => {
|
|
47
|
-
o?.warn("presence: could not subscribe — this instance will only see its own members", { cause: e });
|
|
60
|
+
s || (n.onTransportSettled === void 0 && o?.warn("presence: could not subscribe — this instance will only see its own members", { cause: e }), l(!1, e));
|
|
48
61
|
}), {
|
|
49
62
|
tracker: a,
|
|
50
|
-
hasTransport
|
|
63
|
+
get hasTransport() {
|
|
64
|
+
return c && !s;
|
|
65
|
+
},
|
|
51
66
|
announce: (e) => {
|
|
52
|
-
|
|
67
|
+
m({
|
|
53
68
|
owner: n.instanceId,
|
|
54
69
|
delta: e
|
|
55
70
|
});
|
|
56
71
|
},
|
|
57
72
|
detach: () => {
|
|
58
|
-
|
|
73
|
+
s = !0, c = !1;
|
|
74
|
+
let e = [];
|
|
75
|
+
try {
|
|
76
|
+
d?.(), d = void 0;
|
|
77
|
+
} catch (t) {
|
|
78
|
+
e.push(t);
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
g?.(), g = void 0;
|
|
82
|
+
} catch (t) {
|
|
83
|
+
e.push(t);
|
|
84
|
+
}
|
|
85
|
+
if (e.length > 0) throw AggregateError(e, "presence: could not detach transport");
|
|
59
86
|
}
|
|
60
87
|
};
|
|
61
88
|
};
|
|
62
|
-
//#endregion
|
|
63
89
|
export { n as PRESENCE_BROADCAST_CHANNEL, a as attachPresenceBus };
|
package/dist/web.js
CHANGED
|
@@ -2,7 +2,7 @@ import { BASE_NAME as e } from "./rpc.js";
|
|
|
2
2
|
import { pluginTag as t } from "@voltro/protocol";
|
|
3
3
|
import { useCallback as n, useEffect as r, useMemo as i, useRef as a, useState as o } from "react";
|
|
4
4
|
import { useMutation as s, useSubscription as c } from "@voltro/client";
|
|
5
|
-
|
|
5
|
+
// src/web.ts
|
|
6
6
|
var l = (n) => t(e, n), u = (e, t = {}) => {
|
|
7
7
|
let { meta: o, key: u, heartbeatMs: d = 15e3, apiName: f = "app" } = t, p = s(f, l("heartbeat")), m = s(f, l("leave")), h = a(p), g = a(m);
|
|
8
8
|
h.current = p, g.current = m;
|
|
@@ -111,5 +111,4 @@ var l = (n) => t(e, n), u = (e, t = {}) => {
|
|
|
111
111
|
}
|
|
112
112
|
}), [e, n]);
|
|
113
113
|
};
|
|
114
|
-
//#endregion
|
|
115
114
|
export { d as activeTypers, u as usePresence, p as usePresenceChannel, f as useTyping };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-presence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.88.0",
|
|
4
4
|
"description": "Ephemeral realtime presence — who is online in a channel, with heartbeat + live roster + per-member metadata (cursor, status — client-supplied and relayed verbatim; identity fields belong in the server-side resolveMember hook). Held in memory, owner-partitioned; cross-instance with @voltro/plugin-broadcast.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"node": ">=24.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@voltro/client": "0.
|
|
52
|
-
"@voltro/protocol": "0.
|
|
51
|
+
"@voltro/client": "0.88.0",
|
|
52
|
+
"@voltro/protocol": "0.88.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@voltro/database": "0.
|
|
56
|
-
"effect": "4.0
|
|
55
|
+
"@voltro/database": "0.88.0",
|
|
56
|
+
"effect": "4.1.0",
|
|
57
57
|
"react": "^19.0.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependenciesMeta": {
|