@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/CHANGELOG.md +639 -2
- package/dist/index.js +61 -61
- package/dist/rpc.d.ts +12 -0
- package/dist/rpc.js +8 -8
- package/dist/web.js +96 -71
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { PresenceKeyMissing as e, PresenceStoreUnavailable as t } from "./errors.js";
|
|
2
|
-
import {
|
|
3
|
-
import { Effect as
|
|
4
|
-
import { definePlugin as
|
|
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
|
|
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 =
|
|
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 || !
|
|
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 || !
|
|
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 =
|
|
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
|
-
},
|
|
127
|
-
let t = new
|
|
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",
|
|
146
|
-
a !== void 0 &&
|
|
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 &&
|
|
149
|
+
n.length !== 0 && s({
|
|
150
150
|
owner: e.instanceId,
|
|
151
151
|
sync: n
|
|
152
152
|
});
|
|
153
153
|
}, u;
|
|
154
|
-
return a !== void 0 &&
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
186
|
-
if (
|
|
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 =
|
|
191
|
-
base:
|
|
192
|
-
alias:
|
|
193
|
-
instance:
|
|
194
|
-
}), g, _,
|
|
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
|
-
|
|
205
|
+
d(_, o);
|
|
206
206
|
}, E = [
|
|
207
207
|
{
|
|
208
|
-
...
|
|
208
|
+
...r,
|
|
209
209
|
description: "Register/refresh the caller's presence in a channel.",
|
|
210
|
-
execute: (
|
|
211
|
-
let
|
|
212
|
-
if (!g) return yield*
|
|
213
|
-
let
|
|
214
|
-
if (
|
|
215
|
-
let
|
|
216
|
-
subject:
|
|
217
|
-
channel:
|
|
218
|
-
key:
|
|
219
|
-
store:
|
|
220
|
-
})) : void 0,
|
|
221
|
-
...
|
|
222
|
-
...
|
|
223
|
-
},
|
|
224
|
-
channel:
|
|
225
|
-
tenantId: w(
|
|
226
|
-
key:
|
|
227
|
-
...
|
|
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(
|
|
229
|
+
return g.announce(d.delta), d.changed && T(), { ok: !0 };
|
|
230
230
|
})
|
|
231
231
|
},
|
|
232
232
|
{
|
|
233
|
-
...
|
|
233
|
+
...a,
|
|
234
234
|
description: "The online roster of a channel (heartbeat-fresh members). Push-driven — updates live.",
|
|
235
|
-
execute: (e, n) =>
|
|
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*
|
|
239
|
+
})) : yield* c.fail(new t({}));
|
|
240
240
|
})
|
|
241
241
|
},
|
|
242
242
|
{
|
|
243
|
-
...
|
|
243
|
+
...i,
|
|
244
244
|
description: "Remove the caller's presence from a channel.",
|
|
245
|
-
execute: (n, r) =>
|
|
245
|
+
execute: (n, r) => c.gen(function* () {
|
|
246
246
|
let i = n;
|
|
247
|
-
if (!g) return yield*
|
|
247
|
+
if (!g) return yield* c.fail(new t({}));
|
|
248
248
|
let a = C(r, i.key);
|
|
249
|
-
if (a === null) return yield*
|
|
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
|
|
259
|
+
return l({
|
|
260
260
|
name: h,
|
|
261
|
-
baseName:
|
|
261
|
+
baseName: b,
|
|
262
262
|
description: "Ephemeral realtime presence — heartbeat roster per channel.",
|
|
263
263
|
routes: E,
|
|
264
|
-
rpcClientDescriptors:
|
|
264
|
+
rpcClientDescriptors: s,
|
|
265
265
|
bindDataStore: (e, t) => {
|
|
266
|
-
_ = e,
|
|
266
|
+
_ = e, v = e;
|
|
267
267
|
let n = t;
|
|
268
|
-
g =
|
|
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) =>
|
|
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: () =>
|
|
288
|
-
f !== void 0 && (clearTimeout(f), f = void 0), x !== void 0 && (clearInterval(x), x = void 0), g?.detach(), g = void 0, _ = 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 {
|
|
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"),
|
|
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
|
-
}),
|
|
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
|
-
}),
|
|
24
|
+
}), u = i({
|
|
25
25
|
name: "presence.list",
|
|
26
|
-
source:
|
|
26
|
+
source: s,
|
|
27
27
|
input: n.Struct({ channel: n.String }),
|
|
28
|
-
output: n.Array(
|
|
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
|
-
}),
|
|
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
|
-
}),
|
|
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 {
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
|
5
|
-
let { meta: o, key:
|
|
6
|
-
|
|
7
|
-
let
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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(
|
|
21
|
-
channel:
|
|
22
|
-
...
|
|
31
|
+
clearInterval(t), g.current.mutate({
|
|
32
|
+
channel: e,
|
|
33
|
+
...u ? { key: u } : {}
|
|
23
34
|
});
|
|
24
35
|
};
|
|
25
36
|
}, [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
])
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
channel:
|
|
41
|
-
...
|
|
64
|
+
_.current.mutate({
|
|
65
|
+
channel: f,
|
|
66
|
+
...n ? { key: n } : {}
|
|
42
67
|
});
|
|
43
68
|
};
|
|
44
69
|
e();
|
|
45
|
-
let t = setInterval(e,
|
|
70
|
+
let t = setInterval(e, i);
|
|
46
71
|
return () => {
|
|
47
|
-
clearInterval(t),
|
|
48
|
-
channel:
|
|
49
|
-
...
|
|
72
|
+
clearInterval(t), v.current.mutate({
|
|
73
|
+
channel: f,
|
|
74
|
+
...n ? { key: n } : {}
|
|
50
75
|
});
|
|
51
76
|
};
|
|
52
77
|
}, [
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
h,
|
|
79
|
+
b,
|
|
80
|
+
f,
|
|
81
|
+
n,
|
|
82
|
+
i
|
|
58
83
|
]), {
|
|
59
|
-
active:
|
|
60
|
-
isTyping:
|
|
61
|
-
start: () =>
|
|
62
|
-
stop: () =>
|
|
84
|
+
active: d(y ?? [], n),
|
|
85
|
+
isTyping: h,
|
|
86
|
+
start: () => g(!0),
|
|
87
|
+
stop: () => g(!1)
|
|
63
88
|
};
|
|
64
|
-
},
|
|
65
|
-
let { selfKey:
|
|
66
|
-
|
|
67
|
-
let { data:
|
|
68
|
-
return
|
|
69
|
-
for (let e of
|
|
70
|
-
if (e.key ===
|
|
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" &&
|
|
73
|
-
|
|
74
|
-
for (let e of
|
|
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
|
-
}, [
|
|
102
|
+
}, [f, n]), i(() => ({
|
|
78
103
|
kind: "presence-channel",
|
|
79
|
-
publish: (
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
channel:
|
|
83
|
-
key:
|
|
84
|
-
meta: { lf:
|
|
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: (
|
|
88
|
-
if (
|
|
89
|
-
return
|
|
90
|
-
|
|
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
|
-
}), [
|
|
118
|
+
}), [e, n]);
|
|
94
119
|
};
|
|
95
120
|
//#endregion
|
|
96
|
-
export {
|
|
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.
|
|
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.
|
|
52
|
-
"@voltro/protocol": "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.
|
|
57
|
+
"@voltro/database": "0.56.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependenciesMeta": {
|
|
60
60
|
"@voltro/database": {
|