@shadow-js/realtime 1.0.2 → 1.0.4
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 +30 -14
- package/dist/dev/client/core.js +7 -5
- package/dist/dev/client/core.js.map +1 -1
- package/dist/dev/client/util.js +1 -15
- package/dist/dev/client/util.js.map +1 -1
- package/dist/dev/protocol.js +6 -15
- package/dist/dev/protocol.js.map +1 -1
- package/dist/dev/server/hub.d.ts +1 -1
- package/dist/dev/server/hub.js +128 -124
- package/dist/dev/server/hub.js.map +1 -1
- package/dist/prod/client/core.js +1 -1
- package/dist/prod/client/core.js.map +1 -1
- package/dist/prod/client/util.js +1 -1
- package/dist/prod/client/util.js.map +1 -1
- package/dist/prod/protocol.js +1 -1
- package/dist/prod/protocol.js.map +1 -1
- package/dist/prod/server/hub.js +1 -1
- package/dist/prod/server/hub.js.map +1 -1
- package/package.json +5 -5
package/dist/dev/server/hub.js
CHANGED
|
@@ -50,6 +50,10 @@ function createHubCore(options = {}) {
|
|
|
50
50
|
const rules = [];
|
|
51
51
|
const conns = /* @__PURE__ */ new Map();
|
|
52
52
|
const chans = /* @__PURE__ */ new Map();
|
|
53
|
+
/** Subscribes whose rules are being awaited: a revoke issued meanwhile takes them out, so the grant cannot outlive it. */
|
|
54
|
+
const judging = /* @__PURE__ */ new Set();
|
|
55
|
+
/** The tail of each channel's presence joins that are queued or running; an entry leaves when its chain is idle. */
|
|
56
|
+
const joinChains = /* @__PURE__ */ new Map();
|
|
53
57
|
let heartbeat;
|
|
54
58
|
let closing = false;
|
|
55
59
|
let beating = false;
|
|
@@ -90,7 +94,7 @@ function createHubCore(options = {}) {
|
|
|
90
94
|
where: `rule.${op}`,
|
|
91
95
|
channel
|
|
92
96
|
});
|
|
93
|
-
throw new RealtimeError("unavailable", "the channel rule could not be evaluated");
|
|
97
|
+
throw new RealtimeError("unavailable", ...["the channel rule could not be evaluated"]);
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
function send(conn, text) {
|
|
@@ -152,11 +156,7 @@ function createHubCore(options = {}) {
|
|
|
152
156
|
chan = {
|
|
153
157
|
name,
|
|
154
158
|
subs: /* @__PURE__ */ new Set(),
|
|
155
|
-
bucket: new TokenBucket(limits.channelPublishesPerSecond, limits.channelPublishesPerSecond, now())
|
|
156
|
-
adapterSub: void 0,
|
|
157
|
-
presenceSub: void 0,
|
|
158
|
-
closing: false,
|
|
159
|
-
frame: void 0
|
|
159
|
+
bucket: new TokenBucket(limits.channelPublishesPerSecond, limits.channelPublishesPerSecond, now())
|
|
160
160
|
};
|
|
161
161
|
chans.set(name, chan);
|
|
162
162
|
}
|
|
@@ -181,32 +181,31 @@ function createHubCore(options = {}) {
|
|
|
181
181
|
else sub.buffer.push(message);
|
|
182
182
|
continue;
|
|
183
183
|
}
|
|
184
|
-
if (message.seq === sub.
|
|
185
|
-
else if (message.seq > sub.
|
|
184
|
+
if (message.seq === sub.seq + 1) deliver(sub, message, false);
|
|
185
|
+
else if (message.seq > sub.seq + 1) {
|
|
186
186
|
sub.buffer.push(message);
|
|
187
187
|
pump(sub, void 0);
|
|
188
|
-
} else if (message.at > sub.
|
|
188
|
+
} else if (message.at > sub.at) removeSub(sub, "reset");
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
function deliver(sub, message, replayed) {
|
|
192
|
-
sub.
|
|
193
|
-
sub.
|
|
192
|
+
sub.seq = message.seq;
|
|
193
|
+
sub.at = Math.max(sub.at, message.at);
|
|
194
194
|
if (!sub.echo && message.publisher === sub.conn.id) return;
|
|
195
195
|
counters.delivered += 1;
|
|
196
196
|
if (replayed) {
|
|
197
197
|
send(sub.conn, messageFrame(sub.channel.name, message, true));
|
|
198
198
|
return;
|
|
199
199
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (cached === void 0 || cached.seq !== message.seq) sub.channel.frame = {
|
|
200
|
+
let cached = sub.channel.frame;
|
|
201
|
+
if (cached === void 0 || cached.seq !== message.seq) cached = sub.channel.frame = {
|
|
203
202
|
seq: message.seq,
|
|
204
|
-
text
|
|
203
|
+
text: messageFrame(sub.channel.name, message, false)
|
|
205
204
|
};
|
|
206
|
-
send(sub.conn, text);
|
|
205
|
+
send(sub.conn, cached.text);
|
|
207
206
|
}
|
|
208
207
|
/**
|
|
209
|
-
* Delivers, in order and without a gap, the messages after `sub.
|
|
208
|
+
* Delivers, in order and without a gap, the messages after `sub.seq` up to `upTo` and whatever is buffered. Fetches the
|
|
210
209
|
* missing ones from history. When history no longer has them: a subscription that reported a gap skips ahead; any other one
|
|
211
210
|
* is removed with `unsubbed why:"gap"`.
|
|
212
211
|
*/
|
|
@@ -215,24 +214,24 @@ function createHubCore(options = {}) {
|
|
|
215
214
|
sub.pumping = true;
|
|
216
215
|
try {
|
|
217
216
|
while (sub.active) {
|
|
218
|
-
sub.buffer = sub.buffer.filter((message) => message.seq > sub.
|
|
217
|
+
sub.buffer = sub.buffer.filter((message) => message.seq > sub.seq);
|
|
219
218
|
const next = sub.buffer[0];
|
|
220
|
-
if (next !== void 0 && next.seq === sub.
|
|
219
|
+
if (next !== void 0 && next.seq === sub.seq + 1) {
|
|
221
220
|
deliver(sub, next, false);
|
|
222
221
|
sub.buffer.shift();
|
|
223
222
|
continue;
|
|
224
223
|
}
|
|
225
224
|
const target = next === void 0 ? upTo ?? 0 : next.seq - 1;
|
|
226
|
-
if (sub.
|
|
227
|
-
const page = await adapter.history(sub.channel.name, sub.
|
|
225
|
+
if (sub.seq >= target) break;
|
|
226
|
+
const page = await adapter.history(sub.channel.name, sub.seq, Math.min(REPLAY_PAGE, target - sub.seq));
|
|
228
227
|
if (!sub.active) return;
|
|
229
228
|
const first = page.messages[0];
|
|
230
|
-
if (first === void 0 || first.seq !== sub.
|
|
229
|
+
if (first === void 0 || first.seq !== sub.seq + 1) {
|
|
231
230
|
if (!sub.gapOk) {
|
|
232
231
|
removeSub(sub, "gap");
|
|
233
232
|
return;
|
|
234
233
|
}
|
|
235
|
-
sub.
|
|
234
|
+
sub.seq = (first?.seq ?? target + 1) - 1;
|
|
236
235
|
continue;
|
|
237
236
|
}
|
|
238
237
|
for (const message of page.messages) {
|
|
@@ -259,7 +258,7 @@ function createHubCore(options = {}) {
|
|
|
259
258
|
sub.state = event.member.s;
|
|
260
259
|
}
|
|
261
260
|
if (!sub.active || !sub.joined && !sub.watch) continue;
|
|
262
|
-
if (sub.
|
|
261
|
+
if (sub.queued !== void 0) sub.queued.push(event);
|
|
263
262
|
else sendPresence(sub, event);
|
|
264
263
|
}
|
|
265
264
|
}
|
|
@@ -310,7 +309,7 @@ function createHubCore(options = {}) {
|
|
|
310
309
|
if (chan.subs.size > 0 || chan.closing) return;
|
|
311
310
|
chan.closing = true;
|
|
312
311
|
if (chans.get(chan.name) === chan) chans.delete(chan.name);
|
|
313
|
-
const stops = [chan.
|
|
312
|
+
const stops = [chan.feed, chan.pfeed].filter((item) => item !== void 0);
|
|
314
313
|
await Promise.all(stops.map(async (stop) => {
|
|
315
314
|
try {
|
|
316
315
|
await (await stop)();
|
|
@@ -327,41 +326,46 @@ function createHubCore(options = {}) {
|
|
|
327
326
|
}
|
|
328
327
|
async function subscribe(conn, frame) {
|
|
329
328
|
const { c: name, n } = frame;
|
|
330
|
-
if (conn.subs.has(name)) return fail(conn, n, "conflict", name, "already subscribed");
|
|
331
|
-
if (conn.subs.size >= limits.maxChannelsPerConnection) return fail(conn, n, "limit", name, "too many channels on this connection");
|
|
329
|
+
if (conn.subs.has(name)) return fail(conn, n, "conflict", name, ...["already subscribed"]);
|
|
330
|
+
if (conn.subs.size >= limits.maxChannelsPerConnection) return fail(conn, n, "limit", name, ...["too many channels on this connection"]);
|
|
332
331
|
const found = match(name);
|
|
333
|
-
if (found === void 0
|
|
332
|
+
if (found === void 0) return fail(conn, n, identityDenied(conn), name);
|
|
334
333
|
const wantsPresence = frame.hasPresence || frame.watch === true;
|
|
335
|
-
|
|
336
|
-
|
|
334
|
+
const pending = {
|
|
335
|
+
conn,
|
|
336
|
+
channel: name
|
|
337
|
+
};
|
|
338
|
+
judging.add(pending);
|
|
339
|
+
try {
|
|
340
|
+
if (!await allowed(found.rule, "subscribe", name, found.params, conn.identity) || wantsPresence && !await allowed(found.rule, "presence", name, found.params, conn.identity) || !judging.has(pending)) return fail(conn, n, identityDenied(conn), name);
|
|
341
|
+
} finally {
|
|
342
|
+
judging.delete(pending);
|
|
343
|
+
}
|
|
344
|
+
if (frame.hasPresence && byteLength(JSON.stringify(frame.presence ?? null)) > limits.maxStateBytes) return fail(conn, n, "too_large", name, ...["the presence state is too large"]);
|
|
337
345
|
if (conn.closed) return;
|
|
338
346
|
const chan = channelState(name);
|
|
339
|
-
if (chan.subs.size >= limits.maxSubscribersPerChannel) return fail(conn, n, "limit", name, "the channel is full");
|
|
347
|
+
if (chan.subs.size >= limits.maxSubscribersPerChannel) return fail(conn, n, "limit", name, ...["the channel is full"]);
|
|
340
348
|
const sub = {
|
|
341
349
|
conn,
|
|
342
350
|
channel: chan,
|
|
343
|
-
|
|
344
|
-
params: found.params,
|
|
351
|
+
...found,
|
|
345
352
|
echo: frame.echo !== false,
|
|
346
353
|
watch: frame.watch === true,
|
|
347
|
-
joined: false,
|
|
348
354
|
version: 0,
|
|
349
355
|
state: null,
|
|
350
|
-
|
|
351
|
-
|
|
356
|
+
seq: 0,
|
|
357
|
+
at: now(),
|
|
352
358
|
pumping: true,
|
|
353
|
-
gapOk: false,
|
|
354
359
|
buffer: [],
|
|
355
|
-
|
|
356
|
-
joining: void 0,
|
|
360
|
+
queued: wantsPresence ? [] : void 0,
|
|
357
361
|
active: true
|
|
358
362
|
};
|
|
359
363
|
conn.subs.set(name, sub);
|
|
360
364
|
chan.subs.add(sub);
|
|
361
365
|
try {
|
|
362
|
-
await ensureFeed(chan, "
|
|
366
|
+
await ensureFeed(chan, "feed", () => adapter.subscribe(chan.name, (message) => onMessage(chan, message)));
|
|
363
367
|
if (wantsPresence) {
|
|
364
|
-
await ensureFeed(chan, "
|
|
368
|
+
await ensureFeed(chan, "pfeed", () => adapter.presenceSubscribe(chan.name, (event) => onPresence(chan, event)));
|
|
365
369
|
startHeartbeat();
|
|
366
370
|
}
|
|
367
371
|
const probe = await adapter.history(name, Number.MAX_SAFE_INTEGER, 0);
|
|
@@ -370,12 +374,12 @@ function createHubCore(options = {}) {
|
|
|
370
374
|
const after = frame.after;
|
|
371
375
|
let gap = false;
|
|
372
376
|
let reset = false;
|
|
373
|
-
if (after === void 0) sub.
|
|
377
|
+
if (after === void 0) sub.seq = head;
|
|
374
378
|
else if (after > head) {
|
|
375
379
|
reset = true;
|
|
376
|
-
sub.
|
|
380
|
+
sub.seq = head;
|
|
377
381
|
} else {
|
|
378
|
-
sub.
|
|
382
|
+
sub.seq = after;
|
|
379
383
|
gap = probe.oldest === 0 ? head > after : probe.oldest > after + 1;
|
|
380
384
|
sub.gapOk = gap;
|
|
381
385
|
}
|
|
@@ -407,19 +411,34 @@ function createHubCore(options = {}) {
|
|
|
407
411
|
}
|
|
408
412
|
}
|
|
409
413
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
414
|
+
/** Joins one at a time per channel: the capacity check reads the set, so two joins in flight would both see room. */
|
|
415
|
+
function joinPresence(sub, state) {
|
|
416
|
+
const name = sub.channel.name;
|
|
417
|
+
const run = (joinChains.get(name) ?? Promise.resolve()).then(async () => {
|
|
418
|
+
if (!sub.active) return;
|
|
419
|
+
if ((await adapter.presenceList(name)).length >= limits.maxPresenceMembers) throw new RealtimeError("limit", ...["the presence set is full"]);
|
|
420
|
+
sub.version = 1;
|
|
421
|
+
sub.state = state;
|
|
422
|
+
sub.joined = true;
|
|
423
|
+
startHeartbeat();
|
|
424
|
+
await adapter.presenceSet(name, memberOf(sub), limits.presenceTtlMs);
|
|
425
|
+
});
|
|
426
|
+
const settle = () => {
|
|
427
|
+
if (joinChains.get(name) === tail) joinChains.delete(name);
|
|
428
|
+
};
|
|
429
|
+
const tail = run.then(settle, settle);
|
|
430
|
+
joinChains.set(name, tail);
|
|
431
|
+
return run;
|
|
432
|
+
}
|
|
433
|
+
/** The presence member a joined subscription stands for. */
|
|
434
|
+
function memberOf(sub) {
|
|
435
|
+
const { user } = sub.conn.identity;
|
|
436
|
+
return {
|
|
417
437
|
id: sub.conn.id,
|
|
418
|
-
...
|
|
419
|
-
s: state,
|
|
420
|
-
v:
|
|
438
|
+
...user === void 0 ? {} : { u: user },
|
|
439
|
+
s: sub.state,
|
|
440
|
+
v: sub.version
|
|
421
441
|
};
|
|
422
|
-
await adapter.presenceSet(sub.channel.name, member, limits.presenceTtlMs);
|
|
423
442
|
}
|
|
424
443
|
async function syncPresence(sub) {
|
|
425
444
|
const members = await adapter.presenceList(sub.channel.name);
|
|
@@ -430,8 +449,8 @@ function createHubCore(options = {}) {
|
|
|
430
449
|
op: "sync",
|
|
431
450
|
m: members
|
|
432
451
|
});
|
|
433
|
-
const queued = sub.
|
|
434
|
-
sub.
|
|
452
|
+
const queued = sub.queued ?? [];
|
|
453
|
+
sub.queued = void 0;
|
|
435
454
|
const known = new Map(members.map((member) => [member.id, member.v]));
|
|
436
455
|
for (const event of queued) {
|
|
437
456
|
const version = known.get(event.member.id);
|
|
@@ -442,18 +461,12 @@ function createHubCore(options = {}) {
|
|
|
442
461
|
async function setPresence(conn, frame) {
|
|
443
462
|
const sub = conn.subs.get(frame.c);
|
|
444
463
|
if (sub === void 0 || !sub.joined) return fail(conn, void 0, "not_subscribed", frame.c);
|
|
445
|
-
if (byteLength(JSON.stringify(frame.s)) > limits.maxStateBytes) return fail(conn, void 0, "too_large", frame.c, "the presence state is too large");
|
|
464
|
+
if (byteLength(JSON.stringify(frame.s)) > limits.maxStateBytes) return fail(conn, void 0, "too_large", frame.c, ...["the presence state is too large"]);
|
|
446
465
|
if (!conn.publishBucket.take(now())) return limit(conn, void 0, frame.c);
|
|
447
466
|
sub.version += 1;
|
|
448
467
|
sub.state = frame.s;
|
|
449
|
-
const member = {
|
|
450
|
-
id: conn.id,
|
|
451
|
-
...conn.identity.user === void 0 ? {} : { u: conn.identity.user },
|
|
452
|
-
s: frame.s,
|
|
453
|
-
v: sub.version
|
|
454
|
-
};
|
|
455
468
|
try {
|
|
456
|
-
await adapter.presenceSet(frame.c,
|
|
469
|
+
await adapter.presenceSet(frame.c, memberOf(sub), limits.presenceTtlMs);
|
|
457
470
|
} catch (error) {
|
|
458
471
|
report(error, {
|
|
459
472
|
where: "presenceSet",
|
|
@@ -471,7 +484,7 @@ function createHubCore(options = {}) {
|
|
|
471
484
|
const { c: name, n } = frame;
|
|
472
485
|
const found = match(name);
|
|
473
486
|
if (found === void 0 || !await allowed(found.rule, "publish", name, found.params, conn.identity)) return fail(conn, n, identityDenied(conn), name);
|
|
474
|
-
if (frame.d.length > limits.maxMessageBytes) return fail(conn, n, "too_large", name, "the message is too large");
|
|
487
|
+
if (frame.d.length > limits.maxMessageBytes) return fail(conn, n, "too_large", name, ...["the message is too large"]);
|
|
475
488
|
if (!conn.publishBucket.take(now()) || chans.get(name)?.bucket.take(now()) === false) return limit(conn, n, name);
|
|
476
489
|
try {
|
|
477
490
|
const result = await adapter.publish(name, {
|
|
@@ -483,7 +496,7 @@ function createHubCore(options = {}) {
|
|
|
483
496
|
history: found.rule.history ?? limits.history,
|
|
484
497
|
historyTtlMs: limits.historyTtlMs,
|
|
485
498
|
idempotencyTtlMs: limits.idempotencyTtlMs,
|
|
486
|
-
...frame.k === void 0 ? {} : { key: frame.k }
|
|
499
|
+
...frame.k === void 0 ? {} : { key: `${frame.k}/${conn.identity.user === void 0 ? "a" : `u:${conn.identity.user}`}` }
|
|
487
500
|
});
|
|
488
501
|
counters.published += result.duplicate ? 0 : 1;
|
|
489
502
|
if (n !== void 0) sendFrame(conn, {
|
|
@@ -512,7 +525,7 @@ function createHubCore(options = {}) {
|
|
|
512
525
|
const sub = target;
|
|
513
526
|
const handler = sub.rule.handlers?.[frame.e];
|
|
514
527
|
if (handler === void 0) return fail(conn, n, "no_handler", name);
|
|
515
|
-
if (conn.pending.size >= limits.maxPendingRequests) return fail(conn, n, "limit", name, "too many requests in flight");
|
|
528
|
+
if (conn.pending.size >= limits.maxPendingRequests) return fail(conn, n, "limit", name, ...["too many requests in flight"]);
|
|
516
529
|
if (!conn.publishBucket.take(now())) return limit(conn, n, name);
|
|
517
530
|
const controller = new AbortController();
|
|
518
531
|
conn.pending.add(controller);
|
|
@@ -533,7 +546,7 @@ function createHubCore(options = {}) {
|
|
|
533
546
|
});
|
|
534
547
|
const reply = await Promise.race([run, aborted]) ?? null;
|
|
535
548
|
const text = JSON.stringify(reply);
|
|
536
|
-
if (text === void 0 || byteLength(text) > limits.maxMessageBytes) return fail(conn, n, "too_large", name, "the reply is too large");
|
|
549
|
+
if (text === void 0 || byteLength(text) > limits.maxMessageBytes) return fail(conn, n, "too_large", name, ...["the reply is too large"]);
|
|
537
550
|
send(conn, `{"t":"rep","n":${n},"d":${text}}`);
|
|
538
551
|
} catch (error) {
|
|
539
552
|
if (error instanceof RealtimeError) fail(conn, n, error.code, name, error.code === "timeout" || error.code === "closed" ? void 0 : error.message);
|
|
@@ -565,7 +578,7 @@ function createHubCore(options = {}) {
|
|
|
565
578
|
}
|
|
566
579
|
const frame = decoded.frame;
|
|
567
580
|
const n = "n" in frame ? frame.n : void 0;
|
|
568
|
-
if ((frame.t === "sub" || frame.t === "unsub") && !conn.
|
|
581
|
+
if ((frame.t === "sub" || frame.t === "unsub") && !conn.ctl.take(now()) && frame.t === "sub") return limit(conn, n, frame.c);
|
|
569
582
|
try {
|
|
570
583
|
switch (frame.t) {
|
|
571
584
|
case "hello": return;
|
|
@@ -655,43 +668,35 @@ function createHubCore(options = {}) {
|
|
|
655
668
|
if (beating) return;
|
|
656
669
|
beating = true;
|
|
657
670
|
try {
|
|
658
|
-
|
|
671
|
+
const refs = [];
|
|
672
|
+
const watched = /* @__PURE__ */ new Set();
|
|
673
|
+
for (const conn of conns.values()) for (const sub of conn.subs.values()) {
|
|
674
|
+
if (sub.joined) refs.push({
|
|
675
|
+
channel: sub.channel.name,
|
|
676
|
+
id: conn.id
|
|
677
|
+
});
|
|
678
|
+
if (sub.joined || sub.watch) watched.add(sub.channel.name);
|
|
679
|
+
}
|
|
680
|
+
if (refs.length === 0 && watched.size === 0) {
|
|
681
|
+
clearInterval(heartbeat);
|
|
682
|
+
heartbeat = void 0;
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
const missing = await adapter.presenceTouch(refs, limits.presenceTtlMs);
|
|
687
|
+
for (const ref of missing) {
|
|
688
|
+
const sub = conns.get(ref.id)?.subs.get(ref.channel);
|
|
689
|
+
if (sub === void 0 || !sub.joined) continue;
|
|
690
|
+
await adapter.presenceSet(ref.channel, memberOf(sub), limits.presenceTtlMs);
|
|
691
|
+
}
|
|
692
|
+
for (const channel of watched) await adapter.presenceReap(channel);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
report(error, { where: "presenceHeartbeat" });
|
|
695
|
+
}
|
|
659
696
|
} finally {
|
|
660
697
|
beating = false;
|
|
661
698
|
}
|
|
662
699
|
}
|
|
663
|
-
async function beatOnce() {
|
|
664
|
-
const refs = [];
|
|
665
|
-
const watched = /* @__PURE__ */ new Set();
|
|
666
|
-
for (const conn of conns.values()) for (const sub of conn.subs.values()) {
|
|
667
|
-
if (sub.joined) refs.push({
|
|
668
|
-
channel: sub.channel.name,
|
|
669
|
-
id: conn.id
|
|
670
|
-
});
|
|
671
|
-
if (sub.joined || sub.watch) watched.add(sub.channel.name);
|
|
672
|
-
}
|
|
673
|
-
if (refs.length === 0 && watched.size === 0) {
|
|
674
|
-
clearInterval(heartbeat);
|
|
675
|
-
heartbeat = void 0;
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
try {
|
|
679
|
-
const missing = await adapter.presenceTouch(refs, limits.presenceTtlMs);
|
|
680
|
-
for (const ref of missing) {
|
|
681
|
-
const sub = conns.get(ref.id)?.subs.get(ref.channel);
|
|
682
|
-
if (sub === void 0 || !sub.joined) continue;
|
|
683
|
-
await adapter.presenceSet(ref.channel, {
|
|
684
|
-
id: ref.id,
|
|
685
|
-
...sub.conn.identity.user === void 0 ? {} : { u: sub.conn.identity.user },
|
|
686
|
-
s: sub.state,
|
|
687
|
-
v: sub.version
|
|
688
|
-
}, limits.presenceTtlMs);
|
|
689
|
-
}
|
|
690
|
-
for (const channel of watched) await adapter.presenceReap(channel);
|
|
691
|
-
} catch (error) {
|
|
692
|
-
report(error, { where: "presenceHeartbeat" });
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
700
|
for (const [pattern, rule] of Object.entries(options.channels ?? {})) rules.push(compileRule(pattern, rule));
|
|
696
701
|
const core = {
|
|
697
702
|
limits,
|
|
@@ -704,8 +709,8 @@ function createHubCore(options = {}) {
|
|
|
704
709
|
return core;
|
|
705
710
|
},
|
|
706
711
|
connect(transport, identity = {}, connectOptions = {}) {
|
|
707
|
-
if (closing) throw new RealtimeError("closed", "the hub is closed");
|
|
708
|
-
let release
|
|
712
|
+
if (closing) throw new RealtimeError("closed", ...["the hub is closed"]);
|
|
713
|
+
let release;
|
|
709
714
|
const done = new Promise((resolve) => {
|
|
710
715
|
release = resolve;
|
|
711
716
|
});
|
|
@@ -718,14 +723,12 @@ function createHubCore(options = {}) {
|
|
|
718
723
|
subs: /* @__PURE__ */ new Map(),
|
|
719
724
|
queue: [],
|
|
720
725
|
publishBucket: connectOptions.publishBucket ?? new TokenBucket(limits.publishesPerSecond, limits.publishBurst, now()),
|
|
721
|
-
|
|
726
|
+
ctl: new TokenBucket(limits.publishesPerSecond, Math.max(limits.publishBurst, limits.maxChannelsPerConnection), now()),
|
|
722
727
|
stateless: connectOptions.stateless === true,
|
|
723
728
|
waiters: [],
|
|
724
729
|
invalid: new WindowCounter(10, 6e4),
|
|
725
730
|
limited: new WindowCounter(50, 1e4),
|
|
726
731
|
pending: /* @__PURE__ */ new Set(),
|
|
727
|
-
running: false,
|
|
728
|
-
closed: false,
|
|
729
732
|
release,
|
|
730
733
|
done
|
|
731
734
|
};
|
|
@@ -766,9 +769,9 @@ function createHubCore(options = {}) {
|
|
|
766
769
|
};
|
|
767
770
|
},
|
|
768
771
|
async publish(channel, event, data, publishOptions = {}) {
|
|
769
|
-
if (!isChannelName(channel)) throw new RealtimeError("bad_channel", `"${channel}" is not a valid channel name`);
|
|
770
|
-
if (!isEventName(event)) throw new RealtimeError("bad_frame", `"${event}" is not a valid event name`);
|
|
771
|
-
if (publishOptions.key !== void 0 && !isIdempotencyKey(publishOptions.key)) throw new RealtimeError("bad_frame", "the idempotency key is not valid");
|
|
772
|
+
if (!isChannelName(channel)) throw new RealtimeError("bad_channel", ...[`"${channel}" is not a valid channel name`]);
|
|
773
|
+
if (!isEventName(event)) throw new RealtimeError("bad_frame", ...[`"${event}" is not a valid event name`]);
|
|
774
|
+
if (publishOptions.key !== void 0 && !isIdempotencyKey(publishOptions.key)) throw new RealtimeError("bad_frame", ...["the idempotency key is not valid"]);
|
|
772
775
|
let text;
|
|
773
776
|
try {
|
|
774
777
|
text = JSON.stringify(data === void 0 ? null : data);
|
|
@@ -776,7 +779,7 @@ function createHubCore(options = {}) {
|
|
|
776
779
|
throw new TypeError("the data is not JSON", { cause: error });
|
|
777
780
|
}
|
|
778
781
|
if (text === void 0) throw new TypeError("the data is not JSON");
|
|
779
|
-
if (byteLength(text) > limits.maxMessageBytes) throw new RealtimeError("too_large", "the message is too large");
|
|
782
|
+
if (byteLength(text) > limits.maxMessageBytes) throw new RealtimeError("too_large", ...["the message is too large"]);
|
|
780
783
|
const found = match(channel);
|
|
781
784
|
const result = await adapter.publish(channel, {
|
|
782
785
|
event,
|
|
@@ -785,18 +788,18 @@ function createHubCore(options = {}) {
|
|
|
785
788
|
history: found?.rule.history ?? limits.history,
|
|
786
789
|
historyTtlMs: limits.historyTtlMs,
|
|
787
790
|
idempotencyTtlMs: limits.idempotencyTtlMs,
|
|
788
|
-
...publishOptions.key === void 0 ? {} : { key: publishOptions.key }
|
|
791
|
+
...publishOptions.key === void 0 ? {} : { key: `${publishOptions.key}/s` }
|
|
789
792
|
});
|
|
790
793
|
counters.published += result.duplicate ? 0 : 1;
|
|
791
794
|
return result;
|
|
792
795
|
},
|
|
793
796
|
async setMemberState(identity, channel, connectionId, state) {
|
|
794
|
-
if (identity.user === void 0) throw new RealtimeError("unauthorized", "presence changes over HTTP need a signed-in user");
|
|
797
|
+
if (identity.user === void 0) throw new RealtimeError("unauthorized", ...["presence changes over HTTP need a signed-in user"]);
|
|
795
798
|
const found = match(channel);
|
|
796
799
|
if (found === void 0 || !await allowed(found.rule, "presence", channel, found.params, identity)) throw new RealtimeError("forbidden");
|
|
797
|
-
if (byteLength(JSON.stringify(state)) > limits.maxStateBytes) throw new RealtimeError("too_large", "the presence state is too large");
|
|
800
|
+
if (byteLength(JSON.stringify(state)) > limits.maxStateBytes) throw new RealtimeError("too_large", ...["the presence state is too large"]);
|
|
798
801
|
const member = (await adapter.presenceList(channel)).find((item) => item.id === connectionId);
|
|
799
|
-
if (member === void 0 || member.u !== identity.user) throw new RealtimeError("forbidden", "that connection is not yours");
|
|
802
|
+
if (member === void 0 || member.u !== identity.user) throw new RealtimeError("forbidden", ...["that connection is not yours"]);
|
|
800
803
|
const local = conns.get(connectionId)?.subs.get(channel);
|
|
801
804
|
const version = Math.max(member.v, local?.version ?? 0) + 1;
|
|
802
805
|
if (local?.joined === true) {
|
|
@@ -812,15 +815,16 @@ function createHubCore(options = {}) {
|
|
|
812
815
|
},
|
|
813
816
|
revoke(target) {
|
|
814
817
|
if (target.user === void 0 && target.connection === void 0 && target.channel === void 0) throw new TypeError("revoke needs a user, a connection or a channel.");
|
|
818
|
+
const hit = (conn, channel) => (target.connection === void 0 || conn.id === target.connection) && (target.user === void 0 || conn.identity.user === target.user) && (target.channel === void 0 || channel === target.channel);
|
|
815
819
|
let count = 0;
|
|
816
|
-
for (const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
820
|
+
for (const item of judging) if (hit(item.conn, item.channel)) {
|
|
821
|
+
judging.delete(item);
|
|
822
|
+
count += 1;
|
|
823
|
+
}
|
|
824
|
+
for (const conn of [...conns.values()]) for (const sub of [...conn.subs.values()]) {
|
|
825
|
+
if (!hit(conn, sub.channel.name)) continue;
|
|
826
|
+
removeSub(sub, "revoked");
|
|
827
|
+
count += 1;
|
|
824
828
|
}
|
|
825
829
|
return count;
|
|
826
830
|
},
|