@shadow-js/realtime 1.0.3 → 1.0.5
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 +13 -0
- package/README.md +1 -1
- package/dist/dev/client/core.js +4 -4
- package/dist/dev/client/core.js.map +1 -1
- package/dist/dev/client/index.d.ts +1 -1
- package/dist/dev/client/util.js +1 -15
- package/dist/dev/client/util.js.map +1 -1
- package/dist/dev/cloudflare/adapter.js +1 -1
- package/dist/dev/cloudflare/object.js +1 -1
- package/dist/dev/cloudflare.d.ts +1 -1
- package/dist/dev/hooks.d.ts +1 -1
- package/dist/dev/index.d.ts +1 -1
- package/dist/dev/protocol.js +6 -15
- package/dist/dev/protocol.js.map +1 -1
- package/dist/dev/server/hub.d.ts +2 -2
- package/dist/dev/server/hub.js +90 -107
- package/dist/dev/server/hub.js.map +1 -1
- package/dist/dev/server/index.d.ts +3 -3
- package/dist/dev/server/index.js +2 -2
- package/dist/dev/testing.d.ts +2 -2
- 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/cloudflare/adapter.js +2 -2
- package/dist/prod/cloudflare/object.js +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/dist/prod/server/index.js +1 -1
- package/package.json +6 -6
package/dist/dev/server/hub.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RealtimeError } from "../errors.js";
|
|
2
|
+
import { memoryAdapter } from "../memory.js";
|
|
2
3
|
import { byteLength, decodeClientFrame, isChannelName, isEventName, isIdempotencyKey } from "../protocol.js";
|
|
3
4
|
import { TokenBucket, WindowCounter, resolveLimits } from "./limits.js";
|
|
4
|
-
import { memoryAdapter } from "../memory.js";
|
|
5
5
|
import { compileRule, matchRule } from "./rules.js";
|
|
6
6
|
// src/server/hub.ts
|
|
7
7
|
const REPLAY_PAGE = 500;
|
|
@@ -50,7 +50,7 @@ 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
|
|
53
|
+
/** Subscribes whose rules are being awaited: a revoke issued meanwhile takes them out, so the grant cannot outlive it. */
|
|
54
54
|
const judging = /* @__PURE__ */ new Set();
|
|
55
55
|
/** The tail of each channel's presence joins that are queued or running; an entry leaves when its chain is idle. */
|
|
56
56
|
const joinChains = /* @__PURE__ */ new Map();
|
|
@@ -94,7 +94,7 @@ function createHubCore(options = {}) {
|
|
|
94
94
|
where: `rule.${op}`,
|
|
95
95
|
channel
|
|
96
96
|
});
|
|
97
|
-
throw new RealtimeError("unavailable", "the channel rule could not be evaluated");
|
|
97
|
+
throw new RealtimeError("unavailable", ...["the channel rule could not be evaluated"]);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
function send(conn, text) {
|
|
@@ -156,11 +156,7 @@ function createHubCore(options = {}) {
|
|
|
156
156
|
chan = {
|
|
157
157
|
name,
|
|
158
158
|
subs: /* @__PURE__ */ new Set(),
|
|
159
|
-
bucket: new TokenBucket(limits.channelPublishesPerSecond, limits.channelPublishesPerSecond, now())
|
|
160
|
-
adapterSub: void 0,
|
|
161
|
-
presenceSub: void 0,
|
|
162
|
-
closing: false,
|
|
163
|
-
frame: void 0
|
|
159
|
+
bucket: new TokenBucket(limits.channelPublishesPerSecond, limits.channelPublishesPerSecond, now())
|
|
164
160
|
};
|
|
165
161
|
chans.set(name, chan);
|
|
166
162
|
}
|
|
@@ -185,32 +181,31 @@ function createHubCore(options = {}) {
|
|
|
185
181
|
else sub.buffer.push(message);
|
|
186
182
|
continue;
|
|
187
183
|
}
|
|
188
|
-
if (message.seq === sub.
|
|
189
|
-
else if (message.seq > sub.
|
|
184
|
+
if (message.seq === sub.seq + 1) deliver(sub, message, false);
|
|
185
|
+
else if (message.seq > sub.seq + 1) {
|
|
190
186
|
sub.buffer.push(message);
|
|
191
187
|
pump(sub, void 0);
|
|
192
|
-
} else if (message.at > sub.
|
|
188
|
+
} else if (message.at > sub.at) removeSub(sub, "reset");
|
|
193
189
|
}
|
|
194
190
|
}
|
|
195
191
|
function deliver(sub, message, replayed) {
|
|
196
|
-
sub.
|
|
197
|
-
sub.
|
|
192
|
+
sub.seq = message.seq;
|
|
193
|
+
sub.at = Math.max(sub.at, message.at);
|
|
198
194
|
if (!sub.echo && message.publisher === sub.conn.id) return;
|
|
199
195
|
counters.delivered += 1;
|
|
200
196
|
if (replayed) {
|
|
201
197
|
send(sub.conn, messageFrame(sub.channel.name, message, true));
|
|
202
198
|
return;
|
|
203
199
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
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 = {
|
|
207
202
|
seq: message.seq,
|
|
208
|
-
text
|
|
203
|
+
text: messageFrame(sub.channel.name, message, false)
|
|
209
204
|
};
|
|
210
|
-
send(sub.conn, text);
|
|
205
|
+
send(sub.conn, cached.text);
|
|
211
206
|
}
|
|
212
207
|
/**
|
|
213
|
-
* 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
|
|
214
209
|
* missing ones from history. When history no longer has them: a subscription that reported a gap skips ahead; any other one
|
|
215
210
|
* is removed with `unsubbed why:"gap"`.
|
|
216
211
|
*/
|
|
@@ -219,24 +214,24 @@ function createHubCore(options = {}) {
|
|
|
219
214
|
sub.pumping = true;
|
|
220
215
|
try {
|
|
221
216
|
while (sub.active) {
|
|
222
|
-
sub.buffer = sub.buffer.filter((message) => message.seq > sub.
|
|
217
|
+
sub.buffer = sub.buffer.filter((message) => message.seq > sub.seq);
|
|
223
218
|
const next = sub.buffer[0];
|
|
224
|
-
if (next !== void 0 && next.seq === sub.
|
|
219
|
+
if (next !== void 0 && next.seq === sub.seq + 1) {
|
|
225
220
|
deliver(sub, next, false);
|
|
226
221
|
sub.buffer.shift();
|
|
227
222
|
continue;
|
|
228
223
|
}
|
|
229
224
|
const target = next === void 0 ? upTo ?? 0 : next.seq - 1;
|
|
230
|
-
if (sub.
|
|
231
|
-
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));
|
|
232
227
|
if (!sub.active) return;
|
|
233
228
|
const first = page.messages[0];
|
|
234
|
-
if (first === void 0 || first.seq !== sub.
|
|
229
|
+
if (first === void 0 || first.seq !== sub.seq + 1) {
|
|
235
230
|
if (!sub.gapOk) {
|
|
236
231
|
removeSub(sub, "gap");
|
|
237
232
|
return;
|
|
238
233
|
}
|
|
239
|
-
sub.
|
|
234
|
+
sub.seq = (first?.seq ?? target + 1) - 1;
|
|
240
235
|
continue;
|
|
241
236
|
}
|
|
242
237
|
for (const message of page.messages) {
|
|
@@ -263,7 +258,7 @@ function createHubCore(options = {}) {
|
|
|
263
258
|
sub.state = event.member.s;
|
|
264
259
|
}
|
|
265
260
|
if (!sub.active || !sub.joined && !sub.watch) continue;
|
|
266
|
-
if (sub.
|
|
261
|
+
if (sub.queued !== void 0) sub.queued.push(event);
|
|
267
262
|
else sendPresence(sub, event);
|
|
268
263
|
}
|
|
269
264
|
}
|
|
@@ -314,7 +309,7 @@ function createHubCore(options = {}) {
|
|
|
314
309
|
if (chan.subs.size > 0 || chan.closing) return;
|
|
315
310
|
chan.closing = true;
|
|
316
311
|
if (chans.get(chan.name) === chan) chans.delete(chan.name);
|
|
317
|
-
const stops = [chan.
|
|
312
|
+
const stops = [chan.feed, chan.pfeed].filter((item) => item !== void 0);
|
|
318
313
|
await Promise.all(stops.map(async (stop) => {
|
|
319
314
|
try {
|
|
320
315
|
await (await stop)();
|
|
@@ -331,51 +326,46 @@ function createHubCore(options = {}) {
|
|
|
331
326
|
}
|
|
332
327
|
async function subscribe(conn, frame) {
|
|
333
328
|
const { c: name, n } = frame;
|
|
334
|
-
if (conn.subs.has(name)) return fail(conn, n, "conflict", name, "already subscribed");
|
|
335
|
-
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"]);
|
|
336
331
|
const found = match(name);
|
|
337
332
|
if (found === void 0) return fail(conn, n, identityDenied(conn), name);
|
|
338
333
|
const wantsPresence = frame.hasPresence || frame.watch === true;
|
|
339
334
|
const pending = {
|
|
340
335
|
conn,
|
|
341
|
-
channel: name
|
|
342
|
-
revoked: false
|
|
336
|
+
channel: name
|
|
343
337
|
};
|
|
344
338
|
judging.add(pending);
|
|
345
339
|
try {
|
|
346
|
-
if (!await allowed(found.rule, "subscribe", name, found.params, conn.identity) || wantsPresence && !await allowed(found.rule, "presence", name, found.params, conn.identity) || pending
|
|
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);
|
|
347
341
|
} finally {
|
|
348
342
|
judging.delete(pending);
|
|
349
343
|
}
|
|
350
|
-
if (frame.hasPresence && byteLength(JSON.stringify(frame.presence ?? null)) > limits.maxStateBytes) return fail(conn, n, "too_large", name, "the presence state is too large");
|
|
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"]);
|
|
351
345
|
if (conn.closed) return;
|
|
352
346
|
const chan = channelState(name);
|
|
353
|
-
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"]);
|
|
354
348
|
const sub = {
|
|
355
349
|
conn,
|
|
356
350
|
channel: chan,
|
|
357
|
-
|
|
358
|
-
params: found.params,
|
|
351
|
+
...found,
|
|
359
352
|
echo: frame.echo !== false,
|
|
360
353
|
watch: frame.watch === true,
|
|
361
|
-
joined: false,
|
|
362
354
|
version: 0,
|
|
363
355
|
state: null,
|
|
364
|
-
|
|
365
|
-
|
|
356
|
+
seq: 0,
|
|
357
|
+
at: now(),
|
|
366
358
|
pumping: true,
|
|
367
|
-
gapOk: false,
|
|
368
359
|
buffer: [],
|
|
369
|
-
|
|
370
|
-
joining: void 0,
|
|
360
|
+
queued: wantsPresence ? [] : void 0,
|
|
371
361
|
active: true
|
|
372
362
|
};
|
|
373
363
|
conn.subs.set(name, sub);
|
|
374
364
|
chan.subs.add(sub);
|
|
375
365
|
try {
|
|
376
|
-
await ensureFeed(chan, "
|
|
366
|
+
await ensureFeed(chan, "feed", () => adapter.subscribe(chan.name, (message) => onMessage(chan, message)));
|
|
377
367
|
if (wantsPresence) {
|
|
378
|
-
await ensureFeed(chan, "
|
|
368
|
+
await ensureFeed(chan, "pfeed", () => adapter.presenceSubscribe(chan.name, (event) => onPresence(chan, event)));
|
|
379
369
|
startHeartbeat();
|
|
380
370
|
}
|
|
381
371
|
const probe = await adapter.history(name, Number.MAX_SAFE_INTEGER, 0);
|
|
@@ -384,12 +374,12 @@ function createHubCore(options = {}) {
|
|
|
384
374
|
const after = frame.after;
|
|
385
375
|
let gap = false;
|
|
386
376
|
let reset = false;
|
|
387
|
-
if (after === void 0) sub.
|
|
377
|
+
if (after === void 0) sub.seq = head;
|
|
388
378
|
else if (after > head) {
|
|
389
379
|
reset = true;
|
|
390
|
-
sub.
|
|
380
|
+
sub.seq = head;
|
|
391
381
|
} else {
|
|
392
|
-
sub.
|
|
382
|
+
sub.seq = after;
|
|
393
383
|
gap = probe.oldest === 0 ? head > after : probe.oldest > after + 1;
|
|
394
384
|
sub.gapOk = gap;
|
|
395
385
|
}
|
|
@@ -424,7 +414,15 @@ function createHubCore(options = {}) {
|
|
|
424
414
|
/** Joins one at a time per channel: the capacity check reads the set, so two joins in flight would both see room. */
|
|
425
415
|
function joinPresence(sub, state) {
|
|
426
416
|
const name = sub.channel.name;
|
|
427
|
-
const run = (joinChains.get(name) ?? Promise.resolve()).then(() =>
|
|
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
|
+
});
|
|
428
426
|
const settle = () => {
|
|
429
427
|
if (joinChains.get(name) === tail) joinChains.delete(name);
|
|
430
428
|
};
|
|
@@ -432,15 +430,6 @@ function createHubCore(options = {}) {
|
|
|
432
430
|
joinChains.set(name, tail);
|
|
433
431
|
return run;
|
|
434
432
|
}
|
|
435
|
-
async function joinOne(sub, state) {
|
|
436
|
-
if (!sub.active) return;
|
|
437
|
-
if ((await adapter.presenceList(sub.channel.name)).length >= limits.maxPresenceMembers) throw new RealtimeError("limit", "the presence set is full");
|
|
438
|
-
sub.version = 1;
|
|
439
|
-
sub.state = state;
|
|
440
|
-
sub.joined = true;
|
|
441
|
-
startHeartbeat();
|
|
442
|
-
await adapter.presenceSet(sub.channel.name, memberOf(sub), limits.presenceTtlMs);
|
|
443
|
-
}
|
|
444
433
|
/** The presence member a joined subscription stands for. */
|
|
445
434
|
function memberOf(sub) {
|
|
446
435
|
const { user } = sub.conn.identity;
|
|
@@ -460,8 +449,8 @@ function createHubCore(options = {}) {
|
|
|
460
449
|
op: "sync",
|
|
461
450
|
m: members
|
|
462
451
|
});
|
|
463
|
-
const queued = sub.
|
|
464
|
-
sub.
|
|
452
|
+
const queued = sub.queued ?? [];
|
|
453
|
+
sub.queued = void 0;
|
|
465
454
|
const known = new Map(members.map((member) => [member.id, member.v]));
|
|
466
455
|
for (const event of queued) {
|
|
467
456
|
const version = known.get(event.member.id);
|
|
@@ -472,7 +461,7 @@ function createHubCore(options = {}) {
|
|
|
472
461
|
async function setPresence(conn, frame) {
|
|
473
462
|
const sub = conn.subs.get(frame.c);
|
|
474
463
|
if (sub === void 0 || !sub.joined) return fail(conn, void 0, "not_subscribed", frame.c);
|
|
475
|
-
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"]);
|
|
476
465
|
if (!conn.publishBucket.take(now())) return limit(conn, void 0, frame.c);
|
|
477
466
|
sub.version += 1;
|
|
478
467
|
sub.state = frame.s;
|
|
@@ -495,7 +484,7 @@ function createHubCore(options = {}) {
|
|
|
495
484
|
const { c: name, n } = frame;
|
|
496
485
|
const found = match(name);
|
|
497
486
|
if (found === void 0 || !await allowed(found.rule, "publish", name, found.params, conn.identity)) return fail(conn, n, identityDenied(conn), name);
|
|
498
|
-
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"]);
|
|
499
488
|
if (!conn.publishBucket.take(now()) || chans.get(name)?.bucket.take(now()) === false) return limit(conn, n, name);
|
|
500
489
|
try {
|
|
501
490
|
const result = await adapter.publish(name, {
|
|
@@ -536,7 +525,7 @@ function createHubCore(options = {}) {
|
|
|
536
525
|
const sub = target;
|
|
537
526
|
const handler = sub.rule.handlers?.[frame.e];
|
|
538
527
|
if (handler === void 0) return fail(conn, n, "no_handler", name);
|
|
539
|
-
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"]);
|
|
540
529
|
if (!conn.publishBucket.take(now())) return limit(conn, n, name);
|
|
541
530
|
const controller = new AbortController();
|
|
542
531
|
conn.pending.add(controller);
|
|
@@ -557,7 +546,7 @@ function createHubCore(options = {}) {
|
|
|
557
546
|
});
|
|
558
547
|
const reply = await Promise.race([run, aborted]) ?? null;
|
|
559
548
|
const text = JSON.stringify(reply);
|
|
560
|
-
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"]);
|
|
561
550
|
send(conn, `{"t":"rep","n":${n},"d":${text}}`);
|
|
562
551
|
} catch (error) {
|
|
563
552
|
if (error instanceof RealtimeError) fail(conn, n, error.code, name, error.code === "timeout" || error.code === "closed" ? void 0 : error.message);
|
|
@@ -589,7 +578,7 @@ function createHubCore(options = {}) {
|
|
|
589
578
|
}
|
|
590
579
|
const frame = decoded.frame;
|
|
591
580
|
const n = "n" in frame ? frame.n : void 0;
|
|
592
|
-
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);
|
|
593
582
|
try {
|
|
594
583
|
switch (frame.t) {
|
|
595
584
|
case "hello": return;
|
|
@@ -679,38 +668,35 @@ function createHubCore(options = {}) {
|
|
|
679
668
|
if (beating) return;
|
|
680
669
|
beating = true;
|
|
681
670
|
try {
|
|
682
|
-
|
|
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
|
+
}
|
|
683
696
|
} finally {
|
|
684
697
|
beating = false;
|
|
685
698
|
}
|
|
686
699
|
}
|
|
687
|
-
async function beatOnce() {
|
|
688
|
-
const refs = [];
|
|
689
|
-
const watched = /* @__PURE__ */ new Set();
|
|
690
|
-
for (const conn of conns.values()) for (const sub of conn.subs.values()) {
|
|
691
|
-
if (sub.joined) refs.push({
|
|
692
|
-
channel: sub.channel.name,
|
|
693
|
-
id: conn.id
|
|
694
|
-
});
|
|
695
|
-
if (sub.joined || sub.watch) watched.add(sub.channel.name);
|
|
696
|
-
}
|
|
697
|
-
if (refs.length === 0 && watched.size === 0) {
|
|
698
|
-
clearInterval(heartbeat);
|
|
699
|
-
heartbeat = void 0;
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
try {
|
|
703
|
-
const missing = await adapter.presenceTouch(refs, limits.presenceTtlMs);
|
|
704
|
-
for (const ref of missing) {
|
|
705
|
-
const sub = conns.get(ref.id)?.subs.get(ref.channel);
|
|
706
|
-
if (sub === void 0 || !sub.joined) continue;
|
|
707
|
-
await adapter.presenceSet(ref.channel, memberOf(sub), limits.presenceTtlMs);
|
|
708
|
-
}
|
|
709
|
-
for (const channel of watched) await adapter.presenceReap(channel);
|
|
710
|
-
} catch (error) {
|
|
711
|
-
report(error, { where: "presenceHeartbeat" });
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
700
|
for (const [pattern, rule] of Object.entries(options.channels ?? {})) rules.push(compileRule(pattern, rule));
|
|
715
701
|
const core = {
|
|
716
702
|
limits,
|
|
@@ -723,8 +709,8 @@ function createHubCore(options = {}) {
|
|
|
723
709
|
return core;
|
|
724
710
|
},
|
|
725
711
|
connect(transport, identity = {}, connectOptions = {}) {
|
|
726
|
-
if (closing) throw new RealtimeError("closed", "the hub is closed");
|
|
727
|
-
let release
|
|
712
|
+
if (closing) throw new RealtimeError("closed", ...["the hub is closed"]);
|
|
713
|
+
let release;
|
|
728
714
|
const done = new Promise((resolve) => {
|
|
729
715
|
release = resolve;
|
|
730
716
|
});
|
|
@@ -737,14 +723,12 @@ function createHubCore(options = {}) {
|
|
|
737
723
|
subs: /* @__PURE__ */ new Map(),
|
|
738
724
|
queue: [],
|
|
739
725
|
publishBucket: connectOptions.publishBucket ?? new TokenBucket(limits.publishesPerSecond, limits.publishBurst, now()),
|
|
740
|
-
|
|
726
|
+
ctl: new TokenBucket(limits.publishesPerSecond, Math.max(limits.publishBurst, limits.maxChannelsPerConnection), now()),
|
|
741
727
|
stateless: connectOptions.stateless === true,
|
|
742
728
|
waiters: [],
|
|
743
729
|
invalid: new WindowCounter(10, 6e4),
|
|
744
730
|
limited: new WindowCounter(50, 1e4),
|
|
745
731
|
pending: /* @__PURE__ */ new Set(),
|
|
746
|
-
running: false,
|
|
747
|
-
closed: false,
|
|
748
732
|
release,
|
|
749
733
|
done
|
|
750
734
|
};
|
|
@@ -785,9 +769,9 @@ function createHubCore(options = {}) {
|
|
|
785
769
|
};
|
|
786
770
|
},
|
|
787
771
|
async publish(channel, event, data, publishOptions = {}) {
|
|
788
|
-
if (!isChannelName(channel)) throw new RealtimeError("bad_channel", `"${channel}" is not a valid channel name`);
|
|
789
|
-
if (!isEventName(event)) throw new RealtimeError("bad_frame", `"${event}" is not a valid event name`);
|
|
790
|
-
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"]);
|
|
791
775
|
let text;
|
|
792
776
|
try {
|
|
793
777
|
text = JSON.stringify(data === void 0 ? null : data);
|
|
@@ -795,7 +779,7 @@ function createHubCore(options = {}) {
|
|
|
795
779
|
throw new TypeError("the data is not JSON", { cause: error });
|
|
796
780
|
}
|
|
797
781
|
if (text === void 0) throw new TypeError("the data is not JSON");
|
|
798
|
-
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"]);
|
|
799
783
|
const found = match(channel);
|
|
800
784
|
const result = await adapter.publish(channel, {
|
|
801
785
|
event,
|
|
@@ -810,12 +794,12 @@ function createHubCore(options = {}) {
|
|
|
810
794
|
return result;
|
|
811
795
|
},
|
|
812
796
|
async setMemberState(identity, channel, connectionId, state) {
|
|
813
|
-
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"]);
|
|
814
798
|
const found = match(channel);
|
|
815
799
|
if (found === void 0 || !await allowed(found.rule, "presence", channel, found.params, identity)) throw new RealtimeError("forbidden");
|
|
816
|
-
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"]);
|
|
817
801
|
const member = (await adapter.presenceList(channel)).find((item) => item.id === connectionId);
|
|
818
|
-
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"]);
|
|
819
803
|
const local = conns.get(connectionId)?.subs.get(channel);
|
|
820
804
|
const version = Math.max(member.v, local?.version ?? 0) + 1;
|
|
821
805
|
if (local?.joined === true) {
|
|
@@ -833,9 +817,8 @@ function createHubCore(options = {}) {
|
|
|
833
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.");
|
|
834
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);
|
|
835
819
|
let count = 0;
|
|
836
|
-
for (const item of judging) {
|
|
837
|
-
|
|
838
|
-
item.revoked = true;
|
|
820
|
+
for (const item of judging) if (hit(item.conn, item.channel)) {
|
|
821
|
+
judging.delete(item);
|
|
839
822
|
count += 1;
|
|
840
823
|
}
|
|
841
824
|
for (const conn of [...conns.values()]) for (const sub of [...conn.subs.values()]) {
|