@voltro/plugin-presence 0.85.0 → 0.87.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 +5987 -0
- package/THIRD-PARTY-NOTICES.md +1125 -34
- package/dist/index.js +83 -263
- package/dist/tracker.js +134 -0
- package/dist/trackerBus.js +63 -0
- package/dist/web.js +1 -7
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,303 +1,123 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
constructor(e) {
|
|
11
|
-
this.selfId = e.instanceId, this.now = e.now ?? Date.now;
|
|
12
|
-
}
|
|
13
|
-
get owner() {
|
|
14
|
-
return this.selfId;
|
|
15
|
-
}
|
|
16
|
-
branch(e, t, n) {
|
|
17
|
-
let r = this.partitions.get(e);
|
|
18
|
-
r === void 0 && (r = /* @__PURE__ */ new Map(), this.partitions.set(e, r));
|
|
19
|
-
let i = f(t, n), a = r.get(i);
|
|
20
|
-
return a === void 0 && (a = {
|
|
21
|
-
tenantId: t,
|
|
22
|
-
channel: n,
|
|
23
|
-
members: /* @__PURE__ */ new Map()
|
|
24
|
-
}, r.set(i, a)), a;
|
|
25
|
-
}
|
|
26
|
-
removeMember(e, t, n, r) {
|
|
27
|
-
let i = this.partitions.get(e);
|
|
28
|
-
if (i === void 0) return !1;
|
|
29
|
-
let a = f(t, n), o = i.get(a);
|
|
30
|
-
if (o === void 0) return !1;
|
|
31
|
-
let s = o.members.delete(r);
|
|
32
|
-
return o.members.size === 0 && i.delete(a), i.size === 0 && this.partitions.delete(e), s;
|
|
33
|
-
}
|
|
34
|
-
track(e) {
|
|
35
|
-
let t = this.now(), n = e.meta ?? null, { members: r } = this.branch(this.selfId, e.tenantId, e.channel), i = r.get(e.key);
|
|
36
|
-
return r.set(e.key, {
|
|
37
|
-
key: e.key,
|
|
38
|
-
meta: n,
|
|
39
|
-
lastSeen: t
|
|
40
|
-
}), {
|
|
41
|
-
delta: {
|
|
42
|
-
op: "set",
|
|
43
|
-
channel: e.channel,
|
|
44
|
-
tenantId: e.tenantId,
|
|
45
|
-
key: e.key,
|
|
46
|
-
meta: n,
|
|
47
|
-
lastSeen: t
|
|
48
|
-
},
|
|
49
|
-
changed: i === void 0 || !m(i.meta, n)
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
untrack(e) {
|
|
53
|
-
let t = this.removeMember(this.selfId, e.tenantId, e.channel, e.key);
|
|
54
|
-
return {
|
|
55
|
-
delta: {
|
|
56
|
-
op: "remove",
|
|
57
|
-
channel: e.channel,
|
|
58
|
-
tenantId: e.tenantId,
|
|
59
|
-
key: e.key
|
|
60
|
-
},
|
|
61
|
-
changed: t
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
apply(e, t) {
|
|
65
|
-
if (e === this.selfId) return !1;
|
|
66
|
-
if (t.op === "remove") return this.removeMember(e, t.tenantId, t.channel, t.key);
|
|
67
|
-
let { members: n } = this.branch(e, t.tenantId, t.channel), r = n.get(t.key);
|
|
68
|
-
return n.set(t.key, {
|
|
69
|
-
key: t.key,
|
|
70
|
-
meta: t.meta,
|
|
71
|
-
lastSeen: t.lastSeen
|
|
72
|
-
}), r === void 0 || !m(r.meta, t.meta);
|
|
73
|
-
}
|
|
74
|
-
dropOwner(e) {
|
|
75
|
-
if (e === this.selfId) return 0;
|
|
76
|
-
let t = this.partitions.get(e);
|
|
77
|
-
if (t === void 0) return 0;
|
|
78
|
-
let n = 0;
|
|
79
|
-
for (let e of t.values()) n += e.members.size;
|
|
80
|
-
return this.partitions.delete(e), n;
|
|
81
|
-
}
|
|
82
|
-
sweep(e) {
|
|
83
|
-
let t = this.partitions.get(this.selfId);
|
|
84
|
-
if (t === void 0) return [];
|
|
85
|
-
let n = this.now() - e, r = [];
|
|
86
|
-
for (let [e, i] of t) {
|
|
87
|
-
for (let [e, t] of i.members) t.lastSeen >= n || (i.members.delete(e), r.push({
|
|
88
|
-
op: "remove",
|
|
89
|
-
channel: i.channel,
|
|
90
|
-
tenantId: i.tenantId,
|
|
91
|
-
key: e
|
|
92
|
-
}));
|
|
93
|
-
i.members.size === 0 && t.delete(e);
|
|
94
|
-
}
|
|
95
|
-
return t.size === 0 && this.partitions.delete(this.selfId), r;
|
|
96
|
-
}
|
|
97
|
-
ownSnapshot() {
|
|
98
|
-
let e = [], t = this.partitions.get(this.selfId);
|
|
99
|
-
if (t === void 0) return e;
|
|
100
|
-
for (let n of t.values()) for (let t of n.members.values()) e.push({
|
|
101
|
-
op: "set",
|
|
102
|
-
channel: n.channel,
|
|
103
|
-
tenantId: n.tenantId,
|
|
104
|
-
key: t.key,
|
|
105
|
-
meta: t.meta,
|
|
106
|
-
lastSeen: t.lastSeen
|
|
107
|
-
});
|
|
108
|
-
return e;
|
|
109
|
-
}
|
|
110
|
-
roster(e, t) {
|
|
111
|
-
let n = f(t, e), r = [];
|
|
112
|
-
for (let [i, a] of this.partitions) {
|
|
113
|
-
let o = a.get(n);
|
|
114
|
-
if (o !== void 0) for (let n of o.members.values()) r.push({
|
|
115
|
-
...n,
|
|
116
|
-
owner: i,
|
|
117
|
-
channel: e,
|
|
118
|
-
tenantId: t
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
return r.sort((e, t) => t.lastSeen - e.lastSeen);
|
|
122
|
-
}
|
|
123
|
-
owners() {
|
|
124
|
-
return [...this.partitions.entries()].map(([e, t]) => {
|
|
125
|
-
let n = 0;
|
|
126
|
-
for (let e of t.values()) n += e.members.size;
|
|
127
|
-
return {
|
|
128
|
-
owner: e,
|
|
129
|
-
members: n
|
|
130
|
-
};
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
size() {
|
|
134
|
-
return this.owners().reduce((e, t) => e + t.members, 0);
|
|
135
|
-
}
|
|
136
|
-
}, g = "voltro:presence", _ = (e) => typeof e == "object" && !!e && typeof e.owner == "string" && typeof e.delta == "object" && e.delta !== null, v = (e) => typeof e == "object" && !!e && typeof e.owner == "string" && Array.isArray(e.sync), y = (e) => {
|
|
137
|
-
let t = new h({
|
|
138
|
-
instanceId: e.instanceId,
|
|
139
|
-
...e.now === void 0 ? {} : { now: e.now }
|
|
140
|
-
}), n = e.logger, r = () => {
|
|
141
|
-
try {
|
|
142
|
-
e.onRosterChanged?.();
|
|
143
|
-
} catch {}
|
|
144
|
-
}, i = e.membership.onChange((e) => {
|
|
145
|
-
if (e.kind === "joined") {
|
|
146
|
-
l();
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
let i = t.dropOwner(e.instanceId);
|
|
150
|
-
i > 0 && (n?.warn("presence: dropped members of a departed instance", {
|
|
151
|
-
instanceId: e.instanceId,
|
|
152
|
-
members: i,
|
|
153
|
-
reason: e.kind
|
|
154
|
-
}), r()), e.kind === "restarted" && l();
|
|
155
|
-
}), a = e.provider, o = e.channel ?? "voltro:presence", s = (e) => {
|
|
156
|
-
a !== void 0 && c.runPromise(a.publish(o, JSON.stringify(e))).catch(() => {});
|
|
157
|
-
}, l = () => {
|
|
158
|
-
let n = t.ownSnapshot();
|
|
159
|
-
n.length !== 0 && s({
|
|
160
|
-
owner: e.instanceId,
|
|
161
|
-
sync: n
|
|
162
|
-
});
|
|
163
|
-
}, u;
|
|
164
|
-
return a !== void 0 && c.runPromise(a.subscribe(o, (e) => {
|
|
165
|
-
let n;
|
|
166
|
-
try {
|
|
167
|
-
n = JSON.parse(e);
|
|
168
|
-
} catch {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
if (v(n)) {
|
|
172
|
-
let e = !1;
|
|
173
|
-
for (let r of n.sync) e = t.apply(n.owner, r) || e;
|
|
174
|
-
e && r();
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
_(n) && t.apply(n.owner, n.delta) && r();
|
|
178
|
-
})).then((e) => {
|
|
179
|
-
u = e;
|
|
180
|
-
}, (e) => {
|
|
181
|
-
n?.warn("presence: could not subscribe — this instance will only see its own members", { cause: e });
|
|
182
|
-
}), {
|
|
183
|
-
tracker: t,
|
|
184
|
-
hasTransport: a !== void 0,
|
|
185
|
-
announce: (t) => {
|
|
186
|
-
s({
|
|
187
|
-
owner: e.instanceId,
|
|
188
|
-
delta: t
|
|
189
|
-
});
|
|
190
|
-
},
|
|
191
|
-
detach: () => {
|
|
192
|
-
i(), u?.();
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
}, b = n, x = (n = {}) => {
|
|
196
|
-
if (n.timeoutMs !== void 0 && n.timeoutMs < 1e3) throw Error(`presencePlugin: \`timeoutMs\` is ${n.timeoutMs}, which expires every member before it can heartbeat again.
|
|
1
|
+
import { PresenceTracker as e } from "./tracker.js";
|
|
2
|
+
import { PRESENCE_BROADCAST_CHANNEL as t, attachPresenceBus as n } from "./trackerBus.js";
|
|
3
|
+
import { PresenceKeyMissing as r, PresenceStoreUnavailable as i } from "./errors.js";
|
|
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
|
+
import { Effect as d } from "effect";
|
|
6
|
+
import { definePlugin as f, pluginInstanceName as p, publishReactivity as m } from "@voltro/protocol";
|
|
7
|
+
//#region src/index.ts
|
|
8
|
+
var h = a, g = (e = {}) => {
|
|
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.
|
|
197
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
|
|
198
11
|
\`heartbeatMs\`, comfortably. The default pair is 30s server / 15s client.
|
|
199
12
|
There is no "never expire" spelling: omit \`timeoutMs\` for the default.`);
|
|
200
|
-
let
|
|
201
|
-
base:
|
|
202
|
-
alias:
|
|
203
|
-
instance:
|
|
204
|
-
}),
|
|
205
|
-
|
|
206
|
-
|
|
13
|
+
let t, a = e.timeoutMs ?? 3e4, g = Math.max(1e3, Math.floor(e.sweepIntervalMs ?? a / 3)), _ = p({
|
|
14
|
+
base: h,
|
|
15
|
+
alias: e.alias,
|
|
16
|
+
instance: e.name
|
|
17
|
+
}), v, y, b = !1, x = (e) => {
|
|
18
|
+
let t = e === void 0 || e.includes("presence");
|
|
19
|
+
if (y !== void 0 && y !== t) throw Error("Presence bind and activation workloads must agree");
|
|
20
|
+
return y = t, t;
|
|
21
|
+
}, S, C, w, T = () => {
|
|
22
|
+
w === void 0 && v !== void 0 && (w = setInterval(() => {
|
|
23
|
+
let e = v;
|
|
207
24
|
if (e === void 0) return;
|
|
208
|
-
let t = e.tracker.sweep(
|
|
25
|
+
let t = e.tracker.sweep(a);
|
|
209
26
|
if (t.length !== 0) {
|
|
210
27
|
for (let n of t) e.announce(n);
|
|
211
|
-
|
|
28
|
+
O();
|
|
212
29
|
}
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
|
|
216
|
-
},
|
|
30
|
+
}, g), w.unref?.());
|
|
31
|
+
}, E = (e, t) => typeof t == "string" && t.length > 0 ? t : e.request.subject?.id ?? null, D = (e) => e.request.subject?.tenantId ?? null, O = () => {
|
|
32
|
+
m(S, l);
|
|
33
|
+
}, k = [
|
|
217
34
|
{
|
|
218
|
-
...
|
|
35
|
+
...o,
|
|
219
36
|
description: "Register/refresh the caller's presence in a channel.",
|
|
220
|
-
execute: (
|
|
221
|
-
let a =
|
|
222
|
-
if (!
|
|
223
|
-
let o =
|
|
224
|
-
if (o === null) return yield*
|
|
225
|
-
let s =
|
|
226
|
-
subject:
|
|
37
|
+
execute: (t, n) => d.gen(function* () {
|
|
38
|
+
let a = t;
|
|
39
|
+
if (!v) return yield* d.fail(new i({}));
|
|
40
|
+
let o = E(n, a.key);
|
|
41
|
+
if (o === null) return yield* d.fail(new r({ channel: a.channel }));
|
|
42
|
+
let s = e.resolveMember !== void 0 && C !== void 0 ? yield* d.promise(async () => e.resolveMember({
|
|
43
|
+
subject: n.request.subject,
|
|
227
44
|
channel: a.channel,
|
|
228
45
|
key: o,
|
|
229
|
-
store:
|
|
230
|
-
})) : void 0,
|
|
231
|
-
...
|
|
46
|
+
store: C
|
|
47
|
+
})) : 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
|
+
...c ?? {},
|
|
232
49
|
...s
|
|
233
|
-
},
|
|
50
|
+
}, u = v.tracker.track({
|
|
234
51
|
channel: a.channel,
|
|
235
|
-
tenantId:
|
|
52
|
+
tenantId: D(n),
|
|
236
53
|
key: o,
|
|
237
|
-
...
|
|
54
|
+
...l === void 0 ? {} : { meta: l }
|
|
238
55
|
});
|
|
239
|
-
return
|
|
56
|
+
return v.announce(u.delta), u.changed && O(), { ok: !0 };
|
|
240
57
|
})
|
|
241
58
|
},
|
|
242
59
|
{
|
|
243
|
-
...
|
|
60
|
+
...c,
|
|
244
61
|
description: "The online roster of a channel (heartbeat-fresh members). Push-driven — updates live.",
|
|
245
|
-
execute: (e,
|
|
246
|
-
return
|
|
62
|
+
execute: (e, t) => d.gen(function* () {
|
|
63
|
+
return v ? v.tracker.roster(e.channel, D(t)).map((e) => ({
|
|
247
64
|
key: e.key,
|
|
248
65
|
meta: e.meta
|
|
249
|
-
})) : yield*
|
|
66
|
+
})) : yield* d.fail(new i({}));
|
|
250
67
|
})
|
|
251
68
|
},
|
|
252
69
|
{
|
|
253
|
-
...
|
|
70
|
+
...s,
|
|
254
71
|
description: "Remove the caller's presence from a channel.",
|
|
255
|
-
execute: (
|
|
256
|
-
let
|
|
257
|
-
if (!
|
|
258
|
-
let a =
|
|
259
|
-
if (a === null) return yield*
|
|
260
|
-
let o =
|
|
261
|
-
channel:
|
|
262
|
-
tenantId:
|
|
72
|
+
execute: (e, t) => d.gen(function* () {
|
|
73
|
+
let n = e;
|
|
74
|
+
if (!v) return yield* d.fail(new i({}));
|
|
75
|
+
let a = E(t, n.key);
|
|
76
|
+
if (a === null) return yield* d.fail(new r({ channel: n.channel }));
|
|
77
|
+
let o = v.tracker.untrack({
|
|
78
|
+
channel: n.channel,
|
|
79
|
+
tenantId: D(t),
|
|
263
80
|
key: a
|
|
264
81
|
});
|
|
265
|
-
return
|
|
82
|
+
return v.announce(o.delta), o.changed && O(), { ok: !0 };
|
|
266
83
|
})
|
|
267
84
|
}
|
|
268
85
|
];
|
|
269
|
-
return
|
|
270
|
-
name:
|
|
271
|
-
baseName:
|
|
86
|
+
return f({
|
|
87
|
+
name: _,
|
|
88
|
+
baseName: h,
|
|
89
|
+
workloads: ["rpc", "presence"],
|
|
272
90
|
description: "Ephemeral realtime presence — heartbeat roster per channel.",
|
|
273
|
-
routes:
|
|
274
|
-
rpcClientDescriptors:
|
|
91
|
+
routes: k,
|
|
92
|
+
rpcClientDescriptors: u,
|
|
275
93
|
bindDataStore: (e, t) => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
...
|
|
283
|
-
|
|
284
|
-
|
|
94
|
+
if (!x(t?.workloads)) return;
|
|
95
|
+
S = e, C = e;
|
|
96
|
+
let r = t;
|
|
97
|
+
v = n({
|
|
98
|
+
instanceId: r?.instanceId ?? `presence-${process.pid}`,
|
|
99
|
+
membership: r?.membership ?? { onChange: () => () => {} },
|
|
100
|
+
...r?.broadcast === void 0 ? {} : { provider: r.broadcast },
|
|
101
|
+
...r?.broadcastChannels?.presence === void 0 ? {} : { channel: r.broadcastChannels.presence },
|
|
102
|
+
onRosterChanged: O
|
|
103
|
+
}), b && T();
|
|
285
104
|
},
|
|
286
|
-
onActivate: (e) =>
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
105
|
+
onActivate: (e) => d.sync(() => {
|
|
106
|
+
if (!x(e.workloads)) return;
|
|
107
|
+
b = !0, T(), e.logger.info("presence active", {
|
|
108
|
+
timeoutMs: a,
|
|
109
|
+
sweepEveryMs: g
|
|
290
110
|
});
|
|
291
|
-
let
|
|
292
|
-
let t =
|
|
111
|
+
let n = Number(process.env.VOLTRO_PRESENCE_BROKER_GRACE_MS ?? 5e3), r = setTimeout(() => {
|
|
112
|
+
let t = v?.hasTransport === !0;
|
|
293
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.");
|
|
294
|
-
},
|
|
295
|
-
|
|
114
|
+
}, n);
|
|
115
|
+
r.unref?.(), t = r;
|
|
296
116
|
}),
|
|
297
|
-
onDeactivate: () =>
|
|
298
|
-
|
|
117
|
+
onDeactivate: () => d.sync(() => {
|
|
118
|
+
b = !1, t !== void 0 && (clearTimeout(t), t = void 0), w !== void 0 && (clearInterval(w), w = void 0), v?.detach(), v = void 0, S = void 0, C = void 0;
|
|
299
119
|
})
|
|
300
120
|
});
|
|
301
121
|
};
|
|
302
122
|
//#endregion
|
|
303
|
-
export {
|
|
123
|
+
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/tracker.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
//#region src/tracker.ts
|
|
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
|
+
partitions = /* @__PURE__ */ new Map();
|
|
4
|
+
selfId;
|
|
5
|
+
now;
|
|
6
|
+
constructor(e) {
|
|
7
|
+
this.selfId = e.instanceId, this.now = e.now ?? Date.now;
|
|
8
|
+
}
|
|
9
|
+
get owner() {
|
|
10
|
+
return this.selfId;
|
|
11
|
+
}
|
|
12
|
+
branch(t, n, r) {
|
|
13
|
+
let i = this.partitions.get(t);
|
|
14
|
+
i === void 0 && (i = /* @__PURE__ */ new Map(), this.partitions.set(t, i));
|
|
15
|
+
let a = e(n, r), o = i.get(a);
|
|
16
|
+
return o === void 0 && (o = {
|
|
17
|
+
tenantId: n,
|
|
18
|
+
channel: r,
|
|
19
|
+
members: /* @__PURE__ */ new Map()
|
|
20
|
+
}, i.set(a, o)), o;
|
|
21
|
+
}
|
|
22
|
+
removeMember(t, n, r, i) {
|
|
23
|
+
let a = this.partitions.get(t);
|
|
24
|
+
if (a === void 0) return !1;
|
|
25
|
+
let o = e(n, r), s = a.get(o);
|
|
26
|
+
if (s === void 0) return !1;
|
|
27
|
+
let c = s.members.delete(i);
|
|
28
|
+
return s.members.size === 0 && a.delete(o), a.size === 0 && this.partitions.delete(t), c;
|
|
29
|
+
}
|
|
30
|
+
track(e) {
|
|
31
|
+
let t = this.now(), r = e.meta ?? null, { members: i } = this.branch(this.selfId, e.tenantId, e.channel), a = i.get(e.key);
|
|
32
|
+
return i.set(e.key, {
|
|
33
|
+
key: e.key,
|
|
34
|
+
meta: r,
|
|
35
|
+
lastSeen: t
|
|
36
|
+
}), {
|
|
37
|
+
delta: {
|
|
38
|
+
op: "set",
|
|
39
|
+
channel: e.channel,
|
|
40
|
+
tenantId: e.tenantId,
|
|
41
|
+
key: e.key,
|
|
42
|
+
meta: r,
|
|
43
|
+
lastSeen: t
|
|
44
|
+
},
|
|
45
|
+
changed: a === void 0 || !n(a.meta, r)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
untrack(e) {
|
|
49
|
+
let t = this.removeMember(this.selfId, e.tenantId, e.channel, e.key);
|
|
50
|
+
return {
|
|
51
|
+
delta: {
|
|
52
|
+
op: "remove",
|
|
53
|
+
channel: e.channel,
|
|
54
|
+
tenantId: e.tenantId,
|
|
55
|
+
key: e.key
|
|
56
|
+
},
|
|
57
|
+
changed: t
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
apply(e, t) {
|
|
61
|
+
if (e === this.selfId) return !1;
|
|
62
|
+
if (t.op === "remove") return this.removeMember(e, t.tenantId, t.channel, t.key);
|
|
63
|
+
let { members: r } = this.branch(e, t.tenantId, t.channel), i = r.get(t.key);
|
|
64
|
+
return r.set(t.key, {
|
|
65
|
+
key: t.key,
|
|
66
|
+
meta: t.meta,
|
|
67
|
+
lastSeen: t.lastSeen
|
|
68
|
+
}), i === void 0 || !n(i.meta, t.meta);
|
|
69
|
+
}
|
|
70
|
+
dropOwner(e) {
|
|
71
|
+
if (e === this.selfId) return 0;
|
|
72
|
+
let t = this.partitions.get(e);
|
|
73
|
+
if (t === void 0) return 0;
|
|
74
|
+
let n = 0;
|
|
75
|
+
for (let e of t.values()) n += e.members.size;
|
|
76
|
+
return this.partitions.delete(e), n;
|
|
77
|
+
}
|
|
78
|
+
sweep(e) {
|
|
79
|
+
let t = this.partitions.get(this.selfId);
|
|
80
|
+
if (t === void 0) return [];
|
|
81
|
+
let n = this.now() - e, r = [];
|
|
82
|
+
for (let [e, i] of t) {
|
|
83
|
+
for (let [e, t] of i.members) t.lastSeen >= n || (i.members.delete(e), r.push({
|
|
84
|
+
op: "remove",
|
|
85
|
+
channel: i.channel,
|
|
86
|
+
tenantId: i.tenantId,
|
|
87
|
+
key: e
|
|
88
|
+
}));
|
|
89
|
+
i.members.size === 0 && t.delete(e);
|
|
90
|
+
}
|
|
91
|
+
return t.size === 0 && this.partitions.delete(this.selfId), r;
|
|
92
|
+
}
|
|
93
|
+
ownSnapshot() {
|
|
94
|
+
let e = [], t = this.partitions.get(this.selfId);
|
|
95
|
+
if (t === void 0) return e;
|
|
96
|
+
for (let n of t.values()) for (let t of n.members.values()) e.push({
|
|
97
|
+
op: "set",
|
|
98
|
+
channel: n.channel,
|
|
99
|
+
tenantId: n.tenantId,
|
|
100
|
+
key: t.key,
|
|
101
|
+
meta: t.meta,
|
|
102
|
+
lastSeen: t.lastSeen
|
|
103
|
+
});
|
|
104
|
+
return e;
|
|
105
|
+
}
|
|
106
|
+
roster(t, n) {
|
|
107
|
+
let r = e(n, t), i = [];
|
|
108
|
+
for (let [e, a] of this.partitions) {
|
|
109
|
+
let o = a.get(r);
|
|
110
|
+
if (o !== void 0) for (let r of o.members.values()) i.push({
|
|
111
|
+
...r,
|
|
112
|
+
owner: e,
|
|
113
|
+
channel: t,
|
|
114
|
+
tenantId: n
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return i.sort((e, t) => t.lastSeen - e.lastSeen);
|
|
118
|
+
}
|
|
119
|
+
owners() {
|
|
120
|
+
return [...this.partitions.entries()].map(([e, t]) => {
|
|
121
|
+
let n = 0;
|
|
122
|
+
for (let e of t.values()) n += e.members.size;
|
|
123
|
+
return {
|
|
124
|
+
owner: e,
|
|
125
|
+
members: n
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
size() {
|
|
130
|
+
return this.owners().reduce((e, t) => e + t.members, 0);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
//#endregion
|
|
134
|
+
export { r as PresenceTracker };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { PresenceTracker as e } from "./tracker.js";
|
|
2
|
+
import { Effect as t } from "effect";
|
|
3
|
+
//#region src/trackerBus.ts
|
|
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
|
+
let a = new e({
|
|
6
|
+
instanceId: n.instanceId,
|
|
7
|
+
...n.now === void 0 ? {} : { now: n.now }
|
|
8
|
+
}), o = n.logger, s = () => {
|
|
9
|
+
try {
|
|
10
|
+
n.onRosterChanged?.();
|
|
11
|
+
} catch {}
|
|
12
|
+
}, c = n.membership.onChange((e) => {
|
|
13
|
+
if (e.kind === "joined") {
|
|
14
|
+
f();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
let t = a.dropOwner(e.instanceId);
|
|
18
|
+
t > 0 && (o?.warn("presence: dropped members of a departed instance", {
|
|
19
|
+
instanceId: e.instanceId,
|
|
20
|
+
members: t,
|
|
21
|
+
reason: e.kind
|
|
22
|
+
}), s()), e.kind === "restarted" && f();
|
|
23
|
+
}), l = n.provider, u = n.channel ?? "voltro:presence", d = (e) => {
|
|
24
|
+
l !== void 0 && t.runPromise(l.publish(u, JSON.stringify(e))).catch(() => {});
|
|
25
|
+
}, f = () => {
|
|
26
|
+
let e = a.ownSnapshot();
|
|
27
|
+
e.length !== 0 && d({
|
|
28
|
+
owner: n.instanceId,
|
|
29
|
+
sync: e
|
|
30
|
+
});
|
|
31
|
+
}, p;
|
|
32
|
+
return l !== void 0 && t.runPromise(l.subscribe(u, (e) => {
|
|
33
|
+
let t;
|
|
34
|
+
try {
|
|
35
|
+
t = JSON.parse(e);
|
|
36
|
+
} catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (i(t)) {
|
|
40
|
+
let e = !1;
|
|
41
|
+
for (let n of t.sync) e = a.apply(t.owner, n) || e;
|
|
42
|
+
e && s();
|
|
43
|
+
} else r(t) && a.apply(t.owner, t.delta) && s();
|
|
44
|
+
})).then((e) => {
|
|
45
|
+
p = e;
|
|
46
|
+
}, (e) => {
|
|
47
|
+
o?.warn("presence: could not subscribe — this instance will only see its own members", { cause: e });
|
|
48
|
+
}), {
|
|
49
|
+
tracker: a,
|
|
50
|
+
hasTransport: l !== void 0,
|
|
51
|
+
announce: (e) => {
|
|
52
|
+
d({
|
|
53
|
+
owner: n.instanceId,
|
|
54
|
+
delta: e
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
detach: () => {
|
|
58
|
+
c(), p?.();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
export { n as PRESENCE_BROADCAST_CHANNEL, a as attachPresenceBus };
|
package/dist/web.js
CHANGED
|
@@ -42,13 +42,7 @@ var l = (n) => t(e, n), u = (e, t = {}) => {
|
|
|
42
42
|
]);
|
|
43
43
|
let S = a(void 0);
|
|
44
44
|
return r(() => {
|
|
45
|
-
|
|
46
|
-
if (S.current === void 0 || S.current === v) {
|
|
47
|
-
S.current = v;
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
S.current = v, x();
|
|
51
|
-
}
|
|
45
|
+
b && (S.current === void 0 || S.current === v ? S.current = v : (S.current = v, x()));
|
|
52
46
|
}, [
|
|
53
47
|
b,
|
|
54
48
|
v,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-presence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.87.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,11 +48,11 @@
|
|
|
48
48
|
"node": ">=24.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@voltro/client": "0.
|
|
52
|
-
"@voltro/protocol": "0.
|
|
51
|
+
"@voltro/client": "0.87.0",
|
|
52
|
+
"@voltro/protocol": "0.87.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@voltro/database": "0.
|
|
55
|
+
"@voltro/database": "0.87.0",
|
|
56
56
|
"effect": "4.0.1",
|
|
57
57
|
"react": "^19.0.0"
|
|
58
58
|
},
|