@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.
@@ -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 marks them, so the grant cannot outlive it. */
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.lastSeq + 1) deliver(sub, message, false);
189
- else if (message.seq > sub.lastSeq + 1) {
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.lastAt) removeSub(sub, "reset");
188
+ } else if (message.at > sub.at) removeSub(sub, "reset");
193
189
  }
194
190
  }
195
191
  function deliver(sub, message, replayed) {
196
- sub.lastSeq = message.seq;
197
- sub.lastAt = Math.max(sub.lastAt, message.at);
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
- const cached = sub.channel.frame;
205
- const text = cached !== void 0 && cached.seq === message.seq ? cached.text : messageFrame(sub.channel.name, message, false);
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.lastSeq` up to `upTo` and whatever is buffered. Fetches the
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.lastSeq);
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.lastSeq + 1) {
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.lastSeq >= target) break;
231
- const page = await adapter.history(sub.channel.name, sub.lastSeq, Math.min(REPLAY_PAGE, target - sub.lastSeq));
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.lastSeq + 1) {
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.lastSeq = (first?.seq ?? target + 1) - 1;
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.presenceQueue !== void 0) sub.presenceQueue.push(event);
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.adapterSub, chan.presenceSub].filter((item) => item !== void 0);
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.revoked) return fail(conn, n, identityDenied(conn), name);
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
- rule: found.rule,
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
- lastSeq: 0,
365
- lastAt: now(),
356
+ seq: 0,
357
+ at: now(),
366
358
  pumping: true,
367
- gapOk: false,
368
359
  buffer: [],
369
- presenceQueue: wantsPresence ? [] : void 0,
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, "adapterSub", () => adapter.subscribe(chan.name, (message) => onMessage(chan, message)));
366
+ await ensureFeed(chan, "feed", () => adapter.subscribe(chan.name, (message) => onMessage(chan, message)));
377
367
  if (wantsPresence) {
378
- await ensureFeed(chan, "presenceSub", () => adapter.presenceSubscribe(chan.name, (event) => onPresence(chan, event)));
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.lastSeq = head;
377
+ if (after === void 0) sub.seq = head;
388
378
  else if (after > head) {
389
379
  reset = true;
390
- sub.lastSeq = head;
380
+ sub.seq = head;
391
381
  } else {
392
- sub.lastSeq = after;
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(() => joinOne(sub, state));
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.presenceQueue ?? [];
464
- sub.presenceQueue = void 0;
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.controlBucket.take(now()) && frame.t === "sub") return limit(conn, n, frame.c);
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
- await beatOnce();
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 = () => void 0;
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
- controlBucket: new TokenBucket(limits.publishesPerSecond, Math.max(limits.publishBurst, limits.maxChannelsPerConnection), now()),
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
- if (item.revoked || !hit(item.conn, item.channel)) continue;
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()]) {