@voltro/plugin-presence 0.54.0 → 0.56.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,9 +1,9 @@
1
1
  import { PresenceKeyMissing as e, PresenceStoreUnavailable as t } from "./errors.js";
2
- import { heartbeatDescriptor as n, leaveDescriptor as r, listDescriptor as i, presenceRoster as a, presenceRpcClientImports as o } from "./rpc.js";
3
- import { Effect as s } from "effect";
4
- import { definePlugin as c, pluginInstanceName as l, publishReactivity as u } from "@voltro/protocol";
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
5
  //#region src/tracker.ts
6
- var d = (e, t) => `${e ?? "~"}::${t}`, f = (e) => e === void 0 ? "undefined" : typeof e != "object" || !e ? JSON.stringify(e) ?? "undefined" : Array.isArray(e) ? `[${e.map(f).join(",")}]` : `{${Object.entries(e).sort(([e], [t]) => e < t ? -1 : +(e > t)).map(([e, t]) => `${JSON.stringify(e)}:${f(t)}`).join(",")}}`, p = (e, t) => e === t || e !== null && t !== null && f(e) === f(t), m = class {
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
7
  partitions = /* @__PURE__ */ new Map();
8
8
  selfId;
9
9
  now;
@@ -16,7 +16,7 @@ var d = (e, t) => `${e ?? "~"}::${t}`, f = (e) => e === void 0 ? "undefined" : t
16
16
  branch(e, t, n) {
17
17
  let r = this.partitions.get(e);
18
18
  r === void 0 && (r = /* @__PURE__ */ new Map(), this.partitions.set(e, r));
19
- let i = d(t, n), a = r.get(i);
19
+ let i = f(t, n), a = r.get(i);
20
20
  return a === void 0 && (a = {
21
21
  tenantId: t,
22
22
  channel: n,
@@ -38,7 +38,7 @@ var d = (e, t) => `${e ?? "~"}::${t}`, f = (e) => e === void 0 ? "undefined" : t
38
38
  meta: n,
39
39
  lastSeen: t
40
40
  },
41
- changed: i === void 0 || !p(i.meta, n)
41
+ changed: i === void 0 || !m(i.meta, n)
42
42
  };
43
43
  }
44
44
  untrack(e) {
@@ -62,7 +62,7 @@ var d = (e, t) => `${e ?? "~"}::${t}`, f = (e) => e === void 0 ? "undefined" : t
62
62
  key: t.key,
63
63
  meta: t.meta,
64
64
  lastSeen: t.lastSeen
65
- }), r === void 0 || !p(r.meta, t.meta);
65
+ }), r === void 0 || !m(r.meta, t.meta);
66
66
  }
67
67
  dropOwner(e) {
68
68
  if (e === this.selfId) return 0;
@@ -98,7 +98,7 @@ var d = (e, t) => `${e ?? "~"}::${t}`, f = (e) => e === void 0 ? "undefined" : t
98
98
  return e;
99
99
  }
100
100
  roster(e, t) {
101
- let n = d(t, e), r = [];
101
+ let n = f(t, e), r = [];
102
102
  for (let [i, a] of this.partitions) {
103
103
  let o = a.get(n);
104
104
  if (o !== void 0) for (let n of o.members.values()) r.push({
@@ -123,8 +123,8 @@ var d = (e, t) => `${e ?? "~"}::${t}`, f = (e) => e === void 0 ? "undefined" : t
123
123
  size() {
124
124
  return this.owners().reduce((e, t) => e + t.members, 0);
125
125
  }
126
- }, h = "voltro:presence", g = (e) => typeof e == "object" && !!e && typeof e.owner == "string" && typeof e.delta == "object" && e.delta !== null, _ = (e) => typeof e == "object" && !!e && typeof e.owner == "string" && Array.isArray(e.sync), v = (e) => {
127
- let t = new m({
126
+ }, 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) => {
127
+ let t = new h({
128
128
  instanceId: e.instanceId,
129
129
  ...e.now === void 0 ? {} : { now: e.now }
130
130
  }), n = e.logger, r = () => {
@@ -142,29 +142,29 @@ var d = (e, t) => `${e ?? "~"}::${t}`, f = (e) => e === void 0 ? "undefined" : t
142
142
  members: i,
143
143
  reason: e.kind
144
144
  }), r()), e.kind === "restarted" && l();
145
- }), a = e.provider, o = e.channel ?? "voltro:presence", c = (e) => {
146
- a !== void 0 && s.runPromise(a.publish(o, JSON.stringify(e))).catch(() => {});
145
+ }), a = e.provider, o = e.channel ?? "voltro:presence", s = (e) => {
146
+ a !== void 0 && c.runPromise(a.publish(o, JSON.stringify(e))).catch(() => {});
147
147
  }, l = () => {
148
148
  let n = t.ownSnapshot();
149
- n.length !== 0 && c({
149
+ n.length !== 0 && s({
150
150
  owner: e.instanceId,
151
151
  sync: n
152
152
  });
153
153
  }, u;
154
- return a !== void 0 && s.runPromise(a.subscribe(o, (e) => {
154
+ return a !== void 0 && c.runPromise(a.subscribe(o, (e) => {
155
155
  let n;
156
156
  try {
157
157
  n = JSON.parse(e);
158
158
  } catch {
159
159
  return;
160
160
  }
161
- if (_(n)) {
161
+ if (v(n)) {
162
162
  let e = !1;
163
163
  for (let r of n.sync) e = t.apply(n.owner, r) || e;
164
164
  e && r();
165
165
  return;
166
166
  }
167
- g(n) && t.apply(n.owner, n.delta) && r();
167
+ _(n) && t.apply(n.owner, n.delta) && r();
168
168
  })).then((e) => {
169
169
  u = e;
170
170
  }, (e) => {
@@ -173,7 +173,7 @@ var d = (e, t) => `${e ?? "~"}::${t}`, f = (e) => e === void 0 ? "undefined" : t
173
173
  tracker: t,
174
174
  hasTransport: a !== void 0,
175
175
  announce: (t) => {
176
- c({
176
+ s({
177
177
  owner: e.instanceId,
178
178
  delta: t
179
179
  });
@@ -182,16 +182,16 @@ var d = (e, t) => `${e ?? "~"}::${t}`, f = (e) => e === void 0 ? "undefined" : t
182
182
  i(), u?.();
183
183
  }
184
184
  };
185
- }, y = "@voltro/plugin-presence", b = (d = {}) => {
186
- if (d.timeoutMs !== void 0 && d.timeoutMs < 1e3) throw Error(`presencePlugin: \`timeoutMs\` is ${d.timeoutMs}, which expires every member before it can heartbeat again.
185
+ }, b = n, x = (n = {}) => {
186
+ if (n.timeoutMs !== void 0 && n.timeoutMs < 1e3) throw Error(`presencePlugin: \`timeoutMs\` is ${n.timeoutMs}, which expires every member before it can heartbeat again.
187
187
  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
188
188
  \`heartbeatMs\`, comfortably. The default pair is 30s server / 15s client.
189
189
  There is no "never expire" spelling: omit \`timeoutMs\` for the default.`);
190
- let f, p = d.timeoutMs ?? 3e4, m = Math.max(1e3, Math.floor(d.sweepIntervalMs ?? p / 3)), h = l({
191
- base: y,
192
- alias: d.alias,
193
- instance: d.name
194
- }), g, _, b, x, S = () => {
190
+ let f, p = n.timeoutMs ?? 3e4, m = Math.max(1e3, Math.floor(n.sweepIntervalMs ?? p / 3)), h = u({
191
+ base: b,
192
+ alias: n.alias,
193
+ instance: n.name
194
+ }), g, _, v, x, S = () => {
195
195
  x === void 0 && g !== void 0 && (x = setInterval(() => {
196
196
  let e = g;
197
197
  if (e === void 0) return;
@@ -202,51 +202,51 @@ var d = (e, t) => `${e ?? "~"}::${t}`, f = (e) => e === void 0 ? "undefined" : t
202
202
  }
203
203
  }, m), x.unref?.());
204
204
  }, C = (e, t) => typeof t == "string" && t.length > 0 ? t : e.request.subject?.id ?? null, w = (e) => e.request.subject?.tenantId ?? null, T = () => {
205
- u(_, a);
205
+ d(_, o);
206
206
  }, E = [
207
207
  {
208
- ...n,
208
+ ...r,
209
209
  description: "Register/refresh the caller's presence in a channel.",
210
- execute: (n, r) => s.gen(function* () {
211
- let i = n;
212
- if (!g) return yield* s.fail(new t({}));
213
- let a = C(r, i.key);
214
- if (a === null) return yield* s.fail(new e({ channel: i.channel }));
215
- let o = d.resolveMember !== void 0 && b !== void 0 ? yield* s.promise(async () => d.resolveMember({
216
- subject: r.request.subject,
217
- channel: i.channel,
218
- key: a,
219
- store: b
220
- })) : void 0, c = d.resolveMember !== void 0 && d.identityFields !== void 0 ? Object.fromEntries(Object.entries(i.meta ?? {}).filter(([e]) => !d.identityFields.includes(e))) : i.meta, l = o === void 0 ? c : {
221
- ...c ?? {},
222
- ...o
223
- }, u = g.tracker.track({
224
- channel: i.channel,
225
- tenantId: w(r),
226
- key: a,
227
- ...l === void 0 ? {} : { meta: l }
210
+ execute: (r, i) => c.gen(function* () {
211
+ let a = r;
212
+ if (!g) return yield* c.fail(new t({}));
213
+ let o = C(i, a.key);
214
+ if (o === null) return yield* c.fail(new e({ channel: a.channel }));
215
+ let s = n.resolveMember !== void 0 && v !== void 0 ? yield* c.promise(async () => n.resolveMember({
216
+ subject: i.request.subject,
217
+ channel: a.channel,
218
+ key: o,
219
+ store: v
220
+ })) : 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 : {
221
+ ...l ?? {},
222
+ ...s
223
+ }, d = g.tracker.track({
224
+ channel: a.channel,
225
+ tenantId: w(i),
226
+ key: o,
227
+ ...u === void 0 ? {} : { meta: u }
228
228
  });
229
- return g.announce(u.delta), u.changed && T(), { ok: !0 };
229
+ return g.announce(d.delta), d.changed && T(), { ok: !0 };
230
230
  })
231
231
  },
232
232
  {
233
- ...i,
233
+ ...a,
234
234
  description: "The online roster of a channel (heartbeat-fresh members). Push-driven — updates live.",
235
- execute: (e, n) => s.gen(function* () {
235
+ execute: (e, n) => c.gen(function* () {
236
236
  return g ? g.tracker.roster(e.channel, w(n)).map((e) => ({
237
237
  key: e.key,
238
238
  meta: e.meta
239
- })) : yield* s.fail(new t({}));
239
+ })) : yield* c.fail(new t({}));
240
240
  })
241
241
  },
242
242
  {
243
- ...r,
243
+ ...i,
244
244
  description: "Remove the caller's presence from a channel.",
245
- execute: (n, r) => s.gen(function* () {
245
+ execute: (n, r) => c.gen(function* () {
246
246
  let i = n;
247
- if (!g) return yield* s.fail(new t({}));
247
+ if (!g) return yield* c.fail(new t({}));
248
248
  let a = C(r, i.key);
249
- if (a === null) return yield* s.fail(new e({ channel: i.channel }));
249
+ if (a === null) return yield* c.fail(new e({ channel: i.channel }));
250
250
  let o = g.tracker.untrack({
251
251
  channel: i.channel,
252
252
  tenantId: w(r),
@@ -256,16 +256,16 @@ var d = (e, t) => `${e ?? "~"}::${t}`, f = (e) => e === void 0 ? "undefined" : t
256
256
  })
257
257
  }
258
258
  ];
259
- return c({
259
+ return l({
260
260
  name: h,
261
- baseName: y,
261
+ baseName: b,
262
262
  description: "Ephemeral realtime presence — heartbeat roster per channel.",
263
263
  routes: E,
264
- rpcClientDescriptors: o,
264
+ rpcClientDescriptors: s,
265
265
  bindDataStore: (e, t) => {
266
- _ = e, b = e;
266
+ _ = e, v = e;
267
267
  let n = t;
268
- g = v({
268
+ g = y({
269
269
  instanceId: n?.instanceId ?? `presence-${process.pid}`,
270
270
  membership: n?.membership ?? { onChange: () => () => {} },
271
271
  ...n?.broadcast === void 0 ? {} : { provider: n.broadcast },
@@ -273,7 +273,7 @@ var d = (e, t) => `${e ?? "~"}::${t}`, f = (e) => e === void 0 ? "undefined" : t
273
273
  onRosterChanged: T
274
274
  });
275
275
  },
276
- onActivate: (e) => s.sync(() => {
276
+ onActivate: (e) => c.sync(() => {
277
277
  S(), e.logger.info("presence active", {
278
278
  timeoutMs: p,
279
279
  sweepEveryMs: m
@@ -284,10 +284,10 @@ var d = (e, t) => `${e ?? "~"}::${t}`, f = (e) => e === void 0 ? "undefined" : t
284
284
  }, t);
285
285
  n.unref?.(), f = n;
286
286
  }),
287
- onDeactivate: () => s.sync(() => {
288
- f !== void 0 && (clearTimeout(f), f = void 0), x !== void 0 && (clearInterval(x), x = void 0), g?.detach(), g = void 0, _ = void 0, b = void 0;
287
+ onDeactivate: () => c.sync(() => {
288
+ 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;
289
289
  })
290
290
  });
291
291
  };
292
292
  //#endregion
293
- export { h as PRESENCE_BROADCAST_CHANNEL, e as PresenceKeyMissing, t as PresenceStoreUnavailable, m as PresenceTracker, v as attachPresenceBus, b as presencePlugin, a as presenceRoster };
293
+ export { g as PRESENCE_BROADCAST_CHANNEL, e as PresenceKeyMissing, t as PresenceStoreUnavailable, h as PresenceTracker, y as attachPresenceBus, x as presencePlugin, o as presenceRoster };
package/dist/rpc.d.ts CHANGED
@@ -4,6 +4,18 @@ import { QueryProcedureDescriptor } from '@voltro/protocol';
4
4
  import { ReactivityChannel } from '@voltro/protocol';
5
5
  import { Schema } from 'effect';
6
6
 
7
+ /**
8
+ * The plugin's canonical package name — the key an app's `alias` is recorded
9
+ * under, and what `./web`'s hooks resolve their tags through.
10
+ *
11
+ * It lives in this BROWSER-SAFE module rather than in `index.ts` because both
12
+ * halves need it and only one of them may reach the server graph: `index.ts`
13
+ * passes it to `pluginInstanceName` / `baseName`, and `web.ts` passes it to
14
+ * `pluginTag`. A second copy in `web.ts` would be a second definition of the
15
+ * plugin's identity with nothing comparing them.
16
+ */
17
+ export declare const BASE_NAME = "@voltro/plugin-presence";
18
+
7
19
  export declare const heartbeatDescriptor: MutationProcedureDescriptor<"presence.heartbeat", Schema.Struct<{
8
20
  channel: typeof Schema.String;
9
21
  key: Schema.optional<typeof Schema.String>;
package/dist/rpc.js CHANGED
@@ -2,13 +2,13 @@ import { PresenceKeyMissing as e, PresenceStoreUnavailable as t } from "./errors
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
  //#region src/rpc.ts
5
- var o = a("presence"), s = n.Struct({
5
+ var o = "@voltro/plugin-presence", s = a("presence"), c = n.Struct({
6
6
  key: n.String,
7
7
  meta: n.NullOr(n.Record({
8
8
  key: n.String,
9
9
  value: n.Unknown
10
10
  }))
11
- }), c = r({
11
+ }), l = r({
12
12
  name: "presence.heartbeat",
13
13
  input: n.Struct({
14
14
  channel: n.String,
@@ -21,14 +21,14 @@ var o = a("presence"), s = n.Struct({
21
21
  output: n.Struct({ ok: n.Boolean }),
22
22
  error: n.Union(e, t),
23
23
  openAccess: "presence serves anonymous-capable apps; the tracked key defaults to the caller's own subject id (PresenceKeyMissing without one) and entries are tenant-scoped in the tracker"
24
- }), l = i({
24
+ }), u = i({
25
25
  name: "presence.list",
26
- source: o,
26
+ source: s,
27
27
  input: n.Struct({ channel: n.String }),
28
- output: n.Array(s),
28
+ output: n.Array(c),
29
29
  error: t,
30
30
  openAccess: "presence serves anonymous-capable apps; the roster is tenant-scoped fail-closed in the tracker (a caller sees only its own tenant's members) and carries only key + caller-published meta"
31
- }), u = r({
31
+ }), d = r({
32
32
  name: "presence.leave",
33
33
  input: n.Struct({
34
34
  channel: n.String,
@@ -37,7 +37,7 @@ var o = a("presence"), s = n.Struct({
37
37
  output: n.Struct({ ok: n.Boolean }),
38
38
  error: n.Union(e, t),
39
39
  openAccess: "presence serves anonymous-capable apps; the key to remove defaults to the caller's own subject id (PresenceKeyMissing without one) and removal is tenant-scoped in the tracker"
40
- }), d = [
40
+ }), f = [
41
41
  {
42
42
  tag: "presence.heartbeat",
43
43
  kind: "mutation",
@@ -64,4 +64,4 @@ var o = a("presence"), s = n.Struct({
64
64
  }
65
65
  ];
66
66
  //#endregion
67
- export { c as heartbeatDescriptor, u as leaveDescriptor, l as listDescriptor, o as presenceRoster, d as presenceRpcClientImports };
67
+ export { o as BASE_NAME, l as heartbeatDescriptor, d as leaveDescriptor, u as listDescriptor, s as presenceRoster, f as presenceRpcClientImports };
package/dist/web.js CHANGED
@@ -1,96 +1,121 @@
1
- import { useEffect as e, useMemo as t, useRef as n, useState as r } from "react";
2
- import { useMutation as i, useSubscription as a } from "@voltro/client";
1
+ import { BASE_NAME as e } from "./rpc.js";
2
+ import { pluginTag as t } from "@voltro/protocol";
3
+ import { useCallback as n, useEffect as r, useMemo as i, useRef as a, useState as o } from "react";
4
+ import { useMutation as s, useSubscription as c } from "@voltro/client";
3
5
  //#region src/web.ts
4
- var o = (t, r = {}) => {
5
- let { meta: o, key: s, heartbeatMs: c = 15e3, apiName: l = "app" } = r, u = i(l, "presence.heartbeat"), d = i(l, "presence.leave"), f = n(u), p = n(d);
6
- f.current = u, p.current = d;
7
- let { data: m } = a(l, "presence.list", { channel: t }), h = m !== void 0;
8
- return e(() => {
9
- if (!h) return;
10
- let e = () => {
11
- f.current.mutate({
12
- channel: t,
13
- ...s ? { key: s } : {},
14
- ...o ? { meta: o } : {}
15
- });
16
- };
17
- e();
18
- let n = setInterval(e, c);
6
+ var l = (n) => t(e, n), u = (e, t = {}) => {
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
+ h.current = p, g.current = m;
9
+ let _ = a(o);
10
+ _.current = o;
11
+ let v = i(() => {
12
+ if (o === void 0) return "";
13
+ try {
14
+ return JSON.stringify(o);
15
+ } catch {
16
+ return "";
17
+ }
18
+ }, [o]), { data: y } = c(f, l("list"), { channel: e }), b = y !== void 0, x = n(() => {
19
+ let t = _.current;
20
+ h.current.mutate({
21
+ channel: e,
22
+ ...u ? { key: u } : {},
23
+ ...t ? { meta: t } : {}
24
+ });
25
+ }, [e, u]);
26
+ r(() => {
27
+ if (!b) return;
28
+ x();
29
+ let t = setInterval(x, d);
19
30
  return () => {
20
- clearInterval(n), p.current.mutate({
21
- channel: t,
22
- ...s ? { key: s } : {}
31
+ clearInterval(t), g.current.mutate({
32
+ channel: e,
33
+ ...u ? { key: u } : {}
23
34
  });
24
35
  };
25
36
  }, [
26
- h,
27
- t,
28
- s,
29
- c,
30
- o
31
- ]), m ?? [];
32
- }, s = (e, t) => t === void 0 ? e : e.filter((e) => e.key !== t), c = (t, o = {}) => {
33
- let { selfKey: c, heartbeatMs: l = 3e3, apiName: u = "app" } = o, d = `typing:${t}`, f = i(u, "presence.heartbeat"), p = i(u, "presence.leave"), [m, h] = r(!1), g = n(f), _ = n(p);
34
- g.current = f, _.current = p;
35
- let { data: v } = a(u, "presence.list", { channel: d }), y = v !== void 0;
36
- return e(() => {
37
- if (!m || !y) return;
37
+ b,
38
+ e,
39
+ u,
40
+ d,
41
+ x
42
+ ]);
43
+ let S = a(void 0);
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
+ }
52
+ }, [
53
+ b,
54
+ v,
55
+ x
56
+ ]), y ?? [];
57
+ }, d = (e, t) => t === void 0 ? e : e.filter((e) => e.key !== t), f = (e, t = {}) => {
58
+ let { selfKey: n, heartbeatMs: i = 3e3, apiName: u = "app" } = t, f = `typing:${e}`, p = s(u, l("heartbeat")), m = s(u, l("leave")), [h, g] = o(!1), _ = a(p), v = a(m);
59
+ _.current = p, v.current = m;
60
+ let { data: y } = c(u, l("list"), { channel: f }), b = y !== void 0;
61
+ return r(() => {
62
+ if (!h || !b) return;
38
63
  let e = () => {
39
- g.current.mutate({
40
- channel: d,
41
- ...c ? { key: c } : {}
64
+ _.current.mutate({
65
+ channel: f,
66
+ ...n ? { key: n } : {}
42
67
  });
43
68
  };
44
69
  e();
45
- let t = setInterval(e, l);
70
+ let t = setInterval(e, i);
46
71
  return () => {
47
- clearInterval(t), _.current.mutate({
48
- channel: d,
49
- ...c ? { key: c } : {}
72
+ clearInterval(t), v.current.mutate({
73
+ channel: f,
74
+ ...n ? { key: n } : {}
50
75
  });
51
76
  };
52
77
  }, [
53
- m,
54
- y,
55
- d,
56
- c,
57
- l
78
+ h,
79
+ b,
80
+ f,
81
+ n,
82
+ i
58
83
  ]), {
59
- active: s(v ?? [], c),
60
- isTyping: m,
61
- start: () => h(!0),
62
- stop: () => h(!1)
84
+ active: d(y ?? [], n),
85
+ isTyping: h,
86
+ start: () => g(!0),
87
+ stop: () => g(!1)
63
88
  };
64
- }, l = (r, o) => {
65
- let { selfKey: s, apiName: c = "app" } = o, l = i(c, "presence.heartbeat"), u = n(l);
66
- u.current = l;
67
- let { data: d } = a(c, "presence.list", { channel: r }), f = n(/* @__PURE__ */ new Set()), p = n(/* @__PURE__ */ new Map());
68
- return e(() => {
69
- for (let e of d ?? []) {
70
- if (e.key === s) continue;
89
+ }, p = (e, t) => {
90
+ let { selfKey: n, apiName: o = "app" } = t, u = s(o, l("heartbeat")), d = a(u);
91
+ d.current = u;
92
+ let { data: f } = c(o, l("list"), { channel: e }), p = a(/* @__PURE__ */ new Set()), m = a(/* @__PURE__ */ new Map());
93
+ return r(() => {
94
+ for (let e of f ?? []) {
95
+ if (e.key === n) continue;
71
96
  let t = e.meta?.lf;
72
- if (typeof t == "string" && p.current.get(e.key) !== t) {
73
- p.current.set(e.key, t);
74
- for (let e of f.current) e(t);
97
+ if (typeof t == "string" && m.current.get(e.key) !== t) {
98
+ m.current.set(e.key, t);
99
+ for (let e of p.current) e(t);
75
100
  }
76
101
  }
77
- }, [d, s]), t(() => ({
102
+ }, [f, n]), i(() => ({
78
103
  kind: "presence-channel",
79
- publish: (e, t) => {
80
- if (e !== r) throw Error(`presence channel is bound to room "${r}", got "${e}"`);
81
- u.current.mutate({
82
- channel: r,
83
- key: s,
84
- meta: { lf: t }
104
+ publish: (t, r) => {
105
+ if (t !== e) throw Error(`presence channel is bound to room "${e}", got "${t}"`);
106
+ d.current.mutate({
107
+ channel: e,
108
+ key: n,
109
+ meta: { lf: r }
85
110
  });
86
111
  },
87
- subscribe: (e, t) => {
88
- if (e !== r) throw Error(`presence channel is bound to room "${r}", got "${e}"`);
89
- return f.current.add(t), () => {
90
- f.current.delete(t);
112
+ subscribe: (t, n) => {
113
+ if (t !== e) throw Error(`presence channel is bound to room "${e}", got "${t}"`);
114
+ return p.current.add(n), () => {
115
+ p.current.delete(n);
91
116
  };
92
117
  }
93
- }), [r, s]);
118
+ }), [e, n]);
94
119
  };
95
120
  //#endregion
96
- export { s as activeTypers, o as usePresence, l as usePresenceChannel, c as useTyping };
121
+ 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.54.0",
3
+ "version": "0.56.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,13 +48,13 @@
48
48
  "node": ">=24.0.0"
49
49
  },
50
50
  "dependencies": {
51
- "@voltro/client": "0.54.0",
52
- "@voltro/protocol": "0.54.0"
51
+ "@voltro/client": "0.56.0",
52
+ "@voltro/protocol": "0.56.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "effect": "^3.22.0",
56
56
  "react": "^19.0.0",
57
- "@voltro/database": "0.54.0"
57
+ "@voltro/database": "0.56.0"
58
58
  },
59
59
  "peerDependenciesMeta": {
60
60
  "@voltro/database": {