@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/dist/index.js CHANGED
@@ -1,303 +1,123 @@
1
- import { PresenceKeyMissing as e, PresenceStoreUnavailable as t } from "./errors.js";
2
- import { BASE_NAME as n, heartbeatDescriptor as r, leaveDescriptor as i, listDescriptor as a, presenceRoster as o, presenceRpcClientImports as s } from "./rpc.js";
3
- import { Effect as c } from "effect";
4
- import { definePlugin as l, pluginInstanceName as u, publishReactivity as d } from "@voltro/protocol";
5
- //#region src/tracker.ts
6
- var f = (e, t) => `${e ?? "~"}::${t}`, p = (e) => e === void 0 ? "undefined" : typeof e != "object" || !e ? JSON.stringify(e) ?? "undefined" : Array.isArray(e) ? `[${e.map(p).join(",")}]` : `{${Object.entries(e).sort(([e], [t]) => e < t ? -1 : +(e > t)).map(([e, t]) => `${JSON.stringify(e)}:${p(t)}`).join(",")}}`, m = (e, t) => e === t || e !== null && t !== null && p(e) === p(t), h = class {
7
- partitions = /* @__PURE__ */ new Map();
8
- selfId;
9
- now;
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 f, p = n.timeoutMs ?? 3e4, m = Math.max(1e3, Math.floor(n.sweepIntervalMs ?? p / 3)), h = u({
201
- base: b,
202
- alias: n.alias,
203
- instance: n.name
204
- }), g, _, v, x, S = () => {
205
- x === void 0 && g !== void 0 && (x = setInterval(() => {
206
- let e = g;
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(p);
25
+ let t = e.tracker.sweep(a);
209
26
  if (t.length !== 0) {
210
27
  for (let n of t) e.announce(n);
211
- T();
28
+ O();
212
29
  }
213
- }, m), x.unref?.());
214
- }, C = (e, t) => typeof t == "string" && t.length > 0 ? t : e.request.subject?.id ?? null, w = (e) => e.request.subject?.tenantId ?? null, T = () => {
215
- d(_, o);
216
- }, E = [
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
- ...r,
35
+ ...o,
219
36
  description: "Register/refresh the caller's presence in a channel.",
220
- execute: (r, i) => c.gen(function* () {
221
- let a = r;
222
- if (!g) return yield* c.fail(new t({}));
223
- let o = C(i, a.key);
224
- if (o === null) return yield* c.fail(new e({ channel: a.channel }));
225
- let s = n.resolveMember !== void 0 && v !== void 0 ? yield* c.promise(async () => n.resolveMember({
226
- subject: i.request.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: v
230
- })) : void 0, l = n.resolveMember !== void 0 && n.identityFields !== void 0 ? Object.fromEntries(Object.entries(a.meta ?? {}).filter(([e]) => !n.identityFields.includes(e))) : a.meta, u = s === void 0 ? l : {
231
- ...l ?? {},
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
- }, d = g.tracker.track({
50
+ }, u = v.tracker.track({
234
51
  channel: a.channel,
235
- tenantId: w(i),
52
+ tenantId: D(n),
236
53
  key: o,
237
- ...u === void 0 ? {} : { meta: u }
54
+ ...l === void 0 ? {} : { meta: l }
238
55
  });
239
- return g.announce(d.delta), d.changed && T(), { ok: !0 };
56
+ return v.announce(u.delta), u.changed && O(), { ok: !0 };
240
57
  })
241
58
  },
242
59
  {
243
- ...a,
60
+ ...c,
244
61
  description: "The online roster of a channel (heartbeat-fresh members). Push-driven — updates live.",
245
- execute: (e, n) => c.gen(function* () {
246
- return g ? g.tracker.roster(e.channel, w(n)).map((e) => ({
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* c.fail(new t({}));
66
+ })) : yield* d.fail(new i({}));
250
67
  })
251
68
  },
252
69
  {
253
- ...i,
70
+ ...s,
254
71
  description: "Remove the caller's presence from a channel.",
255
- execute: (n, r) => c.gen(function* () {
256
- let i = n;
257
- if (!g) return yield* c.fail(new t({}));
258
- let a = C(r, i.key);
259
- if (a === null) return yield* c.fail(new e({ channel: i.channel }));
260
- let o = g.tracker.untrack({
261
- channel: i.channel,
262
- tenantId: w(r),
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 g.announce(o.delta), o.changed && T(), { ok: !0 };
82
+ return v.announce(o.delta), o.changed && O(), { ok: !0 };
266
83
  })
267
84
  }
268
85
  ];
269
- return l({
270
- name: h,
271
- baseName: b,
86
+ return f({
87
+ name: _,
88
+ baseName: h,
89
+ workloads: ["rpc", "presence"],
272
90
  description: "Ephemeral realtime presence — heartbeat roster per channel.",
273
- routes: E,
274
- rpcClientDescriptors: s,
91
+ routes: k,
92
+ rpcClientDescriptors: u,
275
93
  bindDataStore: (e, t) => {
276
- _ = e, v = e;
277
- let n = t;
278
- g = y({
279
- instanceId: n?.instanceId ?? `presence-${process.pid}`,
280
- membership: n?.membership ?? { onChange: () => () => {} },
281
- ...n?.broadcast === void 0 ? {} : { provider: n.broadcast },
282
- ...n?.broadcastChannels?.presence === void 0 ? {} : { channel: n.broadcastChannels.presence },
283
- onRosterChanged: T
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) => c.sync(() => {
287
- S(), e.logger.info("presence active", {
288
- timeoutMs: p,
289
- sweepEveryMs: m
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 t = Number(process.env.VOLTRO_PRESENCE_BROKER_GRACE_MS ?? 5e3), n = setTimeout(() => {
292
- let t = g?.hasTransport === !0;
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
- }, t);
295
- n.unref?.(), f = n;
114
+ }, n);
115
+ r.unref?.(), t = r;
296
116
  }),
297
- onDeactivate: () => c.sync(() => {
298
- f !== void 0 && (clearTimeout(f), f = void 0), x !== void 0 && (clearInterval(x), x = void 0), g?.detach(), g = void 0, _ = void 0, v = void 0;
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 { g as PRESENCE_BROADCAST_CHANNEL, e as PresenceKeyMissing, t as PresenceStoreUnavailable, h as PresenceTracker, y as attachPresenceBus, x as presencePlugin, o as presenceRoster };
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 };
@@ -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
- if (b) {
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.85.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.85.0",
52
- "@voltro/protocol": "0.85.0"
51
+ "@voltro/client": "0.87.0",
52
+ "@voltro/protocol": "0.87.0"
53
53
  },
54
54
  "peerDependencies": {
55
- "@voltro/database": "0.85.0",
55
+ "@voltro/database": "0.87.0",
56
56
  "effect": "4.0.1",
57
57
  "react": "^19.0.0"
58
58
  },