@fitzzero/quickdraw-core 5.1.2 → 5.1.3

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.
Files changed (124) hide show
  1. package/README.md +793 -57
  2. package/dist/{admin-DWWrT1h6.d.ts → admin-CS7JB_UH.d.ts} +2 -2
  3. package/dist/{adminSchemas-D4bFKsrb.d.ts → adminSchemas-29cGgrg5.d.ts} +1 -1
  4. package/dist/{auth-DXiSJZaf.d.ts → auth-DtdYjrZ9.d.ts} +11 -7
  5. package/dist/{caller-BT-Mv4bD.d.ts → caller-BTwGl7vm.d.ts} +183 -15
  6. package/dist/chunk-2FDSKLBO.js +82 -0
  7. package/dist/chunk-2FDSKLBO.js.map +1 -0
  8. package/dist/{chunk-GSIPUNJL.js → chunk-64I6MYBC.js} +139 -792
  9. package/dist/chunk-64I6MYBC.js.map +1 -0
  10. package/dist/chunk-7473HK24.js +17 -0
  11. package/dist/chunk-7473HK24.js.map +1 -0
  12. package/dist/chunk-7Y5ROPXL.js +33 -0
  13. package/dist/chunk-7Y5ROPXL.js.map +1 -0
  14. package/dist/chunk-A2RBQD66.js +7 -0
  15. package/dist/chunk-A2RBQD66.js.map +1 -0
  16. package/dist/chunk-ASAMUGHG.js +651 -0
  17. package/dist/chunk-ASAMUGHG.js.map +1 -0
  18. package/dist/{chunk-B2P7AZ25.js → chunk-CYE4UBOB.js} +6 -4
  19. package/dist/chunk-CYE4UBOB.js.map +1 -0
  20. package/dist/chunk-DNP65TD5.js +832 -0
  21. package/dist/chunk-DNP65TD5.js.map +1 -0
  22. package/dist/{chunk-5CGO426C.js → chunk-GYLWEJHW.js} +5 -1
  23. package/dist/chunk-GYLWEJHW.js.map +1 -0
  24. package/dist/{chunk-AONAFOYU.js → chunk-I3JCFS7H.js} +25 -13
  25. package/dist/chunk-I3JCFS7H.js.map +1 -0
  26. package/dist/{chunk-X5CWPX7H.js → chunk-IIQ3WMJK.js} +13 -2
  27. package/dist/chunk-IIQ3WMJK.js.map +1 -0
  28. package/dist/{chunk-6K5BY47X.js → chunk-JVTONQFI.js} +1277 -277
  29. package/dist/chunk-JVTONQFI.js.map +1 -0
  30. package/dist/{chunk-RL4VKLBX.js → chunk-KKQANRLG.js} +2 -2
  31. package/dist/{chunk-RL4VKLBX.js.map → chunk-KKQANRLG.js.map} +1 -1
  32. package/dist/{chunk-GKH3LJ7N.js → chunk-MPQAT55F.js} +38 -205
  33. package/dist/chunk-MPQAT55F.js.map +1 -0
  34. package/dist/chunk-NX2V6SIL.js +202 -0
  35. package/dist/chunk-NX2V6SIL.js.map +1 -0
  36. package/dist/{chunk-7QCUX4TK.js → chunk-NZHA4TZD.js} +72 -175
  37. package/dist/chunk-NZHA4TZD.js.map +1 -0
  38. package/dist/{chunk-MBH6IDSM.js → chunk-OCZJ4EYV.js} +40 -17
  39. package/dist/chunk-OCZJ4EYV.js.map +1 -0
  40. package/dist/{chunk-RJ5I2VO3.js → chunk-PHCURHC7.js} +6 -6
  41. package/dist/chunk-PHCURHC7.js.map +1 -0
  42. package/dist/{chunk-4YO5H5ON.js → chunk-Q4OGQM5I.js} +4 -10
  43. package/dist/chunk-Q4OGQM5I.js.map +1 -0
  44. package/dist/chunk-QSU47XEX.js +13 -0
  45. package/dist/chunk-QSU47XEX.js.map +1 -0
  46. package/dist/{chunk-LCC7HNFP.js → chunk-RZXJTDH5.js} +2 -2
  47. package/dist/{chunk-Z2LZ7IZ7.js → chunk-SN3OZVEQ.js} +5 -31
  48. package/dist/chunk-SN3OZVEQ.js.map +1 -0
  49. package/dist/{chunk-Z5ILULF2.js → chunk-SQ5VUG77.js} +4 -3
  50. package/dist/{chunk-Z5ILULF2.js.map → chunk-SQ5VUG77.js.map} +1 -1
  51. package/dist/chunk-STH5DLQP.js +1192 -0
  52. package/dist/chunk-STH5DLQP.js.map +1 -0
  53. package/dist/{chunk-NMXYENH4.js → chunk-VZFJTKIW.js} +6 -6
  54. package/dist/{chunk-753VJIUP.js → chunk-X4ZU4QH5.js} +43 -9
  55. package/dist/chunk-X4ZU4QH5.js.map +1 -0
  56. package/dist/chunk-XD7SRDUY.js +188 -0
  57. package/dist/chunk-XD7SRDUY.js.map +1 -0
  58. package/dist/chunk-Z7I6SONO.js +825 -0
  59. package/dist/chunk-Z7I6SONO.js.map +1 -0
  60. package/dist/cli/quickdraw-docs.js +131 -113
  61. package/dist/cli/quickdraw-docs.js.map +1 -1
  62. package/dist/client/core.d.ts +387 -0
  63. package/dist/client/core.js +109 -0
  64. package/dist/client/core.js.map +1 -0
  65. package/dist/client/index.d.ts +30 -193
  66. package/dist/client/index.js +56 -217
  67. package/dist/client/index.js.map +1 -1
  68. package/dist/{clientTypes-hWcPUVS9.d.ts → clientTypes-CUUNJwWs.d.ts} +3 -3
  69. package/dist/{connection-CgijdGpT.d.ts → connection-2EeQ_Et0.d.ts} +16 -9
  70. package/dist/{createServer-Cp-Wk500.d.ts → createServer-Bx5HOcYK.d.ts} +37 -9
  71. package/dist/{createTestApp-BB_PkqQ7.d.ts → createTestApp-DAMANT79.d.ts} +36 -9
  72. package/dist/{envelope-DzWyIXjj.d.ts → envelope-BMBaxppV.d.ts} +200 -8
  73. package/dist/{http-DotQ5bj8.d.ts → http-YzLAar3w.d.ts} +15 -50
  74. package/dist/index.d.ts +7 -6
  75. package/dist/index.js +61 -28
  76. package/dist/index.js.map +1 -1
  77. package/dist/{infer-BmoZsL9a.d.ts → infer-Br62cnyW.d.ts} +39 -1
  78. package/dist/{metrics-DW80Exve.d.ts → metrics-CTVpw0gu.d.ts} +2 -2
  79. package/dist/parser.js +2 -1
  80. package/dist/prisma/index.d.ts +13 -6
  81. package/dist/prisma/index.js +155 -9
  82. package/dist/prisma/index.js.map +1 -1
  83. package/dist/remote-N7OUAGOW.js +15 -0
  84. package/dist/remote-N7OUAGOW.js.map +1 -0
  85. package/dist/server/auth/index.d.ts +3 -3
  86. package/dist/server/auth/index.js +5 -3
  87. package/dist/server/auth/index.js.map +1 -1
  88. package/dist/server/index.d.ts +140 -36
  89. package/dist/server/index.js +162 -55
  90. package/dist/server/index.js.map +1 -1
  91. package/dist/server/mcp/index.d.ts +34 -18
  92. package/dist/server/mcp/index.js +29 -12
  93. package/dist/server/mcp/index.js.map +1 -1
  94. package/dist/server/otel.d.ts +3 -3
  95. package/dist/{sharing-UDLn1049.d.ts → sharing-BrsmlDv_.d.ts} +3 -3
  96. package/dist/testing/client.d.ts +13 -13
  97. package/dist/testing/client.js +17 -10
  98. package/dist/testing/client.js.map +1 -1
  99. package/dist/testing/index.d.ts +49 -12
  100. package/dist/testing/index.js +102 -19
  101. package/dist/testing/index.js.map +1 -1
  102. package/dist/testing/mock.d.ts +5 -5
  103. package/dist/testing/mock.js +15 -9
  104. package/dist/tracked-BR5b2O-U.d.ts +43 -0
  105. package/dist/{types-CYliTP65.d.ts → types-CLRCQGnO.d.ts} +4 -3
  106. package/dist/types-tMagHbjy.d.ts +151 -0
  107. package/dist/utils/index.d.ts +16 -4
  108. package/dist/utils/index.js +6 -3
  109. package/package.json +5 -1
  110. package/dist/chunk-4YO5H5ON.js.map +0 -1
  111. package/dist/chunk-5CGO426C.js.map +0 -1
  112. package/dist/chunk-6K5BY47X.js.map +0 -1
  113. package/dist/chunk-753VJIUP.js.map +0 -1
  114. package/dist/chunk-7QCUX4TK.js.map +0 -1
  115. package/dist/chunk-AONAFOYU.js.map +0 -1
  116. package/dist/chunk-B2P7AZ25.js.map +0 -1
  117. package/dist/chunk-GKH3LJ7N.js.map +0 -1
  118. package/dist/chunk-GSIPUNJL.js.map +0 -1
  119. package/dist/chunk-MBH6IDSM.js.map +0 -1
  120. package/dist/chunk-RJ5I2VO3.js.map +0 -1
  121. package/dist/chunk-X5CWPX7H.js.map +0 -1
  122. package/dist/chunk-Z2LZ7IZ7.js.map +0 -1
  123. /package/dist/{chunk-LCC7HNFP.js.map → chunk-RZXJTDH5.js.map} +0 -0
  124. /package/dist/{chunk-NMXYENH4.js.map → chunk-VZFJTKIW.js.map} +0 -0
@@ -12,11 +12,11 @@ import {
12
12
  quietly,
13
13
  strictWarningsOf,
14
14
  versionTime
15
- } from "./chunk-Z5ILULF2.js";
15
+ } from "./chunk-SQ5VUG77.js";
16
16
  import {
17
17
  isCallEnvelope,
18
18
  isCancel
19
- } from "./chunk-RL4VKLBX.js";
19
+ } from "./chunk-KKQANRLG.js";
20
20
  import {
21
21
  NEVER_ABORTED,
22
22
  SUBSCRIBER_LEVELS,
@@ -52,12 +52,12 @@ import {
52
52
  watchCallerReads,
53
53
  wholeView,
54
54
  withSignal
55
- } from "./chunk-MBH6IDSM.js";
55
+ } from "./chunk-OCZJ4EYV.js";
56
56
  import {
57
57
  createRecorder,
58
58
  describeError,
59
59
  failureLevel
60
- } from "./chunk-LCC7HNFP.js";
60
+ } from "./chunk-RZXJTDH5.js";
61
61
  import {
62
62
  INTERNAL_FAILURE,
63
63
  acknowledge,
@@ -76,25 +76,57 @@ import {
76
76
  toCallReply,
77
77
  transportCookieNaming,
78
78
  unreadable
79
- } from "./chunk-RJ5I2VO3.js";
79
+ } from "./chunk-PHCURHC7.js";
80
80
  import {
81
81
  ANY_FIELD,
82
82
  modelKey,
83
83
  storageOf
84
84
  } from "./chunk-DUBU3BB3.js";
85
+ import {
86
+ DEFAULT_COLLECTION_LIMIT,
87
+ DEFAULT_COLLECTION_MAX_LIMIT
88
+ } from "./chunk-4D2DOGAW.js";
89
+ import {
90
+ isAccessLevel
91
+ } from "./chunk-AK23XSWD.js";
92
+ import {
93
+ consoleLogger,
94
+ errorText
95
+ } from "./chunk-DKE6QO26.js";
96
+ import {
97
+ validateNow
98
+ } from "./chunk-7473HK24.js";
99
+ import {
100
+ isPeerLabel,
101
+ isRoomName,
102
+ isSocketId,
103
+ readSignal
104
+ } from "./chunk-2FDSKLBO.js";
105
+ import {
106
+ QUICKDRAW_VERSION
107
+ } from "./chunk-A2RBQD66.js";
85
108
  import {
86
109
  CLIENT_EVENTS,
87
110
  MAX_ENTITY_IDS_PER_SOCKET,
111
+ MAX_PEER_LINKS,
112
+ MAX_PEER_MESSAGE_BYTES,
88
113
  MAX_SCOPES_PER_SOCKET,
89
114
  MAX_SCOPE_LENGTH,
90
115
  MAX_SUBSCRIBE_IDS,
91
116
  MAX_TOPICS_PER_SOCKET,
117
+ PEER_RELAY_BURST,
118
+ PEER_RELAY_BYTES_BURST,
119
+ PEER_RELAY_BYTES_PER_SECOND,
120
+ PEER_RELAY_FAST_BACKLOG,
121
+ PEER_RELAY_RATE,
122
+ PEER_SIGNAL_BURST,
123
+ PEER_SIGNAL_RATE,
92
124
  PROTOCOL_MISMATCH,
93
125
  PROTOCOL_VERSION,
94
- QUICKDRAW_VERSION,
95
126
  RESERVED_ROOM_PREFIXES,
96
127
  SERVER_EVENTS,
97
128
  SERVICE_TOPIC,
129
+ STREAM_ROOM_PREFIX,
98
130
  collectionRoom,
99
131
  collectionTopic,
100
132
  entityRoom,
@@ -104,24 +136,19 @@ import {
104
136
  streamRoom,
105
137
  topicRoom,
106
138
  userRoom
107
- } from "./chunk-753VJIUP.js";
139
+ } from "./chunk-X4ZU4QH5.js";
108
140
  import {
109
- DEFAULT_COLLECTION_LIMIT,
110
- DEFAULT_COLLECTION_MAX_LIMIT
111
- } from "./chunk-4D2DOGAW.js";
112
- import {
113
- isAccessLevel
114
- } from "./chunk-AK23XSWD.js";
115
- import {
116
- consoleLogger,
117
- errorText
118
- } from "./chunk-DKE6QO26.js";
141
+ utf8ByteLength
142
+ } from "./chunk-7Y5ROPXL.js";
119
143
  import {
120
144
  INTERNAL_MESSAGE,
121
145
  QuickdrawError,
122
146
  httpStatus,
123
147
  toWire
124
- } from "./chunk-4YO5H5ON.js";
148
+ } from "./chunk-Q4OGQM5I.js";
149
+ import {
150
+ isRecord
151
+ } from "./chunk-QSU47XEX.js";
125
152
 
126
153
  // src/server/access/forms.ts
127
154
  function custom(check) {
@@ -130,7 +157,7 @@ function custom(check) {
130
157
  }
131
158
  return Object.freeze({ kind: "custom", check });
132
159
  }
133
- function isRecord(value) {
160
+ function isRecord2(value) {
134
161
  return typeof value === "object" && value !== null && !Array.isArray(value);
135
162
  }
136
163
  function isCustomAccess(form) {
@@ -150,7 +177,7 @@ function objectFormProblem(form) {
150
177
  return "needs service, entry or scope set to an access level";
151
178
  }
152
179
  if (form.scope !== void 0) {
153
- const valid = levels.length === 1 && isRecord(form.of) && isIdSelector(form.id);
180
+ const valid = levels.length === 1 && isRecord2(form.of) && isIdSelector(form.id);
154
181
  return valid ? void 0 : "a scope form is { scope, of: contract, id } and nothing else";
155
182
  }
156
183
  if (form.of !== void 0 || form.id !== void 0 && !isIdSelector(form.id)) {
@@ -162,7 +189,7 @@ function accessFormProblem(form) {
162
189
  if (form === "public" || form === "authenticated") {
163
190
  return void 0;
164
191
  }
165
- if (!isRecord(form)) {
192
+ if (!isRecord2(form)) {
166
193
  return 'must be "public", "authenticated", { service }, { entry }, { scope, of, id } or custom(fn)';
167
194
  }
168
195
  if (form.kind === "custom") {
@@ -182,7 +209,7 @@ function accessIds(form, input) {
182
209
  let value;
183
210
  if (typeof id === "function") {
184
211
  value = id(input);
185
- } else if (isRecord(input)) {
212
+ } else if (isRecord2(input)) {
186
213
  value = input[id ?? "id"];
187
214
  }
188
215
  const ids = idsFrom(value);
@@ -366,24 +393,41 @@ function linkQualifies(via, values) {
366
393
  }
367
394
  return unknown ? void 0 : false;
368
395
  }
369
- var OPTION_KEYS = /* @__PURE__ */ new Set(["anchor", "scopeAccess", "bulkThreshold"]);
370
- function isRecord2(value) {
396
+ var OPTION_KEYS = /* @__PURE__ */ new Set([
397
+ "anchor",
398
+ "scopeAccess",
399
+ "requires",
400
+ "bulkThreshold"
401
+ ]);
402
+ function isRecord3(value) {
371
403
  return typeof value === "object" && value !== null && !Array.isArray(value);
372
404
  }
373
405
  function isContract(value) {
374
- return isRecord2(value) && typeof value.name === "string" && value.name.length > 0 && Object.isFrozen(value);
406
+ return isRecord3(value) && typeof value.name === "string" && value.name.length > 0 && Object.isFrozen(value);
407
+ }
408
+ function checkRequires(owner, requires, fail5) {
409
+ if (requires === void 0) {
410
+ return void 0;
411
+ }
412
+ const valid = isRecord3(requires) && Object.keys(requires).length === 1 && isAccessLevel(requires.service);
413
+ if (!valid) {
414
+ fail5(
415
+ `${owner}: requires must be { service: level }, the service-wide grant every subscriber needs, as { service: "Read" }`
416
+ );
417
+ }
418
+ return Object.freeze({ service: requires.service });
375
419
  }
376
420
  function checkOption(owner, option, fail5) {
377
- if (!isRecord2(option)) {
421
+ if (!isRecord3(option)) {
378
422
  fail5(`${owner} must be { anchor: contract } or { scopeAccess: "self" }`);
379
423
  }
380
424
  const unknownKey = Object.keys(option).find((key) => !OPTION_KEYS.has(key));
381
425
  if (unknownKey !== void 0) {
382
426
  fail5(
383
- `${owner} has an unknown option "${unknownKey}"; the options are anchor, scopeAccess and bulkThreshold`
427
+ `${owner} has an unknown option "${unknownKey}"; the options are anchor, scopeAccess, requires and bulkThreshold`
384
428
  );
385
429
  }
386
- const { anchor, scopeAccess, bulkThreshold = DEFAULT_BULK_THRESHOLD } = option;
430
+ const { anchor, scopeAccess, requires, bulkThreshold = DEFAULT_BULK_THRESHOLD } = option;
387
431
  if (anchor === void 0 === (scopeAccess === void 0)) {
388
432
  fail5(
389
433
  `${owner} needs exactly one of anchor (the contract whose rows its scope values are ids of) and scopeAccess: "self"`
@@ -400,7 +444,11 @@ function checkOption(owner, option, fail5) {
400
444
  if (typeof bulkThreshold !== "number" || !Number.isSafeInteger(bulkThreshold) || bulkThreshold < 1) {
401
445
  fail5(`${owner}: bulkThreshold must be a positive whole number`);
402
446
  }
403
- return { anchor, bulkThreshold };
447
+ return {
448
+ anchor,
449
+ requires: checkRequires(owner, requires, fail5),
450
+ bulkThreshold
451
+ };
404
452
  }
405
453
  function checkIndex(name, def, item, access, fail5) {
406
454
  const { index, views } = def;
@@ -514,7 +562,7 @@ function compileOne([name, def], contract, projections, [option, policy], fail5)
514
562
  `collection "${name}" needs its scope's access: collections: { ${name}: { anchor: contract } }, or { scopeAccess: "self" } for a scope that is the subscriber's user id`
515
563
  );
516
564
  }
517
- const { anchor, bulkThreshold } = checkOption(`collections.${name}`, option, fail5);
565
+ const { anchor, requires, bulkThreshold } = checkOption(`collections.${name}`, option, fail5);
518
566
  const item = projections.get(def.item);
519
567
  if (item === void 0) {
520
568
  fail5(`collection "${name}": item "${def.item}" is not a projection of the contract`);
@@ -531,12 +579,13 @@ function compileOne([name, def], contract, projections, [option, policy], fail5)
531
579
  maxLimit: def.maxLimit ?? DEFAULT_COLLECTION_MAX_LIMIT,
532
580
  access,
533
581
  anchor,
582
+ requires,
534
583
  bulkThreshold,
535
584
  index: checkIndex(name, def, item, access, fail5)
536
585
  });
537
586
  }
538
587
  function compileCollections(contract, projections, model, option, fail5, policy) {
539
- if (option !== void 0 && !isRecord2(option)) {
588
+ if (option !== void 0 && !isRecord3(option)) {
540
589
  fail5(
541
590
  'collections must map each collection of the contract to { anchor } or { scopeAccess: "self" }'
542
591
  );
@@ -682,7 +731,7 @@ function createDeltaBuffer(options = {}) {
682
731
 
683
732
  // src/server/collections/items.ts
684
733
  var MAX_ITEM_IDS = 200;
685
- function isRecord3(value) {
734
+ function isRecord4(value) {
686
735
  return typeof value === "object" && value !== null && !Array.isArray(value);
687
736
  }
688
737
  function hiddenOf(collection) {
@@ -690,7 +739,7 @@ function hiddenOf(collection) {
690
739
  }
691
740
  function itemOf(collection, row) {
692
741
  const item = projectRow(collection.item, row);
693
- return isRecord3(item) ? strip(item, hiddenOf(collection)) : item;
742
+ return isRecord4(item) ? strip(item, hiddenOf(collection)) : item;
694
743
  }
695
744
  function patchOf(collection, row, fields) {
696
745
  const hidden = hiddenOf(collection);
@@ -849,7 +898,7 @@ function afterCursor(order, values, nullable) {
849
898
 
850
899
  // src/server/collections/index.ts
851
900
  var INDEX_MAX_ROWS = 5e4;
852
- function isRecord4(value) {
901
+ function isRecord5(value) {
853
902
  return typeof value === "object" && value !== null && !Array.isArray(value);
854
903
  }
855
904
  function indexColumns(collection) {
@@ -866,7 +915,7 @@ function indexSelect(collection, fields) {
866
915
  return selectWith(select, indexColumns(collection));
867
916
  }
868
917
  function indexRowFrom(collection, item, row, rev) {
869
- const values = isRecord4(item) ? item : {};
918
+ const values = isRecord5(item) ? item : {};
870
919
  const { versionColumn } = collection.service;
871
920
  const version = versionColumn === void 0 ? void 0 : versionTime(row[versionColumn]);
872
921
  const id = typeof row.id === "string" ? row.id : "";
@@ -1425,17 +1474,17 @@ var SubscriptionIndex = class {
1425
1474
  * changed row (any row of the service when `id` is absent), of the changed
1426
1475
  * user's sockets (every user's when `userId` is absent).
1427
1476
  */
1428
- matching(change) {
1429
- const key = change.id === void 0 ? void 0 : anchorKey(change.service, change.id);
1430
- const sockets = key === void 0 ? this.#byService.get(change.service) : this.#byAnchor.get(key);
1477
+ matching(change2) {
1478
+ const key = change2.id === void 0 ? void 0 : anchorKey(change2.service, change2.id);
1479
+ const sockets = key === void 0 ? this.#byService.get(change2.service) : this.#byAnchor.get(key);
1431
1480
  const found = /* @__PURE__ */ new Map();
1432
1481
  for (const socket of sockets?.keys() ?? []) {
1433
- if (change.userId !== void 0 && socket.data.principal?.userId !== change.userId) {
1482
+ if (change2.userId !== void 0 && socket.data.principal?.userId !== change2.userId) {
1434
1483
  continue;
1435
1484
  }
1436
1485
  const entries = [...this.entries(socket)].filter(
1437
1486
  ({ subscription }) => subscription.anchors.some(
1438
- (anchor) => key === void 0 ? serviceOf(anchor) === change.service : anchor === key
1487
+ (anchor) => key === void 0 ? serviceOf(anchor) === change2.service : anchor === key
1439
1488
  )
1440
1489
  );
1441
1490
  if (entries.length > 0) {
@@ -1447,17 +1496,6 @@ var SubscriptionIndex = class {
1447
1496
  };
1448
1497
 
1449
1498
  // src/server/realtime/validate.ts
1450
- function isThenable(value) {
1451
- return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
1452
- }
1453
- function validateNow(schema, value) {
1454
- const result = schema["~standard"].validate(value);
1455
- if (isThenable(result)) {
1456
- Promise.resolve(result).catch(() => void 0);
1457
- return "async";
1458
- }
1459
- return result.issues === void 0 ? { value: result.value } : { issues: result.issues };
1460
- }
1461
1499
  function checksItems(stream, outputValidation) {
1462
1500
  return stream.validate === "always" || outputValidation;
1463
1501
  }
@@ -1479,31 +1517,47 @@ function checkOutgoing(schema, value, label) {
1479
1517
  // src/server/realtime/channels.ts
1480
1518
  var CHANNEL_ABUSE_WINDOW_MS = 1e4;
1481
1519
  var CHANNEL_ABUSE_MULTIPLIER = 100;
1482
- function take(bucket, channel, now) {
1483
- const refill = (now - bucket.refilledAt) / 1e3 * channel.ratePerSecond;
1484
- bucket.tokens = Math.min(channel.burst, bucket.tokens + refill);
1485
- bucket.refilledAt = now;
1486
- if (bucket.tokens < 1) {
1520
+ function warnHostServed(deps, service, name) {
1521
+ if (typeof service !== "string" || typeof name !== "string") {
1522
+ return;
1523
+ }
1524
+ const channels = deps.hub.registry.services.get(service)?.contract.channels;
1525
+ const def = channels !== void 0 && Object.hasOwn(channels, name) ? channels[name] : void 0;
1526
+ const key = `${service}\0${name}`;
1527
+ if (def?.serve !== "host" || deps.hostWarned.has(key) || process.env.NODE_ENV === "production") {
1528
+ return;
1529
+ }
1530
+ deps.hostWarned.add(key);
1531
+ deps.hub.logger.warn(
1532
+ `Channel ${service}.${name} is served by the room's host (serve: "host"), but a client sent it to the server: the server drops it; send it over the link to the host of its room (useChannel does)`,
1533
+ { category: "quickdraw.channel" }
1534
+ );
1535
+ }
1536
+ function take(bucket2, channel, now) {
1537
+ const refill = (now - bucket2.refilledAt) / 1e3 * channel.ratePerSecond;
1538
+ bucket2.tokens = Math.min(channel.burst, bucket2.tokens + refill);
1539
+ bucket2.refilledAt = now;
1540
+ if (bucket2.tokens < 1) {
1487
1541
  return false;
1488
1542
  }
1489
- bucket.tokens -= 1;
1543
+ bucket2.tokens -= 1;
1490
1544
  return true;
1491
1545
  }
1492
- function abusive(bucket, channel, now) {
1493
- if (now - bucket.windowStart > CHANNEL_ABUSE_WINDOW_MS) {
1494
- bucket.windowStart = now;
1495
- bucket.dropped = 0;
1546
+ function abusive(bucket2, channel, now) {
1547
+ if (now - bucket2.windowStart > CHANNEL_ABUSE_WINDOW_MS) {
1548
+ bucket2.windowStart = now;
1549
+ bucket2.dropped = 0;
1496
1550
  }
1497
- bucket.dropped += 1;
1498
- return bucket.dropped > channel.ratePerSecond * CHANNEL_ABUSE_MULTIPLIER;
1551
+ bucket2.dropped += 1;
1552
+ return bucket2.dropped > channel.ratePerSecond * CHANNEL_ABUSE_MULTIPLIER;
1499
1553
  }
1500
1554
  function bucketOf(state, channel, now) {
1501
- let bucket = state.buckets.get(channel);
1502
- if (bucket === void 0) {
1503
- bucket = { tokens: channel.burst, refilledAt: now, windowStart: now, dropped: 0 };
1504
- state.buckets.set(channel, bucket);
1555
+ let bucket2 = state.buckets.get(channel);
1556
+ if (bucket2 === void 0) {
1557
+ bucket2 = { tokens: channel.burst, refilledAt: now, windowStart: now, dropped: 0 };
1558
+ state.buckets.set(channel, bucket2);
1505
1559
  }
1506
- return bucket;
1560
+ return bucket2;
1507
1561
  }
1508
1562
  function parse(deps, channel, payload) {
1509
1563
  const result = validateNow(channel.payload, payload);
@@ -1589,13 +1643,13 @@ function run(deps, channel, value, ctx) {
1589
1643
  failed(deps, channel, ctx, error);
1590
1644
  }
1591
1645
  }
1592
- function flooded(deps, state, channel, bucket) {
1646
+ function flooded(deps, state, channel, bucket2) {
1593
1647
  deps.hub.logger.warn(
1594
1648
  `Disconnecting socket ${state.socket.id} for sustained flooding of channel ${channel.service}.${channel.name}`,
1595
1649
  {
1596
1650
  category: "quickdraw.channel",
1597
1651
  userId: state.socket.data.principal?.userId,
1598
- dropped: bucket.dropped
1652
+ dropped: bucket2.dropped
1599
1653
  }
1600
1654
  );
1601
1655
  state.socket.disconnect(true);
@@ -1607,13 +1661,14 @@ function receive(deps, state, frame) {
1607
1661
  const [service, name, payload] = frame;
1608
1662
  const channel = typeof service === "string" && typeof name === "string" ? deps.hub.registry.services.get(service)?.channels.get(name) : void 0;
1609
1663
  if (channel === void 0) {
1664
+ warnHostServed(deps, service, name);
1610
1665
  return;
1611
1666
  }
1612
1667
  const now = Date.now();
1613
- const bucket = bucketOf(state, channel, now);
1614
- if (!take(bucket, channel, now)) {
1615
- if (abusive(bucket, channel, now)) {
1616
- flooded(deps, state, channel, bucket);
1668
+ const bucket2 = bucketOf(state, channel, now);
1669
+ if (!take(bucket2, channel, now)) {
1670
+ if (abusive(bucket2, channel, now)) {
1671
+ flooded(deps, state, channel, bucket2);
1617
1672
  }
1618
1673
  return;
1619
1674
  }
@@ -2026,8 +2081,8 @@ var PRESENCE_FALLBACK_MS = 3e4;
2026
2081
  var PRESENCE_WARN_EVERY_MS = 10 * 6e4;
2027
2082
  function coalescer() {
2028
2083
  const slots = /* @__PURE__ */ new Map();
2029
- const start = (key, ask) => {
2030
- const running = ask();
2084
+ const start = (key, ask2) => {
2085
+ const running = ask2();
2031
2086
  const slot = { running, queued: void 0 };
2032
2087
  slots.set(key, slot);
2033
2088
  const settle2 = () => {
@@ -2038,14 +2093,14 @@ function coalescer() {
2038
2093
  running.then(settle2, settle2);
2039
2094
  return running;
2040
2095
  };
2041
- return (key, ask) => {
2096
+ return (key, ask2) => {
2042
2097
  const slot = slots.get(key);
2043
2098
  if (slot === void 0) {
2044
- return start(key, ask);
2099
+ return start(key, ask2);
2045
2100
  }
2046
2101
  slot.queued ??= slot.running.then(
2047
- () => start(key, ask),
2048
- () => start(key, ask)
2102
+ () => start(key, ask2),
2103
+ () => start(key, ask2)
2049
2104
  );
2050
2105
  return slot.queued;
2051
2106
  };
@@ -2061,6 +2116,18 @@ function idsOf(answers) {
2061
2116
  }
2062
2117
  return [...ids];
2063
2118
  }
2119
+ function countOf(answers) {
2120
+ let total = 0;
2121
+ for (const answer2 of Array.isArray(answers) ? answers : []) {
2122
+ if (typeof answer2 === "number" && Number.isSafeInteger(answer2) && answer2 > 0) {
2123
+ total += answer2;
2124
+ }
2125
+ }
2126
+ return total;
2127
+ }
2128
+ function answersOf(error) {
2129
+ return typeof error === "object" && error !== null ? error.responses : void 0;
2130
+ }
2064
2131
  async function readUsers(hub, room) {
2065
2132
  const { io } = hub;
2066
2133
  if (io === void 0) {
@@ -2094,11 +2161,13 @@ function answerRequests(hub, records) {
2094
2161
  hub.logger,
2095
2162
  PRESENCE_EVENT,
2096
2163
  (payload, answer2) => {
2097
- const { room, userId } = payload ?? {};
2164
+ const { room, userId, sockets } = payload ?? {};
2098
2165
  if (typeof room === "string") {
2099
2166
  answer2(records.users(room) ?? []);
2100
2167
  } else if (typeof userId === "string") {
2101
2168
  answer2((io.sockets.adapter.rooms.get(userRoom(userId))?.size ?? 0) > 0);
2169
+ } else if (typeof sockets === "string" && sockets.startsWith(STREAM_ROOM_PREFIX)) {
2170
+ answer2(io.sockets.adapter.rooms.get(sockets)?.size ?? 0);
2102
2171
  } else {
2103
2172
  answer2(null);
2104
2173
  }
@@ -2106,6 +2175,37 @@ function answerRequests(hub, records) {
2106
2175
  null
2107
2176
  );
2108
2177
  }
2178
+ function createCount(hub, now) {
2179
+ let warnedAt;
2180
+ const timedOut = () => {
2181
+ const at = now();
2182
+ if (warnedAt === void 0 || at - warnedAt >= PRESENCE_WARN_EVERY_MS) {
2183
+ warnedAt = at;
2184
+ hub.logger.warn(
2185
+ "A node did not answer a stream's subscriber count within cluster.timeoutMs (a node of an earlier quickdraw version during a rolling deploy, a node that stopped answering, or an adapter that does not answer server-side events): the sockets of the nodes whose answers did not arrive counted as 0",
2186
+ { category: "quickdraw.presence" }
2187
+ );
2188
+ }
2189
+ };
2190
+ return async (room) => {
2191
+ const { io } = hub;
2192
+ if (io === void 0) {
2193
+ return 0;
2194
+ }
2195
+ const timeoutMs = hub.cluster?.timeoutMs ?? DEFAULT_CLUSTER_TIMEOUT_MS;
2196
+ try {
2197
+ return countOf(
2198
+ await within(
2199
+ io.serverSideEmitWithAck(PRESENCE_EVENT, tagged(io, { sockets: room })),
2200
+ timeoutMs
2201
+ )
2202
+ );
2203
+ } catch (error) {
2204
+ timedOut();
2205
+ return countOf(answersOf(error));
2206
+ }
2207
+ };
2208
+ }
2109
2209
  function createPresenceRequests(hub, records, now = Date.now) {
2110
2210
  const coalesced = coalescer();
2111
2211
  let fallbackUntil = 0;
@@ -2121,7 +2221,8 @@ function createPresenceRequests(hub, records, now = Date.now) {
2121
2221
  );
2122
2222
  }
2123
2223
  };
2124
- const ask = async (payload, answered, read) => {
2224
+ const count3 = createCount(hub, now);
2225
+ const ask2 = async (payload, answered, read) => {
2125
2226
  const { io } = hub;
2126
2227
  if (io === void 0 || now() < fallbackUntil) {
2127
2228
  return await read();
@@ -2139,15 +2240,16 @@ function createPresenceRequests(hub, records, now = Date.now) {
2139
2240
  }
2140
2241
  };
2141
2242
  return Object.freeze({
2142
- users: (room) => coalesced(`r\0${room}`, () => ask({ room }, idsOf, () => readUsers(hub, room))),
2243
+ users: (room) => coalesced(`r\0${room}`, () => ask2({ room }, idsOf, () => readUsers(hub, room))),
2143
2244
  online: (userId) => coalesced(
2144
2245
  `u\0${userId}`,
2145
- () => ask(
2246
+ () => ask2(
2146
2247
  { userId },
2147
2248
  (answers) => Array.isArray(answers) && answers.includes(true),
2148
2249
  () => readOnline(hub, userId)
2149
2250
  )
2150
2251
  ),
2252
+ sockets: (room) => coalesced(`s\0${room}`, () => count3(room)),
2151
2253
  listen: () => {
2152
2254
  answerRequests(hub, records);
2153
2255
  }
@@ -2230,6 +2332,14 @@ async function usersInRoom(state, room) {
2230
2332
  const remote = await state.requests.users(room);
2231
2333
  return [.../* @__PURE__ */ new Set([...remote, ...localUsers(records, room)])];
2232
2334
  }
2335
+ async function socketsInRoom(state, room) {
2336
+ const { io } = state.hub;
2337
+ if (io === void 0) {
2338
+ return 0;
2339
+ }
2340
+ const remote = state.hub.probe.local() ? 0 : await state.requests.sockets(room);
2341
+ return remote + (io.sockets.adapter.rooms.get(room)?.size ?? 0);
2342
+ }
2233
2343
  async function inRoom(state, room, userId) {
2234
2344
  return (await usersInRoom(state, room)).includes(userId);
2235
2345
  }
@@ -2823,17 +2933,17 @@ async function touchedSince(batch, rev) {
2823
2933
  return { stale: moved === void 0 ? [] : joined, fresh: moved ?? rev };
2824
2934
  }
2825
2935
  const service = target2.service.name;
2826
- const stale = joined.filter((id) => hub.changeLog.lastChange(service, id) > rev);
2827
- return { stale, fresh: stale.length === 0 ? rev : currentRev() };
2936
+ const stale2 = joined.filter((id) => hub.changeLog.lastChange(service, id) > rev);
2937
+ return { stale: stale2, fresh: stale2.length === 0 ? rev : currentRev() };
2828
2938
  }
2829
2939
  async function rereadTouched(batch, rev) {
2830
2940
  const { hub, target: target2 } = batch;
2831
- const { stale, fresh } = await touchedSince(batch, rev);
2832
- if (stale.length === 0) {
2941
+ const { stale: stale2, fresh } = await touchedSince(batch, rev);
2942
+ if (stale2.length === 0) {
2833
2943
  return;
2834
2944
  }
2835
- const rows = await readRows2(hub, target2, stale);
2836
- for (const id of stale) {
2945
+ const rows = await readRows2(hub, target2, stale2);
2946
+ for (const id of stale2) {
2837
2947
  const row = rows.get(id);
2838
2948
  const answer2 = batch.answers.get(id);
2839
2949
  if (row === void 0 || answer2 === void 0) {
@@ -3100,17 +3210,17 @@ var StreamIndex = class {
3100
3210
  * changed row (any row of the service when `id` is absent), of the changed
3101
3211
  * user's sockets (every user's when `userId` is absent).
3102
3212
  */
3103
- matching(change) {
3104
- const key = change.id === void 0 ? void 0 : anchorKey(change.service, change.id);
3105
- const sockets = key === void 0 ? this.#byService.get(change.service) : this.#byAnchor.get(key);
3213
+ matching(change2) {
3214
+ const key = change2.id === void 0 ? void 0 : anchorKey(change2.service, change2.id);
3215
+ const sockets = key === void 0 ? this.#byService.get(change2.service) : this.#byAnchor.get(key);
3106
3216
  const found = /* @__PURE__ */ new Map();
3107
3217
  for (const socket of sockets?.keys() ?? []) {
3108
- if (change.userId !== void 0 && socket.data.principal?.userId !== change.userId) {
3218
+ if (change2.userId !== void 0 && socket.data.principal?.userId !== change2.userId) {
3109
3219
  continue;
3110
3220
  }
3111
3221
  const entries = this.entries(socket).filter(
3112
3222
  ({ anchors }) => anchors.some(
3113
- (anchor) => key === void 0 ? serviceOf(anchor) === change.service : anchor === key
3223
+ (anchor) => key === void 0 ? serviceOf(anchor) === change2.service : anchor === key
3114
3224
  )
3115
3225
  );
3116
3226
  if (entries.length > 0) {
@@ -3224,30 +3334,30 @@ async function settleAll(work) {
3224
3334
  throw failed2.reason instanceof Error ? failed2.reason : new Error("A re-resolution failed", { cause: failed2.reason });
3225
3335
  }
3226
3336
  }
3227
- function withRowState(hub, change) {
3228
- const { service, id } = change;
3337
+ function withRowState(hub, change2) {
3338
+ const { service, id } = change2;
3229
3339
  if (id === void 0) {
3230
- return change;
3340
+ return change2;
3231
3341
  }
3232
3342
  return {
3233
- ...change,
3343
+ ...change2,
3234
3344
  rev: hub.changeLog.lastChange(service, id),
3235
3345
  removed: hub.changeLog.removed(service, id)
3236
3346
  };
3237
3347
  }
3238
- async function broadcastChange(hub, change) {
3239
- await hub.broadcasts?.broadcast(ACCESS_CHANGED_EVENT, change);
3348
+ async function broadcastChange(hub, change2) {
3349
+ await hub.broadcasts?.broadcast(ACCESS_CHANGED_EVENT, change2);
3240
3350
  }
3241
3351
  function createRevocation(hub, hooks = []) {
3242
3352
  return Object.freeze({
3243
- async changed(change, remote) {
3353
+ async changed(change2, remote) {
3244
3354
  hub.subscriptions.accessChanges += 1;
3245
- const others = !remote && hub.io !== void 0 && !hub.probe.local() ? broadcastChange(hub, withRowState(hub, change)) : void 0;
3246
- const trustLog = !remote || change.removed !== void 0;
3355
+ const others = !remote && hub.io !== void 0 && !hub.probe.local() ? broadcastChange(hub, withRowState(hub, change2)) : void 0;
3356
+ const trustLog = !remote || change2.removed !== void 0;
3247
3357
  await settleAll([
3248
3358
  others,
3249
- reresolveAll(hub, hub.subscriptions.matching(change), trustLog),
3250
- ...hooks.map(async (hook) => await hook.changed(change))
3359
+ reresolveAll(hub, hub.subscriptions.matching(change2), trustLog),
3360
+ ...hooks.map(async (hook) => await hook.changed(change2))
3251
3361
  ]);
3252
3362
  },
3253
3363
  async regranted(userId) {
@@ -3288,24 +3398,24 @@ function readChange(value) {
3288
3398
  ...typeof userId === "string" && userId.length > 0 ? { userId } : {}
3289
3399
  };
3290
3400
  }
3291
- function recordRowState(hub, change) {
3292
- if (change.id !== void 0 && change.rev !== void 0 && change.removed !== void 0) {
3293
- hub.changeLog.record(change.service, change.id, change.rev, change.removed);
3401
+ function recordRowState(hub, change2) {
3402
+ if (change2.id !== void 0 && change2.rev !== void 0 && change2.removed !== void 0) {
3403
+ hub.changeLog.record(change2.service, change2.id, change2.rev, change2.removed);
3294
3404
  }
3295
3405
  }
3296
- function listenForChanges(hub, revocation, forget2) {
3406
+ function listenForChanges(hub, revocation, forget3) {
3297
3407
  if (hub.io === void 0) {
3298
3408
  return;
3299
3409
  }
3300
3410
  onNodeEvent(hub.io, hub.logger, ACCESS_CHANGED_EVENT, (broadcast, answer2) => {
3301
- const change = readChange(broadcast);
3302
- if (change === void 0) {
3411
+ const change2 = readChange(broadcast);
3412
+ if (change2 === void 0) {
3303
3413
  answer2(false);
3304
3414
  return;
3305
3415
  }
3306
- recordRowState(hub, change);
3307
- forget2(change);
3308
- revocation.changed(change, true).then(
3416
+ recordRowState(hub, change2);
3417
+ forget3(change2);
3418
+ revocation.changed(change2, true).then(
3309
3419
  () => {
3310
3420
  answer2(true);
3311
3421
  },
@@ -3313,7 +3423,7 @@ function listenForChanges(hub, revocation, forget2) {
3313
3423
  answer2(false);
3314
3424
  hub.logger.error("Revoking for an access change another node broadcast failed", {
3315
3425
  category: "quickdraw.access",
3316
- service: change.service,
3426
+ service: change2.service,
3317
3427
  error: describeError(error)
3318
3428
  });
3319
3429
  }
@@ -3346,6 +3456,12 @@ function streamTarget(hub, value, event) {
3346
3456
  if (stream === void 0) {
3347
3457
  throw new QuickdrawError("NOT_FOUND", `${frame.s} has no stream "${frame.stream}"`);
3348
3458
  }
3459
+ if (stream.host) {
3460
+ throw unreadable(
3461
+ `${frame.s}.${frame.stream} is served by the room's host: subscribe over the link to the host of its room`,
3462
+ ["stream"]
3463
+ );
3464
+ }
3349
3465
  const problem = scopeProblem(stream, frame.scope);
3350
3466
  if (problem !== void 0) {
3351
3467
  throw unreadable(problem, ["scope"]);
@@ -3391,7 +3507,9 @@ async function authorizeStream(hub, socket, target2) {
3391
3507
  signal: NEVER_ABORTED,
3392
3508
  log: hub.logger,
3393
3509
  requestId: crypto.randomUUID(),
3394
- transport: "socket"
3510
+ transport: "socket",
3511
+ // A custom form may ask about a row its scope names, as a method's does.
3512
+ access: hub.policies
3395
3513
  });
3396
3514
  const input = { scope: target2.scope };
3397
3515
  await hub.access.authorize(form, {
@@ -3488,7 +3606,7 @@ async function reauthorizeAll(hub, index, found) {
3488
3606
  }
3489
3607
  function createStreamRevocation(hub, index) {
3490
3608
  return Object.freeze({
3491
- changed: async (change) => await reauthorizeAll(hub, index, index.matching(change)),
3609
+ changed: async (change2) => await reauthorizeAll(hub, index, index.matching(change2)),
3492
3610
  regranted: async (sockets) => await reauthorizeAll(
3493
3611
  hub,
3494
3612
  index,
@@ -3877,17 +3995,17 @@ var ScopeIndex = class {
3877
3995
  * changed row (any row of the service when `id` is absent), of the changed
3878
3996
  * user's sockets (every user's when `userId` is absent).
3879
3997
  */
3880
- matching(change) {
3881
- const key = change.id === void 0 ? void 0 : anchorKey(change.service, change.id);
3882
- const sockets = key === void 0 ? this.#byService.get(change.service) : this.#byAnchor.get(key);
3998
+ matching(change2) {
3999
+ const key = change2.id === void 0 ? void 0 : anchorKey(change2.service, change2.id);
4000
+ const sockets = key === void 0 ? this.#byService.get(change2.service) : this.#byAnchor.get(key);
3883
4001
  const found = /* @__PURE__ */ new Map();
3884
4002
  for (const socket of sockets?.keys() ?? []) {
3885
- if (change.userId !== void 0 && socket.data.principal?.userId !== change.userId) {
4003
+ if (change2.userId !== void 0 && socket.data.principal?.userId !== change2.userId) {
3886
4004
  continue;
3887
4005
  }
3888
4006
  const entries = this.entries(socket).filter(
3889
4007
  ({ anchors }) => anchors.some(
3890
- (anchor) => key === void 0 ? serviceOf2(anchor) === change.service : anchor === key
4008
+ (anchor) => key === void 0 ? serviceOf2(anchor) === change2.service : anchor === key
3891
4009
  )
3892
4010
  );
3893
4011
  if (entries.length > 0) {
@@ -4489,12 +4607,16 @@ async function allowedScopes(policies, rule, principal, scopes) {
4489
4607
  const allowed2 = /* @__PURE__ */ new Map();
4490
4608
  const { anchor, service } = rule;
4491
4609
  const anchorRow = (scope) => anchor === void 0 ? [] : [anchorKey(anchor, scope)];
4492
- if (service.adminBypass && serviceGrant(principal, service.name) === "Admin") {
4610
+ const grant = serviceGrant(principal, service.name);
4611
+ if (service.adminBypass && grant === "Admin") {
4493
4612
  for (const scope of scopes) {
4494
4613
  allowed2.set(scope, anchorRow(scope));
4495
4614
  }
4496
4615
  return allowed2;
4497
4616
  }
4617
+ if (rule.requires !== void 0 && !meetsLevel(grant, rule.requires.service)) {
4618
+ return allowed2;
4619
+ }
4498
4620
  if (anchor === void 0) {
4499
4621
  for (const scope of scopes.filter((candidate) => candidate === principal.userId)) {
4500
4622
  allowed2.set(scope, []);
@@ -4519,7 +4641,8 @@ async function authorizeScopes(hub, collection, principal, scopes) {
4519
4641
  const rule = {
4520
4642
  service: collection.service,
4521
4643
  anchor: collection.anchorService?.name,
4522
- access: collection.access
4644
+ access: collection.access,
4645
+ requires: collection.requires
4523
4646
  };
4524
4647
  return await allowedScopes(hub.policies, rule, principal, scopes);
4525
4648
  }
@@ -4957,12 +5080,12 @@ function createConcurrencyLimiter(limits) {
4957
5080
  }
4958
5081
  };
4959
5082
  }
4960
- function enqueue(connectionId, lane, signal) {
5083
+ function enqueue(connectionId, lane, signal2) {
4961
5084
  return new Promise((resolve, reject) => {
4962
5085
  const enqueuedAt = performance.now();
4963
5086
  const waiter = { grant };
4964
5087
  function grant() {
4965
- signal?.removeEventListener("abort", onAbort);
5088
+ signal2?.removeEventListener("abort", onAbort);
4966
5089
  resolve(slot(connectionId, lane, performance.now() - enqueuedAt));
4967
5090
  }
4968
5091
  function onAbort() {
@@ -4973,13 +5096,13 @@ function createConcurrencyLimiter(limits) {
4973
5096
  dropIfIdle(connectionId, lane);
4974
5097
  reject(cancelledError());
4975
5098
  }
4976
- signal?.addEventListener("abort", onAbort, { once: true });
5099
+ signal2?.addEventListener("abort", onAbort, { once: true });
4977
5100
  lane.queue.push(waiter);
4978
5101
  });
4979
5102
  }
4980
5103
  return {
4981
- acquire(connectionId, signal) {
4982
- if (signal?.aborted === true) {
5104
+ acquire(connectionId, signal2) {
5105
+ if (signal2?.aborted === true) {
4983
5106
  return Promise.reject(cancelledError());
4984
5107
  }
4985
5108
  let lane = lanes.get(connectionId);
@@ -4997,7 +5120,7 @@ function createConcurrencyLimiter(limits) {
4997
5120
  new QuickdrawError("RATE_LIMITED", message, { retryAfterMs: limits.retryAfterMs })
4998
5121
  );
4999
5122
  }
5000
- return enqueue(connectionId, lane, signal);
5123
+ return enqueue(connectionId, lane, signal2);
5001
5124
  },
5002
5125
  get connections() {
5003
5126
  return lanes.size;
@@ -5111,14 +5234,14 @@ function answerEvent(socket, context, event, work) {
5111
5234
  }
5112
5235
 
5113
5236
  // src/server/collections/requests.ts
5114
- function isRecord5(value) {
5237
+ function isRecord6(value) {
5115
5238
  return typeof value === "object" && value !== null && !Array.isArray(value);
5116
5239
  }
5117
5240
  function isName2(value) {
5118
5241
  return typeof value === "string" && value.length > 0;
5119
5242
  }
5120
5243
  function readRef(frame, event) {
5121
- if (!isRecord5(frame) || !isName2(frame.s) || !isName2(frame.c) || !isName2(frame.scope)) {
5244
+ if (!isRecord6(frame) || !isName2(frame.s) || !isName2(frame.c) || !isName2(frame.scope)) {
5122
5245
  throw unreadable(`A ${event} frame needs { s, c, scope } with each a non-empty string`);
5123
5246
  }
5124
5247
  if (frame.scope.length > MAX_SCOPE_LENGTH) {
@@ -5565,7 +5688,7 @@ function raiseFloor(hub, collection, userId) {
5565
5688
  hub.collections.buffer.skipAll(groupOf(s, collection.name));
5566
5689
  return;
5567
5690
  }
5568
- hub.collections.buffer.skip(roomOf({ s, c: collection.name, scope: userId }), currentRev());
5691
+ hub.collections.buffer.reset(roomOf({ s, c: collection.name, scope: userId }), currentRev());
5569
5692
  }
5570
5693
  function logFailure(hub, collection, error) {
5571
5694
  hub.logger.error("Checking a self scope's rows again failed; the scope is reset", {
@@ -5608,33 +5731,38 @@ async function removeUnreadable(hub, io, collection, scope, id) {
5608
5731
  await resetScope(hub, io, collection, scope);
5609
5732
  }
5610
5733
  }
5611
- function recheckScopes(hub, io, collection, change) {
5612
- const own = change.service === collection.service.name ? change.id : void 0;
5613
- return hub.collections.scopes.scopes(collection.service.name, collection.name).filter((scope) => change.userId === void 0 || scope === change.userId).map(
5734
+ function recheckScopes(hub, io, collection, change2) {
5735
+ const own = change2.service === collection.service.name ? change2.id : void 0;
5736
+ return hub.collections.scopes.scopes(collection.service.name, collection.name).filter((scope) => change2.userId === void 0 || scope === change2.userId).map(
5614
5737
  (scope) => own === void 0 ? resetScope(hub, io, collection, scope) : removeUnreadable(hub, io, collection, scope, own)
5615
5738
  );
5616
5739
  }
5617
- async function recheckSelf(hub, change) {
5618
- if (change.id !== void 0 && change.userId === void 0) {
5740
+ async function recheckSelf(hub, change2) {
5741
+ if (change2.id !== void 0 && change2.userId === void 0) {
5619
5742
  return;
5620
5743
  }
5621
5744
  const { io } = hub;
5622
5745
  const work = [];
5623
5746
  for (const collection of selfCollections(hub)) {
5624
- if (!policyServices(hub, collection.service).has(change.service)) {
5747
+ if (!policyServices(hub, collection.service).has(change2.service)) {
5625
5748
  continue;
5626
5749
  }
5627
- raiseFloor(hub, collection, change.userId);
5750
+ raiseFloor(hub, collection, change2.userId);
5628
5751
  if (io !== void 0) {
5629
- work.push(...recheckScopes(hub, io, collection, change));
5752
+ work.push(...recheckScopes(hub, io, collection, change2));
5630
5753
  }
5631
5754
  }
5632
5755
  await Promise.all(work);
5633
5756
  }
5634
- async function resetSelf(hub, sockets, userId) {
5635
- for (const collection of userId === void 0 ? [] : selfCollections(hub)) {
5757
+ function raiseSelfFloors(hub, userId) {
5758
+ for (const collection of selfCollections(hub)) {
5636
5759
  raiseFloor(hub, collection, userId);
5637
5760
  }
5761
+ }
5762
+ async function resetSelf(hub, sockets, userId) {
5763
+ if (userId !== void 0) {
5764
+ raiseSelfFloors(hub, userId);
5765
+ }
5638
5766
  const { io } = hub;
5639
5767
  if (io === void 0) {
5640
5768
  return;
@@ -5717,17 +5845,22 @@ async function reauthorizeAll2(hub, found) {
5717
5845
  }
5718
5846
  function createScopeRevocation(hub) {
5719
5847
  return Object.freeze({
5720
- changed: async (change) => {
5848
+ changed: async (change2) => {
5721
5849
  await settleAll([
5722
- reauthorizeAll2(hub, hub.collections.scopes.matching(change)),
5723
- recheckSelf(hub, change)
5850
+ reauthorizeAll2(hub, hub.collections.scopes.matching(change2)),
5851
+ recheckSelf(hub, change2)
5724
5852
  ]);
5725
5853
  },
5726
5854
  regranted: async (sockets, userId) => {
5855
+ raiseSelfFloors(hub, userId);
5727
5856
  const found = new Map(
5728
5857
  sockets.map((socket) => [socket, hub.collections.scopes.entries(socket)])
5729
5858
  );
5730
- await settleAll([reauthorizeAll2(hub, found), resetSelf(hub, sockets, userId)]);
5859
+ try {
5860
+ await reauthorizeAll2(hub, found);
5861
+ } finally {
5862
+ await resetSelf(hub, sockets, userId);
5863
+ }
5731
5864
  }
5732
5865
  });
5733
5866
  }
@@ -5762,13 +5895,13 @@ function createLiveCollections(hub) {
5762
5895
  }
5763
5896
 
5764
5897
  // src/server/realtime/events.ts
5765
- function isRecord6(value) {
5898
+ function isRecord7(value) {
5766
5899
  return typeof value === "object" && value !== null && !Array.isArray(value);
5767
5900
  }
5768
5901
  function payloadSchemaOf(contract, event) {
5769
- const events = isRecord6(contract) ? contract.events : void 0;
5770
- if (!isRecord6(events) || typeof event !== "string" || !Object.hasOwn(events, event)) {
5771
- const name = isRecord6(contract) ? String(contract.name) : "the contract";
5902
+ const events = isRecord7(contract) ? contract.events : void 0;
5903
+ if (!isRecord7(events) || typeof event !== "string" || !Object.hasOwn(events, event)) {
5904
+ const name = isRecord7(contract) ? String(contract.name) : "the contract";
5772
5905
  throw new TypeError(`ctx.rooms.emit: ${name} declares no event "${String(event)}"`);
5773
5906
  }
5774
5907
  return events[event];
@@ -5805,6 +5938,304 @@ function createRoomEvents(hub) {
5805
5938
  });
5806
5939
  }
5807
5940
 
5941
+ // src/server/realtime/roomNames.ts
5942
+ function checkRoom2(room) {
5943
+ if (typeof room !== "string" || room === "" || room.length > MAX_SCOPE_LENGTH) {
5944
+ throw unreadable(`A room name is a string of 1 to ${MAX_SCOPE_LENGTH} characters`);
5945
+ }
5946
+ if (RESERVED_ROOM_PREFIXES.some((prefix) => room.startsWith(prefix))) {
5947
+ throw unreadable(
5948
+ `Room "${room}" is reserved: rooms starting with ${RESERVED_ROOM_PREFIXES.join(" or ")} are joined only through their own subscriptions`
5949
+ );
5950
+ }
5951
+ if (socketIdShaped(room)) {
5952
+ throw unreadable(
5953
+ `Room "${room}" has the shape of a Socket.IO socket id (20 letters, digits, "_" or "-"): every socket is alone in the room of its id, so an app room never has that shape; give it a prefix ("doc:${room}")`
5954
+ );
5955
+ }
5956
+ }
5957
+
5958
+ // src/server/realtime/hosts.ts
5959
+ var ROOM_HOST_EVENT = "quickdraw:room-host";
5960
+ var ROOM_HOSTS_EVENT = "quickdraw:room-hosts";
5961
+ var MAX_CLEARED = 1e4;
5962
+ var MICROS_PER_MS = 1e3;
5963
+ function wins(record, other) {
5964
+ return other === void 0 || record.version > other.version || record.version === other.version && record.host > other.host;
5965
+ }
5966
+ function stale(state, room, record) {
5967
+ const cleared = state.cleared.get(room);
5968
+ return cleared !== void 0 && !wins(record, cleared);
5969
+ }
5970
+ function observe(state, version) {
5971
+ state.clock = Math.max(state.clock, version);
5972
+ }
5973
+ function nextVersion(state) {
5974
+ state.clock = Math.max(Date.now() * MICROS_PER_MS, state.clock + 1);
5975
+ return state.clock;
5976
+ }
5977
+ function tell(state, room, host) {
5978
+ state.hub.io?.local.to(room).emit(SERVER_EVENTS.host, [room, host]);
5979
+ }
5980
+ function apply(state, room, record) {
5981
+ if (stale(state, room, record) || !wins(record, state.hosts.get(room))) {
5982
+ return false;
5983
+ }
5984
+ state.hosts.set(room, record);
5985
+ tell(state, room, record.host);
5986
+ return true;
5987
+ }
5988
+ function forget2(state, room, cleared) {
5989
+ const current = state.hosts.get(room);
5990
+ if (current !== void 0 && !wins(current, cleared)) {
5991
+ state.hosts.delete(room);
5992
+ tell(state, room, null);
5993
+ }
5994
+ const known = state.cleared.get(room);
5995
+ if (known === void 0 || wins(cleared, known)) {
5996
+ state.cleared.delete(room);
5997
+ state.cleared.set(room, cleared);
5998
+ const oldest = state.cleared.size > MAX_CLEARED ? state.cleared.keys().next().value : void 0;
5999
+ if (oldest !== void 0) {
6000
+ state.cleared.delete(oldest);
6001
+ }
6002
+ }
6003
+ }
6004
+ function publish(state, room, host, record) {
6005
+ const { io } = state.hub;
6006
+ if (io !== void 0 && !state.hub.probe.local()) {
6007
+ const was = host === null ? { was: record.host } : {};
6008
+ emitToNodes(io, ROOM_HOST_EVENT, { room, host, version: record.version, ...was });
6009
+ }
6010
+ }
6011
+ function change(state, room, host) {
6012
+ const record = { host, version: nextVersion(state) };
6013
+ apply(state, room, record);
6014
+ publish(state, room, host, record);
6015
+ }
6016
+ function repair(state, room, record) {
6017
+ const current = state.hosts.get(room);
6018
+ const cleared = state.cleared.get(room);
6019
+ if (cleared !== void 0 && wins(cleared, current)) {
6020
+ if (wins(cleared, record)) {
6021
+ publish(state, room, null, cleared);
6022
+ }
6023
+ } else if (current !== void 0 && wins(current, record)) {
6024
+ publish(state, room, current.host, current);
6025
+ }
6026
+ }
6027
+ function clear(state, room) {
6028
+ const current = state.hosts.get(room);
6029
+ if (current !== void 0) {
6030
+ forget2(state, room, current);
6031
+ publish(state, room, null, current);
6032
+ }
6033
+ }
6034
+ function setHost(state, socket, room) {
6035
+ checkRoom2(room);
6036
+ if (!state.served()) {
6037
+ throw new QuickdrawError(
6038
+ "INTERNAL",
6039
+ "ctx.rooms.setHost: host mode runs over peer links; create the server with peers: true"
6040
+ );
6041
+ }
6042
+ if (!socket.connected) {
6043
+ return false;
6044
+ }
6045
+ const joined = socket.data.appRooms;
6046
+ if (joined === void 0 || !Object.hasOwn(joined, room)) {
6047
+ throw new QuickdrawError("FORBIDDEN", `Only a socket in room "${room}" can host it`);
6048
+ }
6049
+ if (socket.data.principal === null) {
6050
+ throw new QuickdrawError("UNAUTHENTICATED", "Sign in to host a room");
6051
+ }
6052
+ if (state.hosts.get(room)?.host !== socket.id) {
6053
+ change(state, room, socket.id);
6054
+ }
6055
+ return true;
6056
+ }
6057
+ function hostLeft(state, socket, room, reason) {
6058
+ clear(state, room);
6059
+ const { hooks, run: run2 } = state;
6060
+ if (run2 === void 0) {
6061
+ return;
6062
+ }
6063
+ const leave2 = {
6064
+ room,
6065
+ socketId: socket.id,
6066
+ principal: socket.data.principal ?? null,
6067
+ reason
6068
+ };
6069
+ for (const { owner, handler } of hooks) {
6070
+ const ran = run2(async (ctx) => {
6071
+ await handler(leave2, ctx);
6072
+ }).catch((error) => {
6073
+ state.hub.logger.error("onHostLeave threw", {
6074
+ category: "quickdraw.rooms",
6075
+ owner,
6076
+ room,
6077
+ socketId: socket.id,
6078
+ reason,
6079
+ error: describeError(error)
6080
+ });
6081
+ });
6082
+ track(state.hub, ran);
6083
+ }
6084
+ }
6085
+ var isVersion = (version) => Number.isSafeInteger(version) && version > 0;
6086
+ function readChange2(payload) {
6087
+ const { room, host, version, was } = payload ?? {};
6088
+ if (!isRoomName(room) || !isVersion(version)) {
6089
+ return void 0;
6090
+ }
6091
+ if (host === null) {
6092
+ return isSocketId(was) ? { room, host, record: { host: was, version } } : void 0;
6093
+ }
6094
+ return isSocketId(host) ? { room, host, record: { host, version } } : void 0;
6095
+ }
6096
+ function merge(state, answers) {
6097
+ for (const answer2 of Array.isArray(answers) ? answers : []) {
6098
+ const { hosts, clock } = answer2 ?? {};
6099
+ if (isVersion(clock)) {
6100
+ observe(state, clock);
6101
+ }
6102
+ for (const entry of Array.isArray(hosts) ? hosts : []) {
6103
+ const [room, host, version] = Array.isArray(entry) ? entry : [];
6104
+ if (isRoomName(room) && isSocketId(host) && isVersion(version)) {
6105
+ observe(state, version);
6106
+ apply(state, room, { host, version });
6107
+ }
6108
+ }
6109
+ }
6110
+ }
6111
+ var HOSTS_BY_IO = /* @__PURE__ */ new WeakMap();
6112
+ function subscriberOf(io) {
6113
+ const adapter = io.sockets.adapter;
6114
+ const client = adapter?.subClient;
6115
+ if (typeof client !== "object" || client === null) {
6116
+ return void 0;
6117
+ }
6118
+ const { ping, isCluster, masters } = client;
6119
+ const cluster = isCluster === true || Array.isArray(masters);
6120
+ return typeof ping === "function" && !cluster ? client : void 0;
6121
+ }
6122
+ async function ask(state, io, timeoutMs) {
6123
+ try {
6124
+ merge(
6125
+ state,
6126
+ await within(io.serverSideEmitWithAck(ROOM_HOSTS_EVENT, tagged(io, {})), timeoutMs)
6127
+ );
6128
+ } catch {
6129
+ }
6130
+ }
6131
+ function pause(ms) {
6132
+ return new Promise((resolve) => {
6133
+ const timer = setTimeout(resolve, ms);
6134
+ timer.unref?.();
6135
+ });
6136
+ }
6137
+ function seed(state) {
6138
+ const { io } = state.hub;
6139
+ if (io === void 0 || state.hub.probe.local()) {
6140
+ return;
6141
+ }
6142
+ const timeoutMs = state.hub.cluster?.timeoutMs ?? DEFAULT_CLUSTER_TIMEOUT_MS;
6143
+ const twice = async () => {
6144
+ await ask(state, io, timeoutMs);
6145
+ await pause(timeoutMs);
6146
+ await ask(state, io, timeoutMs);
6147
+ };
6148
+ const subscriber = subscriberOf(io);
6149
+ if (subscriber === void 0) {
6150
+ void twice();
6151
+ return;
6152
+ }
6153
+ void within(
6154
+ Promise.resolve().then(() => subscriber.ping()),
6155
+ timeoutMs
6156
+ ).then(async () => {
6157
+ await ask(state, io, timeoutMs);
6158
+ }, twice);
6159
+ }
6160
+ function listen(state) {
6161
+ const { io } = state.hub;
6162
+ if (io === void 0) {
6163
+ return;
6164
+ }
6165
+ onNodeEvent(io, state.hub.logger, ROOM_HOST_EVENT, (payload) => {
6166
+ const read = readChange2(payload);
6167
+ if (read === void 0) {
6168
+ return;
6169
+ }
6170
+ observe(state, read.record.version);
6171
+ if (read.host === null) {
6172
+ forget2(state, read.room, read.record);
6173
+ } else if (!apply(state, read.room, read.record)) {
6174
+ repair(state, read.room, read.record);
6175
+ }
6176
+ });
6177
+ onNodeEvent(
6178
+ io,
6179
+ state.hub.logger,
6180
+ ROOM_HOSTS_EVENT,
6181
+ (_payload, answer2) => {
6182
+ answer2({
6183
+ hosts: [...state.hosts].filter(([, record]) => io.sockets.sockets.has(record.host)).map(([room, record]) => [room, record.host, record.version]),
6184
+ clock: state.clock
6185
+ });
6186
+ },
6187
+ { hosts: [] }
6188
+ );
6189
+ HOSTS_BY_IO.set(io, state);
6190
+ seed(state);
6191
+ }
6192
+ function seedRoomHosts(io) {
6193
+ const state = HOSTS_BY_IO.get(io);
6194
+ if (state !== void 0) {
6195
+ seed(state);
6196
+ }
6197
+ }
6198
+ function createRoomHosts(hub, served) {
6199
+ const state = {
6200
+ hub,
6201
+ served,
6202
+ hosts: /* @__PURE__ */ new Map(),
6203
+ cleared: /* @__PURE__ */ new Map(),
6204
+ clock: 0,
6205
+ hooks: [],
6206
+ run: void 0
6207
+ };
6208
+ return Object.freeze({
6209
+ set: (socket, room) => setHost(state, socket, room),
6210
+ of(room) {
6211
+ checkRoom2(room);
6212
+ return state.hosts.get(room)?.host ?? null;
6213
+ },
6214
+ clear(room) {
6215
+ checkRoom2(room);
6216
+ clear(state, room);
6217
+ },
6218
+ joined(socket, room) {
6219
+ const record = state.hosts.get(room);
6220
+ if (record !== void 0) {
6221
+ socket.emit(SERVER_EVENTS.host, [room, record.host]);
6222
+ }
6223
+ },
6224
+ left(socket, room, reason) {
6225
+ if (state.hosts.get(room)?.host === socket.id) {
6226
+ hostLeft(state, socket, room, reason);
6227
+ }
6228
+ },
6229
+ onLeave(hooks, run2) {
6230
+ state.hooks = hooks;
6231
+ state.run = run2;
6232
+ },
6233
+ listen: () => {
6234
+ listen(state);
6235
+ }
6236
+ });
6237
+ }
6238
+
5808
6239
  // src/server/realtime/roomFrames.ts
5809
6240
  function logFailure2(state, room, error) {
5810
6241
  state.hub.logger.error("Reading a room's presence from the cluster failed", {
@@ -5878,25 +6309,11 @@ function exited(state, socket, room, told, everywhere = false) {
5878
6309
  return last;
5879
6310
  }
5880
6311
 
5881
- // src/server/realtime/roomNames.ts
5882
- function checkRoom2(room) {
5883
- if (typeof room !== "string" || room === "" || room.length > MAX_SCOPE_LENGTH) {
5884
- throw unreadable(`A room name is a string of 1 to ${MAX_SCOPE_LENGTH} characters`);
5885
- }
5886
- if (RESERVED_ROOM_PREFIXES.some((prefix) => room.startsWith(prefix))) {
5887
- throw unreadable(
5888
- `Room "${room}" is reserved: rooms starting with ${RESERVED_ROOM_PREFIXES.join(" or ")} are joined only through their own subscriptions`
5889
- );
5890
- }
5891
- if (socketIdShaped(room)) {
5892
- throw unreadable(
5893
- `Room "${room}" has the shape of a Socket.IO socket id (20 letters, digits, "_" or "-"): every socket is alone in the room of its id, so an app room never has that shape; give it a prefix ("doc:${room}")`
5894
- );
5895
- }
5896
- }
5897
-
5898
6312
  // src/server/realtime/leaving.ts
5899
6313
  var ROOM_LEAVE_EVENT = "quickdraw:room-leave";
6314
+ function leavingState(base, left, served) {
6315
+ return { ...base, listener: void 0, left, hosts: createRoomHosts(base.hub, served) };
6316
+ }
5900
6317
  function leaveListener(hooks, run2, logger) {
5901
6318
  return async (leave2) => {
5902
6319
  await Promise.all(
@@ -5948,6 +6365,7 @@ function leaveRoom(state, socket, room, reason) {
5948
6365
  state.left?.(socket);
5949
6366
  }
5950
6367
  const last = exited(state, socket, room, socket.connected, reason === "removed");
6368
+ state.hosts.left(socket, room, reason);
5951
6369
  heard(state, socket, reason, [[room, last]]);
5952
6370
  return true;
5953
6371
  }
@@ -5955,6 +6373,9 @@ function leaveAll(state, socket) {
5955
6373
  const left = Object.keys(socket.data.appRooms ?? {}).map(
5956
6374
  (room) => [room, exited(state, socket, room, false)]
5957
6375
  );
6376
+ for (const [room] of left) {
6377
+ state.hosts.left(socket, room, "disconnect");
6378
+ }
5958
6379
  socket.data.appRooms = emptyRecords();
5959
6380
  heard(state, socket, "disconnect", left);
5960
6381
  }
@@ -6018,6 +6439,7 @@ function join3(state, socket, room) {
6018
6439
  joined[room] = true;
6019
6440
  void socket.join(room);
6020
6441
  entered(state, socket, room);
6442
+ state.hosts.joined(socket, room);
6021
6443
  return true;
6022
6444
  }
6023
6445
  function sizeOf(state, room) {
@@ -6033,21 +6455,38 @@ function leaveOf(own, removal) {
6033
6455
  return own(room);
6034
6456
  });
6035
6457
  }
6036
- function createRooms(base, left) {
6037
- const state = { ...base, listener: void 0, left };
6458
+ function createRooms(base, left, served = () => false) {
6459
+ const state = leavingState(base, left, served);
6038
6460
  const events = createRoomEvents(state.hub);
6039
6461
  const removal = (room, target2) => removeUser(state, room, target2);
6040
6462
  const size = (room) => sizeOf(state, room);
6041
- const server = Object.freeze({ ...events, leave: removal, size });
6463
+ const { hosts } = state;
6464
+ const hostOf = (room) => hosts.of(room);
6465
+ const clearHost = (room) => {
6466
+ hosts.clear(room);
6467
+ };
6468
+ const server = Object.freeze({
6469
+ ...events,
6470
+ leave: removal,
6471
+ size,
6472
+ hostOf,
6473
+ clearHost
6474
+ });
6042
6475
  const bySocket = /* @__PURE__ */ new WeakMap();
6043
6476
  const detached = Object.freeze({
6044
6477
  ...events,
6045
6478
  size,
6479
+ hostOf,
6480
+ clearHost,
6046
6481
  join: (room) => {
6047
6482
  checkRoom2(room);
6048
6483
  return false;
6049
6484
  },
6050
- leave: leaveOf(() => false, removal)
6485
+ leave: leaveOf(() => false, removal),
6486
+ setHost: (room) => {
6487
+ checkRoom2(room);
6488
+ return false;
6489
+ }
6051
6490
  });
6052
6491
  return Object.freeze({
6053
6492
  of(socket) {
@@ -6056,8 +6495,11 @@ function createRooms(base, left) {
6056
6495
  rooms = Object.freeze({
6057
6496
  ...events,
6058
6497
  size,
6498
+ hostOf,
6499
+ clearHost,
6059
6500
  join: (room) => join3(state, socket, room),
6060
- leave: leaveOf((room) => leaveRoom(state, socket, room, "leave"), removal)
6501
+ leave: leaveOf((room) => leaveRoom(state, socket, room, "leave"), removal),
6502
+ setHost: (room) => hosts.set(socket, room)
6061
6503
  });
6062
6504
  bySocket.set(socket, rooms);
6063
6505
  }
@@ -6076,8 +6518,10 @@ function createRooms(base, left) {
6076
6518
  onLeave(hooks, run2) {
6077
6519
  state.listener = hooks.length === 0 ? void 0 : leaveListener(hooks, run2, state.hub.logger);
6078
6520
  },
6521
+ hosts,
6079
6522
  listen() {
6080
6523
  listenForRemovals(state);
6524
+ hosts.listen();
6081
6525
  }
6082
6526
  });
6083
6527
  }
@@ -6091,6 +6535,7 @@ function unjoinable(rooms, share) {
6091
6535
  return Object.freeze({
6092
6536
  ...rooms,
6093
6537
  join: () => refuse2("join"),
6538
+ setHost: () => refuse2("setHost"),
6094
6539
  leave: ((room, target2) => target2 === void 0 ? refuse2("leave") : rooms.leave(room, target2))
6095
6540
  });
6096
6541
  }
@@ -6205,6 +6650,11 @@ function servedStream(hub, contract, name) {
6205
6650
  if (stream === void 0) {
6206
6651
  throw new TypeError(`stream: ${service.name} has no stream "${String(name)}"`);
6207
6652
  }
6653
+ if (stream.host) {
6654
+ throw new TypeError(
6655
+ `stream: ${service.name}.${stream.name} is served by the room's host (serve: "host"): its host's client pushes it, with defineHost; the server cannot`
6656
+ );
6657
+ }
6208
6658
  return { service, stream };
6209
6659
  }
6210
6660
  function feedOf(served, method, what, args) {
@@ -6238,7 +6688,17 @@ function pushMany(hub, seeds, served, args) {
6238
6688
  );
6239
6689
  sendItems(hub, seeds, served, feed, checked);
6240
6690
  }
6241
- function createStreams(hub) {
6691
+ async function subscribers(served, count3, args) {
6692
+ const { service, stream } = served;
6693
+ const scope = args[0];
6694
+ const problem = scopeProblem(stream, scope);
6695
+ if (problem !== void 0 || args.length !== (stream.scoped ? 1 : 0)) {
6696
+ const usage = stream.scoped ? "pass (scope)" : "pass no argument";
6697
+ throw new TypeError(`${service.name}.${stream.name}.subscribers: ${problem ?? usage}`);
6698
+ }
6699
+ return await count3(streamRoom(service.name, stream.name, scope));
6700
+ }
6701
+ function createStreams(hub, count3) {
6242
6702
  const seeds = new StreamSeeds();
6243
6703
  const feeds = createStreamFeeds(hub, seeds);
6244
6704
  return Object.freeze({
@@ -6250,7 +6710,8 @@ function createStreams(hub) {
6250
6710
  },
6251
6711
  pushMany: (...args) => {
6252
6712
  pushMany(hub, seeds, served, args);
6253
- }
6713
+ },
6714
+ subscribers: (...args) => subscribers(served, count3, args)
6254
6715
  });
6255
6716
  },
6256
6717
  extension: feeds.extension,
@@ -6263,12 +6724,25 @@ function createStreams(hub) {
6263
6724
  }
6264
6725
 
6265
6726
  // src/server/realtime/realtime.ts
6266
- function createRealtime(hub) {
6727
+ function createRealtime(hub, peers) {
6267
6728
  const state = createPresenceState(hub);
6268
6729
  const presence = createPresence(state);
6269
- const streams = createStreams(hub);
6270
- const rooms = createRooms(state, streams.leftRooms);
6271
- const channels = channelMessages({ hub, rooms, presence, warned: /* @__PURE__ */ new WeakSet() });
6730
+ const streams = createStreams(hub, (room) => socketsInRoom(state, room));
6731
+ const rooms = createRooms(
6732
+ state,
6733
+ (socket) => {
6734
+ streams.leftRooms(socket);
6735
+ peers?.left(socket);
6736
+ },
6737
+ () => peers?.served() ?? false
6738
+ );
6739
+ const channels = channelMessages({
6740
+ hub,
6741
+ rooms,
6742
+ presence,
6743
+ warned: /* @__PURE__ */ new WeakSet(),
6744
+ hostWarned: /* @__PURE__ */ new Set()
6745
+ });
6272
6746
  return Object.freeze({
6273
6747
  extension(socket, context) {
6274
6748
  channels(socket);
@@ -6287,6 +6761,9 @@ function createRealtime(hub) {
6287
6761
  onRoomLeave: (hooks, run2) => {
6288
6762
  rooms.onLeave(hooks, run2);
6289
6763
  },
6764
+ onHostLeave: (hooks, run2) => {
6765
+ rooms.hosts.onLeave(hooks, run2);
6766
+ },
6290
6767
  roomsFor(transport, connectionId, share) {
6291
6768
  const socket = (transport === "socket" || transport === "legacy") && connectionId !== void 0 ? hub.io?.sockets.sockets.get(connectionId) : void 0;
6292
6769
  const own = socket === void 0 ? rooms.detached : rooms.of(socket);
@@ -6306,6 +6783,398 @@ function createRealtime(hub) {
6306
6783
  });
6307
6784
  }
6308
6785
 
6786
+ // src/server/realtime/peerFrames.ts
6787
+ var SIGNAL_SHAPE = "A qd:peer frame needs { room, to, sig }: an app room, another socket's id, and a signal holding one of sdp, ice, relay: true or bye: true";
6788
+ function readSignalFrame(frame, self) {
6789
+ if (!isRecord(frame) || !isRoomName(frame.room) || !isSocketId(frame.to)) {
6790
+ throw unreadable(SIGNAL_SHAPE);
6791
+ }
6792
+ if (frame.to === self) {
6793
+ throw unreadable("A socket cannot link to itself", ["to"]);
6794
+ }
6795
+ const sig = readSignal(frame.sig);
6796
+ if (sig === void 0) {
6797
+ throw unreadable(SIGNAL_SHAPE, ["sig"]);
6798
+ }
6799
+ return { room: frame.room, to: frame.to, sig };
6800
+ }
6801
+ function messageBytes(text) {
6802
+ if (text.length > MAX_PEER_MESSAGE_BYTES) {
6803
+ return void 0;
6804
+ }
6805
+ const bytes = utf8ByteLength(text);
6806
+ return bytes > MAX_PEER_MESSAGE_BYTES ? void 0 : bytes;
6807
+ }
6808
+ function readRelayFrame(frame) {
6809
+ if (!Array.isArray(frame)) {
6810
+ return void 0;
6811
+ }
6812
+ const [room, to, label, text] = frame;
6813
+ if (!isRoomName(room) || !isSocketId(to) || !isPeerLabel(label) || typeof text !== "string") {
6814
+ return void 0;
6815
+ }
6816
+ const bytes = messageBytes(text);
6817
+ return bytes === void 0 ? void 0 : { room, to, label, text, bytes };
6818
+ }
6819
+ function readForwardedSignal(value) {
6820
+ if (!isRecord(value)) {
6821
+ return void 0;
6822
+ }
6823
+ const { room, to, from, userId } = value;
6824
+ const sig = readSignal(value.sig);
6825
+ if (!isRoomName(room) || !isSocketId(to) || !isSocketId(from) || sig === void 0) {
6826
+ return void 0;
6827
+ }
6828
+ return typeof userId === "string" && userId !== "" ? { room, to, from, userId, sig } : void 0;
6829
+ }
6830
+ function readForwardedRelay(value) {
6831
+ if (!isRecord(value)) {
6832
+ return void 0;
6833
+ }
6834
+ const { room, to, from, label, text } = value;
6835
+ if (!isRoomName(room) || !isSocketId(to) || !isSocketId(from) || !isPeerLabel(label)) {
6836
+ return void 0;
6837
+ }
6838
+ return typeof text === "string" && messageBytes(text) !== void 0 ? { room, to, from, label, text } : void 0;
6839
+ }
6840
+
6841
+ // src/server/realtime/peerState.ts
6842
+ var MAX_HEARD = MAX_PEER_LINKS * 2;
6843
+ var STATES = /* @__PURE__ */ new WeakMap();
6844
+ function bucket(tokens, now) {
6845
+ return { tokens, refilledAt: now };
6846
+ }
6847
+ function peersOf(socket, now = Date.now()) {
6848
+ let state = STATES.get(socket);
6849
+ if (state === void 0) {
6850
+ state = {
6851
+ socket,
6852
+ signals: bucket(PEER_SIGNAL_BURST, now),
6853
+ bytes: bucket(PEER_RELAY_BYTES_BURST, now),
6854
+ links: /* @__PURE__ */ new Map(),
6855
+ count: 0,
6856
+ heard: /* @__PURE__ */ new Map(),
6857
+ signalDrops: { windowStart: now, dropped: 0 },
6858
+ relayDrops: { windowStart: now, dropped: 0 }
6859
+ };
6860
+ STATES.set(socket, state);
6861
+ }
6862
+ return state;
6863
+ }
6864
+ function knownPeers(socket) {
6865
+ return STATES.get(socket);
6866
+ }
6867
+ function take2(state, rate, burst, now, cost = 1) {
6868
+ state.tokens = Math.min(burst, state.tokens + (now - state.refilledAt) / 1e3 * rate);
6869
+ state.refilledAt = now;
6870
+ if (state.tokens < cost) {
6871
+ return false;
6872
+ }
6873
+ state.tokens -= cost;
6874
+ return true;
6875
+ }
6876
+ function takeSignal(state, now) {
6877
+ return take2(state.signals, PEER_SIGNAL_RATE, PEER_SIGNAL_BURST, now);
6878
+ }
6879
+ function signalWaitMs(state) {
6880
+ return Math.ceil((1 - state.signals.tokens) / PEER_SIGNAL_RATE * 1e3);
6881
+ }
6882
+ function takeRelay(state, pair, bytes, now) {
6883
+ if (!take2(pair, PEER_RELAY_RATE, PEER_RELAY_BURST, now)) {
6884
+ return false;
6885
+ }
6886
+ return take2(state.bytes, PEER_RELAY_BYTES_PER_SECOND, PEER_RELAY_BYTES_BURST, now, bytes);
6887
+ }
6888
+ function abusive2(drops, rate, now) {
6889
+ if (now - drops.windowStart > CHANNEL_ABUSE_WINDOW_MS) {
6890
+ drops.windowStart = now;
6891
+ drops.dropped = 0;
6892
+ }
6893
+ drops.dropped += 1;
6894
+ return drops.dropped > rate * CHANNEL_ABUSE_MULTIPLIER;
6895
+ }
6896
+ function signalFlood(state, now) {
6897
+ return abusive2(state.signalDrops, PEER_SIGNAL_RATE, now);
6898
+ }
6899
+ function relayFlood(state, now) {
6900
+ return abusive2(state.relayDrops, PEER_RELAY_RATE, now);
6901
+ }
6902
+ function pairOf(state, room, peer) {
6903
+ return state.links.get(room)?.get(peer);
6904
+ }
6905
+ var heardKey = (room, peer) => `${room}\0${peer}`;
6906
+ function hear(state, room, peer) {
6907
+ const pair = pairOf(state, room, peer);
6908
+ if (pair !== void 0) {
6909
+ pair.mutual = true;
6910
+ return;
6911
+ }
6912
+ const key = heardKey(room, peer);
6913
+ state.heard.delete(key);
6914
+ state.heard.set(key, [room, peer]);
6915
+ if (state.heard.size > MAX_HEARD) {
6916
+ const [oldest] = state.heard.keys();
6917
+ if (oldest !== void 0) {
6918
+ state.heard.delete(oldest);
6919
+ }
6920
+ }
6921
+ }
6922
+ function engage(state, room, peer, now) {
6923
+ if (pairOf(state, room, peer) !== void 0) {
6924
+ return true;
6925
+ }
6926
+ if (state.count >= MAX_PEER_LINKS) {
6927
+ return false;
6928
+ }
6929
+ let peers = state.links.get(room);
6930
+ if (peers === void 0) {
6931
+ peers = /* @__PURE__ */ new Map();
6932
+ state.links.set(room, peers);
6933
+ }
6934
+ const mutual = state.heard.delete(heardKey(room, peer));
6935
+ peers.set(peer, { ...bucket(PEER_RELAY_BURST, now), mutual });
6936
+ state.count += 1;
6937
+ return true;
6938
+ }
6939
+ function disengage(state, room, peer) {
6940
+ state.heard.delete(heardKey(room, peer));
6941
+ const peers = state.links.get(room);
6942
+ if (peers?.delete(peer) === true) {
6943
+ state.count -= 1;
6944
+ if (peers.size === 0) {
6945
+ state.links.delete(room);
6946
+ }
6947
+ }
6948
+ }
6949
+ function disengageAll(state, ended = () => true) {
6950
+ const gone = [];
6951
+ for (const [room, peers] of state.links) {
6952
+ if (ended(room)) {
6953
+ for (const peer of peers.keys()) {
6954
+ gone.push([room, peer]);
6955
+ }
6956
+ state.links.delete(room);
6957
+ state.count -= peers.size;
6958
+ }
6959
+ }
6960
+ for (const [key, entry] of state.heard) {
6961
+ if (ended(entry[0])) {
6962
+ state.heard.delete(key);
6963
+ gone.push(entry);
6964
+ }
6965
+ }
6966
+ return gone;
6967
+ }
6968
+ function inRoom2(socket, room) {
6969
+ return ownRecord(socket.data.appRooms, room) !== void 0;
6970
+ }
6971
+ function reachable(socket, room) {
6972
+ return socket.connected && socket.data.protocol !== "legacy" && socket.data.principal !== null && inRoom2(socket, room);
6973
+ }
6974
+
6975
+ // src/server/realtime/peerDelivery.ts
6976
+ var PEER_SIGNAL_EVENT = "quickdraw:peer";
6977
+ var PEER_RELAY_EVENT = "quickdraw:relay";
6978
+ function localSocket(hub, id) {
6979
+ return hub.io?.sockets.sockets.get(id);
6980
+ }
6981
+ function clusterOf(hub) {
6982
+ return hub.probe.local() ? void 0 : hub.io;
6983
+ }
6984
+ function emitSignal(target2, frame) {
6985
+ target2.emit(SERVER_EVENTS.peer, frame);
6986
+ if (frame.sig.bye !== true) {
6987
+ hear(peersOf(target2), frame.room, frame.from);
6988
+ return;
6989
+ }
6990
+ const state = knownPeers(target2);
6991
+ if (state !== void 0) {
6992
+ disengage(state, frame.room, frame.from);
6993
+ }
6994
+ }
6995
+ var waiting = /* @__PURE__ */ new WeakMap();
6996
+ function emitRelay(target2, frame) {
6997
+ const count3 = waiting.get(target2) ?? 0;
6998
+ if (frame[2] === "fast" && count3 >= PEER_RELAY_FAST_BACKLOG) {
6999
+ return;
7000
+ }
7001
+ if (count3 === 0) {
7002
+ target2.conn.once("drain", () => {
7003
+ waiting.delete(target2);
7004
+ });
7005
+ }
7006
+ waiting.set(target2, count3 + 1);
7007
+ target2.emit(SERVER_EVENTS.relay, frame);
7008
+ }
7009
+ function sendSignal(hub, to, frame) {
7010
+ const target2 = localSocket(hub, to);
7011
+ if (target2 !== void 0) {
7012
+ if (reachable(target2, frame.room)) {
7013
+ emitSignal(target2, frame);
7014
+ }
7015
+ return;
7016
+ }
7017
+ const io = clusterOf(hub);
7018
+ if (io !== void 0) {
7019
+ emitToNodes(io, PEER_SIGNAL_EVENT, { ...frame, to });
7020
+ }
7021
+ }
7022
+ function sendRelay(hub, to, frame) {
7023
+ const target2 = localSocket(hub, to);
7024
+ if (target2 !== void 0) {
7025
+ if (reachable(target2, frame[0])) {
7026
+ emitRelay(target2, frame);
7027
+ }
7028
+ return;
7029
+ }
7030
+ const io = clusterOf(hub);
7031
+ if (io !== void 0) {
7032
+ const [room, from, label, text] = frame;
7033
+ emitToNodes(io, PEER_RELAY_EVENT, { room, to, from, label, text });
7034
+ }
7035
+ }
7036
+ function sayGoodbye(hub, state, ended) {
7037
+ const { socket } = state;
7038
+ const userId = socket.data.principal?.userId;
7039
+ for (const [room, peer] of disengageAll(state, ended)) {
7040
+ if (userId !== void 0) {
7041
+ sendSignal(hub, peer, { room, from: socket.id, userId, sig: { bye: true } });
7042
+ }
7043
+ }
7044
+ }
7045
+ function listenForPeers(hub) {
7046
+ const { io } = hub;
7047
+ if (io === void 0) {
7048
+ return;
7049
+ }
7050
+ onNodeEvent(io, hub.logger, PEER_SIGNAL_EVENT, (payload) => {
7051
+ const signal2 = readForwardedSignal(payload);
7052
+ const target2 = signal2 === void 0 ? void 0 : localSocket(hub, signal2.to);
7053
+ if (signal2 !== void 0 && target2 !== void 0 && reachable(target2, signal2.room)) {
7054
+ const { room, from, userId, sig } = signal2;
7055
+ emitSignal(target2, { room, from, userId, sig });
7056
+ }
7057
+ });
7058
+ onNodeEvent(io, hub.logger, PEER_RELAY_EVENT, (payload) => {
7059
+ const relay2 = readForwardedRelay(payload);
7060
+ const target2 = relay2 === void 0 ? void 0 : localSocket(hub, relay2.to);
7061
+ if (relay2 !== void 0 && target2 !== void 0 && reachable(target2, relay2.room)) {
7062
+ emitRelay(target2, [relay2.room, relay2.from, relay2.label, relay2.text]);
7063
+ }
7064
+ });
7065
+ }
7066
+
7067
+ // src/server/realtime/peers.ts
7068
+ var OK = Object.freeze({ ok: true });
7069
+ function flooded2(hub, state, event) {
7070
+ hub.logger.warn(`Disconnecting socket ${state.socket.id} for sustained flooding of ${event}`, {
7071
+ category: "quickdraw.peer",
7072
+ userId: state.socket.data.principal?.userId
7073
+ });
7074
+ state.socket.disconnect(true);
7075
+ }
7076
+ function overRate(hub, state, now) {
7077
+ if (signalFlood(state, now)) {
7078
+ flooded2(hub, state, CLIENT_EVENTS.peer);
7079
+ }
7080
+ return new QuickdrawError("RATE_LIMITED", "Too many peer signals from this socket", {
7081
+ retryAfterMs: signalWaitMs(state)
7082
+ });
7083
+ }
7084
+ function signal(hub, state, frame) {
7085
+ const { socket } = state;
7086
+ const { room, to, sig } = readSignalFrame(frame, socket.id);
7087
+ const now = Date.now();
7088
+ if (!takeSignal(state, now)) {
7089
+ if (sig.bye === true) {
7090
+ disengage(state, room, to);
7091
+ }
7092
+ throw overRate(hub, state, now);
7093
+ }
7094
+ const { principal } = socket.data;
7095
+ if (principal === null) {
7096
+ throw new QuickdrawError("UNAUTHENTICATED", "Sign in to signal a peer");
7097
+ }
7098
+ if (!inRoom2(socket, room)) {
7099
+ throw new QuickdrawError("FORBIDDEN", `This socket is not in room "${room}"`);
7100
+ }
7101
+ const forward = { room, from: socket.id, userId: principal.userId, sig };
7102
+ if (sig.bye === true) {
7103
+ disengage(state, room, to);
7104
+ sendSignal(hub, to, forward);
7105
+ return OK;
7106
+ }
7107
+ const target2 = localSocket(hub, to);
7108
+ if (target2 === void 0 ? clusterOf(hub) === void 0 : !reachable(target2, room)) {
7109
+ disengage(state, room, to);
7110
+ throw new QuickdrawError("NOT_FOUND", `No signed-in socket ${to} is in room "${room}"`);
7111
+ }
7112
+ if (!engage(state, room, to, now)) {
7113
+ throw new QuickdrawError(
7114
+ "CONFLICT",
7115
+ `A socket may signal at most ${MAX_PEER_LINKS} peers at once; close a link first`
7116
+ );
7117
+ }
7118
+ sendSignal(hub, to, forward);
7119
+ return OK;
7120
+ }
7121
+ function relay(hub, state, frame) {
7122
+ const now = Date.now();
7123
+ const read = readRelayFrame(frame);
7124
+ const pair = read === void 0 ? void 0 : pairOf(state, read.room, read.to);
7125
+ if (read === void 0 || pair?.mutual !== true || !inRoom2(state.socket, read.room) || !takeRelay(state, pair, read.bytes, now)) {
7126
+ if (relayFlood(state, now)) {
7127
+ flooded2(hub, state, CLIENT_EVENTS.relay);
7128
+ }
7129
+ return;
7130
+ }
7131
+ sendRelay(hub, read.to, [read.room, state.socket.id, read.label, read.text]);
7132
+ }
7133
+ function createPeerLinks(hub) {
7134
+ let served = false;
7135
+ return Object.freeze({
7136
+ extension(socket, context) {
7137
+ socket.on(CLIENT_EVENTS.peer, (frame, ack) => {
7138
+ answerNow(
7139
+ socket,
7140
+ context,
7141
+ CLIENT_EVENTS.peer,
7142
+ ack,
7143
+ () => signal(hub, peersOf(socket), frame)
7144
+ );
7145
+ });
7146
+ socket.on(CLIENT_EVENTS.relay, (frame) => {
7147
+ try {
7148
+ relay(hub, peersOf(socket), frame);
7149
+ } catch (error) {
7150
+ hub.logger.error("A relay message failed", {
7151
+ category: "quickdraw.peer",
7152
+ socketId: socket.id,
7153
+ error: describeError(error)
7154
+ });
7155
+ }
7156
+ });
7157
+ onDisconnect(socket, context, () => {
7158
+ const state = knownPeers(socket);
7159
+ if (state !== void 0) {
7160
+ sayGoodbye(hub, state);
7161
+ }
7162
+ });
7163
+ },
7164
+ listen: () => {
7165
+ served = true;
7166
+ listenForPeers(hub);
7167
+ },
7168
+ served: () => served,
7169
+ left(socket) {
7170
+ const state = knownPeers(socket);
7171
+ if (state !== void 0) {
7172
+ sayGoodbye(hub, state, (room) => !inRoom2(socket, room));
7173
+ }
7174
+ }
7175
+ });
7176
+ }
7177
+
6309
7178
  // src/server/collections/changed.ts
6310
7179
  function mark(flush, s, topic) {
6311
7180
  flush.frames.set(topicRoom(s, topic), { s, topic, rev: flush.rev });
@@ -6640,17 +7509,17 @@ var TopicIndex = class {
6640
7509
  * absent), of the changed user's sockets (every user's when `userId` is
6641
7510
  * absent).
6642
7511
  */
6643
- matching(change) {
6644
- const key = change.id === void 0 ? void 0 : anchorKey(change.service, change.id);
6645
- const sockets = key === void 0 ? this.#byAnchorService.get(change.service) : this.#byAnchor.get(key);
7512
+ matching(change2) {
7513
+ const key = change2.id === void 0 ? void 0 : anchorKey(change2.service, change2.id);
7514
+ const sockets = key === void 0 ? this.#byAnchorService.get(change2.service) : this.#byAnchor.get(key);
6646
7515
  const found = /* @__PURE__ */ new Map();
6647
7516
  for (const socket of sockets?.keys() ?? []) {
6648
- if (change.userId !== void 0 && socket.data.principal?.userId !== change.userId) {
7517
+ if (change2.userId !== void 0 && socket.data.principal?.userId !== change2.userId) {
6649
7518
  continue;
6650
7519
  }
6651
7520
  const watches = this.entries(socket).filter(
6652
7521
  ({ anchors = [] }) => anchors.some(
6653
- (anchor) => key === void 0 ? serviceOf(anchor) === change.service : anchor === key
7522
+ (anchor) => key === void 0 ? serviceOf(anchor) === change2.service : anchor === key
6654
7523
  )
6655
7524
  );
6656
7525
  if (watches.length > 0) {
@@ -6773,7 +7642,7 @@ async function rewatchAll(hub, index, found) {
6773
7642
  }
6774
7643
  function createTopicRevocation(hub, index) {
6775
7644
  return Object.freeze({
6776
- changed: async (change) => await rewatchAll(hub, index, index.matching(change)),
7645
+ changed: async (change2) => await rewatchAll(hub, index, index.matching(change2)),
6777
7646
  regranted: async (sockets) => await rewatchAll(
6778
7647
  hub,
6779
7648
  index,
@@ -6944,7 +7813,7 @@ function createEntitySinks(hub) {
6944
7813
  }
6945
7814
 
6946
7815
  // src/server/emit/extension.ts
6947
- function isRecord7(value) {
7816
+ function isRecord8(value) {
6948
7817
  return typeof value === "object" && value !== null && !Array.isArray(value);
6949
7818
  }
6950
7819
  function isIdList(value) {
@@ -6954,7 +7823,7 @@ function isHeld(value) {
6954
7823
  return value === null || typeof value === "number" && Number.isFinite(value);
6955
7824
  }
6956
7825
  function readIds(frame, event, shape) {
6957
- if (!isRecord7(frame) || typeof frame.s !== "string" || frame.s === "" || !isIdList(frame.ids)) {
7826
+ if (!isRecord8(frame) || typeof frame.s !== "string" || frame.s === "" || !isIdList(frame.ids)) {
6958
7827
  throw unreadable(`A ${event} frame needs ${shape} with ids a list of row ids`);
6959
7828
  }
6960
7829
  if (frame.ids.length > MAX_SUBSCRIBE_IDS) {
@@ -7022,14 +7891,15 @@ function createLive(options) {
7022
7891
  const collections = createLiveCollections(hub);
7023
7892
  const topics = createTopics(collections.hub);
7024
7893
  const sinks = createEntitySinks(hub);
7025
- const realtime = createRealtime(hub);
7894
+ const peers = createPeerLinks(hub);
7895
+ const realtime = createRealtime(hub, peers);
7026
7896
  const revocation = createRevocation(hub, [
7027
7897
  collections.revocation,
7028
7898
  topics.revocation,
7029
7899
  realtime.revocation
7030
7900
  ]);
7031
7901
  const entities = entitySubscriptions(hub);
7032
- options.policies.onAccessChanged((change) => revocation.changed(change, false));
7902
+ options.policies.onAccessChanged((change2) => revocation.changed(change2, false));
7033
7903
  return Object.freeze({
7034
7904
  intake: sinks.intake,
7035
7905
  emit: sinks.emit,
@@ -7044,6 +7914,7 @@ function createLive(options) {
7044
7914
  },
7045
7915
  legacyExtension: realtime.legacyExtension,
7046
7916
  realtime,
7917
+ peers,
7047
7918
  revisions: hub.revisions,
7048
7919
  attach(io, probe, cluster, broadcasts) {
7049
7920
  hub.io = io;
@@ -7052,8 +7923,8 @@ function createLive(options) {
7052
7923
  hub.broadcasts = broadcasts;
7053
7924
  collections.listen();
7054
7925
  realtime.listen();
7055
- listenForChanges(hub, revocation, (change) => {
7056
- options.policies.forget(change);
7926
+ listenForChanges(hub, revocation, (change2) => {
7927
+ options.policies.forget(change2);
7057
7928
  });
7058
7929
  },
7059
7930
  regranted: (userId) => {
@@ -7104,8 +7975,42 @@ function checkIds(ids) {
7104
7975
  }
7105
7976
  return ids;
7106
7977
  }
7978
+ function checkLevel(level, member) {
7979
+ if (!isAccessLevel(level)) {
7980
+ throw new TypeError(`${member}: level must be an access level`);
7981
+ }
7982
+ }
7983
+ function contextAccess(state, api, principal) {
7984
+ const levelsFor = async (service, ids) => {
7985
+ if (principal !== null) {
7986
+ return await api.levelsFor(service, principal, ids);
7987
+ }
7988
+ bindingFor(state, service);
7989
+ checkIds(ids);
7990
+ return /* @__PURE__ */ new Map();
7991
+ };
7992
+ return Object.freeze({
7993
+ levelsFor,
7994
+ async can(service, id, level) {
7995
+ checkLevel(level, "ctx.access.can");
7996
+ if (typeof id !== "string" || id === "") {
7997
+ throw new TypeError("ctx.access.can: id must be a row id");
7998
+ }
7999
+ const levels = await levelsFor(service, [id]);
8000
+ return meetsLevel(levels.get(id), level);
8001
+ },
8002
+ async where(service, level) {
8003
+ if (principal !== null) {
8004
+ return await api.accessWhere(service, principal, level);
8005
+ }
8006
+ bindingFor(state, service);
8007
+ checkLevel(level, "ctx.access.where");
8008
+ return "none";
8009
+ }
8010
+ });
8011
+ }
7107
8012
  function createAccessApi(state) {
7108
- return {
8013
+ const api = {
7109
8014
  async levelsFor(service, principal, ids) {
7110
8015
  const binding = bindingFor(state, service);
7111
8016
  const who = checkPrincipal(principal);
@@ -7141,6 +8046,7 @@ function createAccessApi(state) {
7141
8046
  return await startCall(state).where(binding, who, level);
7142
8047
  }
7143
8048
  };
8049
+ return { ...api, forCaller: (principal) => contextAccess(state, api, principal) };
7144
8050
  }
7145
8051
 
7146
8052
  // src/server/access/bindings.ts
@@ -7277,17 +8183,17 @@ function membershipKeys(write2, read) {
7277
8183
  }
7278
8184
  var Changes = class {
7279
8185
  #changes = /* @__PURE__ */ new Map();
7280
- add(change) {
8186
+ add(change2) {
7281
8187
  this.#changes.set(
7282
- `${change.service}\0${change.id ?? ""}\0${change.userId ?? ""}`,
7283
- change
8188
+ `${change2.service}\0${change2.id ?? ""}\0${change2.userId ?? ""}`,
8189
+ change2
7284
8190
  );
7285
8191
  }
7286
8192
  get all() {
7287
8193
  return [...this.#changes.values()];
7288
8194
  }
7289
8195
  };
7290
- function apply(rule, write2, cache, changes) {
8196
+ function apply2(rule, write2, cache, changes) {
7291
8197
  const service = rule.binding.service.name;
7292
8198
  if (rule.kind === "rows") {
7293
8199
  if (sets(write2, rule.binding.columns)) {
@@ -7319,16 +8225,16 @@ function apply(rule, write2, cache, changes) {
7319
8225
  }
7320
8226
  async function notify(listeners, changes, logger) {
7321
8227
  const running = [];
7322
- for (const change of changes) {
8228
+ for (const change2 of changes) {
7323
8229
  for (const listener of listeners) {
7324
8230
  running.push(
7325
8231
  (async () => {
7326
- await listener(change);
8232
+ await listener(change2);
7327
8233
  })().catch((error) => {
7328
8234
  logger.error("An onAccessChanged listener failed", {
7329
8235
  category: "quickdraw.access",
7330
- service: change.service,
7331
- id: change.id,
8236
+ service: change2.service,
8237
+ id: change2.id,
7332
8238
  error: describeError(error)
7333
8239
  });
7334
8240
  })
@@ -7362,7 +8268,7 @@ function createChangeSink(bindings, cache, listeners, logger) {
7362
8268
  const changes = new Changes();
7363
8269
  for (const write2 of writes) {
7364
8270
  for (const rule of rules.get(modelKey(write2.model)) ?? []) {
7365
- apply(rule, write2, cache, changes);
8271
+ apply2(rule, write2, cache, changes);
7366
8272
  }
7367
8273
  }
7368
8274
  if (listeners.size > 0) {
@@ -7422,7 +8328,7 @@ function createPolicyEngine(options) {
7422
8328
  if (typeof listener !== "function") {
7423
8329
  throw new TypeError("dispatcher.access.onAccessChanged: listener must be a function");
7424
8330
  }
7425
- const entry = (change) => listener(change);
8331
+ const entry = (change2) => listener(change2);
7426
8332
  listeners.add(entry);
7427
8333
  return () => {
7428
8334
  listeners.delete(entry);
@@ -7848,7 +8754,7 @@ function checkClaims(check, principal, input, what) {
7848
8754
  throw new QuickdrawError("UNAUTHENTICATED", "Authentication required");
7849
8755
  }
7850
8756
  const allowed2 = check(principal.claims, input);
7851
- if (isThenable2(allowed2)) {
8757
+ if (isThenable(allowed2)) {
7852
8758
  Promise.resolve(allowed2).catch(() => void 0);
7853
8759
  throw new QuickdrawError(
7854
8760
  "INTERNAL",
@@ -7859,7 +8765,7 @@ function checkClaims(check, principal, input, what) {
7859
8765
  throw new QuickdrawError("FORBIDDEN", `${what} is not open to the caller's claims`);
7860
8766
  }
7861
8767
  }
7862
- function isThenable2(value) {
8768
+ function isThenable(value) {
7863
8769
  return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
7864
8770
  }
7865
8771
  function checkKindAndClaims(method, principal, input, what) {
@@ -7868,8 +8774,8 @@ function checkKindAndClaims(method, principal, input, what) {
7868
8774
  }
7869
8775
 
7870
8776
  // src/server/pipeline/run.ts
7871
- function stopped(signal) {
7872
- return { ok: false, error: abortError(signal) };
8777
+ function stopped(signal2) {
8778
+ return { ok: false, error: abortError(signal2) };
7873
8779
  }
7874
8780
  function createState(options, controller) {
7875
8781
  let resolveOutcome = () => void 0;
@@ -7902,8 +8808,8 @@ function createState(options, controller) {
7902
8808
  }
7903
8809
  return state;
7904
8810
  }
7905
- function runHandler(options, state, signal) {
7906
- const invoke = () => options.invoke(signal);
8811
+ function runHandler(options, state, signal2) {
8812
+ const invoke = () => options.invoke(signal2);
7907
8813
  let started;
7908
8814
  try {
7909
8815
  started = options.unit.run(options.quiet === true ? () => quietly(invoke) : invoke);
@@ -7928,26 +8834,26 @@ function runHandler(options, state, signal) {
7928
8834
  }
7929
8835
  );
7930
8836
  }
7931
- function joinRun(state, signal) {
7932
- if (signal?.aborted === true) {
7933
- return Promise.resolve(stopped(signal));
8837
+ function joinRun(state, signal2) {
8838
+ if (signal2?.aborted === true) {
8839
+ return Promise.resolve(stopped(signal2));
7934
8840
  }
7935
8841
  state.participants += 1;
7936
- if (signal === void 0) {
8842
+ if (signal2 === void 0) {
7937
8843
  return state.outcome;
7938
8844
  }
7939
8845
  return new Promise((resolve) => {
7940
8846
  const onAbort = () => {
7941
8847
  state.participants -= 1;
7942
- const left = stopped(signal);
8848
+ const left = stopped(signal2);
7943
8849
  resolve(left);
7944
8850
  if (state.participants === 0) {
7945
8851
  state.end(left, true);
7946
8852
  }
7947
8853
  };
7948
- signal.addEventListener("abort", onAbort, { once: true });
8854
+ signal2.addEventListener("abort", onAbort, { once: true });
7949
8855
  void state.outcome.then((result) => {
7950
- signal.removeEventListener("abort", onAbort);
8856
+ signal2.removeEventListener("abort", onAbort);
7951
8857
  resolve(result);
7952
8858
  });
7953
8859
  });
@@ -7966,7 +8872,7 @@ function startRun(options) {
7966
8872
  get handlerSettled() {
7967
8873
  return state.handlerSettled;
7968
8874
  },
7969
- join: (signal) => joinRun(state, signal)
8875
+ join: (signal2) => joinRun(state, signal2)
7970
8876
  };
7971
8877
  }
7972
8878
 
@@ -8220,8 +9126,8 @@ function execute(settings, shares, target2, call, input, ctx) {
8220
9126
  // The pipeline starts every call's time limit at admission, before this.
8221
9127
  timeLimit: call.expired ?? NEVER_ABORTED,
8222
9128
  unit,
8223
- invoke: (signal) => {
8224
- const runCtx = withSignal(ctx, signal);
9129
+ invoke: (signal2) => {
9130
+ const runCtx = withSignal(ctx, signal2);
8225
9131
  const handlerCtx = watch ? watchCallerReads(runCtx, (member) => warnSharedRead(settings, target2, member)) : runCtx;
8226
9132
  return method.handler({ input, ctx: handlerCtx, db: settings.db });
8227
9133
  },
@@ -8237,7 +9143,7 @@ function execute(settings, shares, target2, call, input, ctx) {
8237
9143
  }
8238
9144
 
8239
9145
  // src/server/pipeline/notModified.ts
8240
- function isVersion(value) {
9146
+ function isVersion2(value) {
8241
9147
  return typeof value === "string" || typeof value === "number" && Number.isFinite(value);
8242
9148
  }
8243
9149
 
@@ -8250,11 +9156,11 @@ function lookup2(registry, request) {
8250
9156
  const message = registry.services.has(request.service) ? `Unknown method "${request.method}" on service "${request.service}"` : `Unknown service "${request.service}"`;
8251
9157
  throw new QuickdrawError("NOT_FOUND", message);
8252
9158
  }
8253
- function admit(limiter, request, target2, signal) {
9159
+ function admit(limiter, request, target2, signal2) {
8254
9160
  if (target2.method.kind !== "query" || request.connectionId === void 0) {
8255
9161
  return Promise.resolve(void 0);
8256
9162
  }
8257
- return limiter.acquire(request.connectionId, signal);
9163
+ return limiter.acquire(request.connectionId, signal2);
8258
9164
  }
8259
9165
  function startTimeLimit(timeoutMs, cancel) {
8260
9166
  const controller = new AbortController();
@@ -8265,29 +9171,29 @@ function startTimeLimit(timeoutMs, cancel) {
8265
9171
  stop: () => clearTimeout(timer)
8266
9172
  };
8267
9173
  }
8268
- function untilStopped(work, signal) {
8269
- if (signal === void 0) {
9174
+ function untilStopped(work, signal2) {
9175
+ if (signal2 === void 0) {
8270
9176
  return Promise.resolve(work);
8271
9177
  }
8272
9178
  return new Promise((resolve, reject) => {
8273
- const onAbort = () => reject(abortError(signal));
8274
- if (signal.aborted) {
9179
+ const onAbort = () => reject(abortError(signal2));
9180
+ if (signal2.aborted) {
8275
9181
  onAbort();
8276
9182
  }
8277
- signal.addEventListener("abort", onAbort, { once: true });
9183
+ signal2.addEventListener("abort", onAbort, { once: true });
8278
9184
  work.then(
8279
9185
  (value) => {
8280
- signal.removeEventListener("abort", onAbort);
9186
+ signal2.removeEventListener("abort", onAbort);
8281
9187
  resolve(value);
8282
9188
  },
8283
9189
  (error) => {
8284
- signal.removeEventListener("abort", onAbort);
9190
+ signal2.removeEventListener("abort", onAbort);
8285
9191
  reject(error);
8286
9192
  }
8287
9193
  );
8288
9194
  });
8289
9195
  }
8290
- function contextFor(settings, request, requestId, target2, signal, dispatch) {
9196
+ function contextFor(settings, request, requestId, target2, signal2, dispatch) {
8291
9197
  const log = settings.logger.child({
8292
9198
  service: target2.service.name,
8293
9199
  method: target2.method.name,
@@ -8296,7 +9202,7 @@ function contextFor(settings, request, requestId, target2, signal, dispatch) {
8296
9202
  const { realtime } = settings.live;
8297
9203
  const fields = {
8298
9204
  principal: request.principal,
8299
- signal: signal ?? NEVER_ABORTED,
9205
+ signal: signal2 ?? NEVER_ABORTED,
8300
9206
  log,
8301
9207
  requestId,
8302
9208
  transport: request.transport,
@@ -8305,9 +9211,13 @@ function contextFor(settings, request, requestId, target2, signal, dispatch) {
8305
9211
  touch: settings.touch,
8306
9212
  rooms: realtime.roomsFor(request.transport, request.connectionId, target2.method.share),
8307
9213
  presence: realtime.presence,
8308
- // `ctx.services` calls the app's services as this principal through this
8309
- // dispatcher; their units of work join this call's, so their writes flush with it.
9214
+ // `ctx.services` calls the app's services as this principal, with this
9215
+ // call's `ctx.mcp`, through this dispatcher; their units of work join
9216
+ // this call's, so their writes flush with it.
8310
9217
  dispatch,
9218
+ // `ctx.access`: the policies' levels and filters for this principal, with
9219
+ // the grants the call runs with.
9220
+ access: settings.policies,
8311
9221
  kit: {
8312
9222
  service: target2.service,
8313
9223
  services: settings.registry.services,
@@ -8327,7 +9237,7 @@ async function currentVersion(settings, target2, input, ctx) {
8327
9237
  return void 0;
8328
9238
  }
8329
9239
  const version = method.version === void 0 ? await settings.versions?.versionOf({ service, method, input, ctx }) : await method.version(input, ctx);
8330
- return isVersion(version) ? version : void 0;
9240
+ return isVersion2(version) ? version : void 0;
8331
9241
  }
8332
9242
  function versionOfResult(settings, target2, call, result) {
8333
9243
  const { service, method } = target2;
@@ -9013,7 +9923,8 @@ function createRateLimiter(options = {}) {
9013
9923
  windowMs = 6e4,
9014
9924
  maxRequests = DEFAULT_MAX_REQUESTS,
9015
9925
  excludeEvents = [],
9016
- excludePrefixes = []
9926
+ excludePrefixes = [],
9927
+ skip = () => false
9017
9928
  } = options;
9018
9929
  const entries = /* @__PURE__ */ new Map();
9019
9930
  const cleanupInterval = setInterval(() => {
@@ -9067,7 +9978,8 @@ function createRateLimiter(options = {}) {
9067
9978
  windowMs,
9068
9979
  maxRequests,
9069
9980
  excludeEvents,
9070
- excludePrefixes
9981
+ excludePrefixes,
9982
+ skip
9071
9983
  }
9072
9984
  };
9073
9985
  }
@@ -9086,7 +9998,7 @@ function applyRateLimitMiddleware(io, rateLimiter, options = {}) {
9086
9998
  error: "Rate limit exceeded",
9087
9999
  code: 429
9088
10000
  }));
9089
- const { excludeEvents, excludePrefixes } = rateLimiter.options;
10001
+ const { excludeEvents, excludePrefixes, skip } = rateLimiter.options;
9090
10002
  io.on("connection", (socket) => {
9091
10003
  socket.use(([eventName, ...args], next) => {
9092
10004
  if (typeof eventName !== "string") {
@@ -9105,6 +10017,10 @@ function applyRateLimitMiddleware(io, rateLimiter, options = {}) {
9105
10017
  next();
9106
10018
  return;
9107
10019
  }
10020
+ if (skip(eventName, args)) {
10021
+ next();
10022
+ return;
10023
+ }
9108
10024
  const key = keyGenerator(socket, eventName);
9109
10025
  if (rateLimiter.check(key)) {
9110
10026
  next();
@@ -9249,8 +10165,8 @@ function startTrackedModelsCheck(registry, options, logger) {
9249
10165
 
9250
10166
  // src/server/lifecycle.ts
9251
10167
  function watchSignals(close, logger) {
9252
- const onSignal = (signal) => {
9253
- logger.info(`Received ${signal}; closing the quickdraw server`, {
10168
+ const onSignal = (signal2) => {
10169
+ logger.info(`Received ${signal2}; closing the quickdraw server`, {
9254
10170
  category: "quickdraw.server"
9255
10171
  });
9256
10172
  close().catch((error) => {
@@ -9369,10 +10285,32 @@ import { Server } from "socket.io";
9369
10285
  function legacyFailure(code, error) {
9370
10286
  return { success: false, error, code: httpStatus(code) };
9371
10287
  }
10288
+ var LISTED_ISSUES = 10;
10289
+ function issueText(issue) {
10290
+ if (!isRecord(issue) || typeof issue.message !== "string" || !Array.isArray(issue.path)) {
10291
+ return void 0;
10292
+ }
10293
+ const path = issue.path;
10294
+ if (!path.every((segment) => typeof segment === "string" || typeof segment === "number")) {
10295
+ return void 0;
10296
+ }
10297
+ return path.length === 0 ? issue.message : `${path.join(".")}: ${issue.message}`;
10298
+ }
10299
+ function validationMessage(message, data) {
10300
+ const issues = isRecord(data) && Array.isArray(data.issues) ? data.issues : [];
10301
+ const texts = issues.map(issueText).filter((text) => text !== void 0);
10302
+ if (texts.length === 0) {
10303
+ return message;
10304
+ }
10305
+ const more = texts.length - LISTED_ISSUES;
10306
+ const listed = texts.slice(0, LISTED_ISSUES).join("; ");
10307
+ return `${message}: ${listed}${more > 0 ? `; and ${String(more)} more` : ""}`;
10308
+ }
9372
10309
  function toLegacyReply(result) {
9373
10310
  if (!result.ok) {
9374
10311
  const wire = toWire(result.error);
9375
- return legacyFailure(wire.code, wire.message);
10312
+ const message = wire.code === "VALIDATION" ? validationMessage(wire.message, wire.data) : wire.message;
10313
+ return legacyFailure(wire.code, message);
9376
10314
  }
9377
10315
  return { success: true, data: result.notModified === true ? void 0 : result.data };
9378
10316
  }
@@ -9515,6 +10453,7 @@ var MISMATCH = Object.freeze({
9515
10453
  expected: PROTOCOL_VERSION
9516
10454
  });
9517
10455
  var UNAUTHENTICATED = Object.freeze({ code: "UNAUTHENTICATED" });
10456
+ var UNAVAILABLE = Object.freeze({ code: "INTERNAL" });
9518
10457
  function protocolMiddleware(legacyWire, context) {
9519
10458
  return (socket, next) => {
9520
10459
  const auth = socket.handshake.auth;
@@ -9570,13 +10509,13 @@ function authMiddleware(resolvePrincipal, context) {
9570
10509
  next();
9571
10510
  },
9572
10511
  (error) => {
9573
- const level = isRefusal(error) ? "debug" : "error";
9574
- context.logger[level]("Socket authentication failed", {
10512
+ const refused = isRefusal(error) || isForbiddenCredential(error);
10513
+ context.logger[refused ? "debug" : "error"]("Socket authentication failed", {
9575
10514
  category: "quickdraw.socket",
9576
10515
  socketId: socket.id,
9577
10516
  error: describeError(error)
9578
10517
  });
9579
- next(refusal("Authentication failed", UNAUTHENTICATED));
10518
+ next(refusal("Authentication failed", refused ? UNAUTHENTICATED : UNAVAILABLE));
9580
10519
  }
9581
10520
  );
9582
10521
  };
@@ -9592,8 +10531,13 @@ var UNLIMITED_EVENTS = [
9592
10531
  CLIENT_EVENTS.watch,
9593
10532
  CLIENT_EVENTS.unwatch,
9594
10533
  CLIENT_EVENTS.streamSub,
9595
- CLIENT_EVENTS.streamUnsub
10534
+ CLIENT_EVENTS.streamUnsub,
10535
+ CLIENT_EVENTS.relay
9596
10536
  ];
10537
+ function isPeerBye(eventName, args) {
10538
+ const frame = args[0];
10539
+ return eventName === CLIENT_EVENTS.peer && frame?.sig?.bye === true;
10540
+ }
9597
10541
  function rateLimited(retryAfterMs) {
9598
10542
  return {
9599
10543
  ok: false,
@@ -9616,9 +10560,11 @@ function noticeForLegacyClients(limiter, options) {
9616
10560
  };
9617
10561
  }
9618
10562
  function applySocketRateLimit(io, options, context) {
10563
+ const { skip } = options;
9619
10564
  const limiter = createRateLimiter({
9620
10565
  ...options,
9621
- excludeEvents: [...UNLIMITED_EVENTS, ...options.excludeEvents ?? []]
10566
+ excludeEvents: [...UNLIMITED_EVENTS, ...options.excludeEvents ?? []],
10567
+ skip: (eventName, args) => isPeerBye(eventName, args) || skip?.(eventName, args) === true
9622
10568
  });
9623
10569
  const notice = options.onRateLimitExceeded ?? noticeForLegacyClients(limiter, options);
9624
10570
  applyRateLimitMiddleware(io, limiter, {
@@ -9637,7 +10583,7 @@ var RESYNC_WITHIN_MS = 2e3;
9637
10583
  function isEmitter(value) {
9638
10584
  return typeof value === "object" && value !== null && typeof value.on === "function";
9639
10585
  }
9640
- function isThenable3(value) {
10586
+ function isThenable2(value) {
9641
10587
  return typeof value === "object" && value !== null && typeof value.then === "function";
9642
10588
  }
9643
10589
  var watched = /* @__PURE__ */ new WeakSet();
@@ -9652,14 +10598,14 @@ function resyncOnReady(io, subClient, logger) {
9652
10598
  }
9653
10599
  function guardPublishes(pubClient, logger) {
9654
10600
  const target2 = pubClient;
9655
- const publish = target2.publish;
9656
- if (typeof publish !== "function") {
10601
+ const publish2 = target2.publish;
10602
+ if (typeof publish2 !== "function") {
9657
10603
  return;
9658
10604
  }
9659
10605
  let reported = false;
9660
10606
  target2.publish = function guardedPublish(...args) {
9661
- const sent = Reflect.apply(publish, this, args);
9662
- if (isThenable3(sent)) {
10607
+ const sent = Reflect.apply(publish2, this, args);
10608
+ if (isThenable2(sent)) {
9663
10609
  sent.then(void 0, (error) => {
9664
10610
  if (!reported) {
9665
10611
  reported = true;
@@ -10019,6 +10965,20 @@ function helloFor(hello, principal) {
10019
10965
  }
10020
10966
 
10021
10967
  // src/server/transports/socketServer.ts
10968
+ var PEER_LIMITS = Object.freeze({
10969
+ maxLinks: MAX_PEER_LINKS,
10970
+ maxMessageBytes: MAX_PEER_MESSAGE_BYTES,
10971
+ relayRate: PEER_RELAY_RATE,
10972
+ relayBurst: PEER_RELAY_BURST,
10973
+ relayBytesPerSecond: PEER_RELAY_BYTES_PER_SECOND,
10974
+ relayBytesBurst: PEER_RELAY_BYTES_BURST
10975
+ });
10976
+ function featuresOf(settings) {
10977
+ return Object.freeze([
10978
+ ...settings.binary ? ["binary"] : [],
10979
+ ...settings.peers === true ? ["peer"] : []
10980
+ ]);
10981
+ }
10022
10982
  function helloFrame(settings) {
10023
10983
  const { limits } = settings.dispatcher;
10024
10984
  return Object.freeze({
@@ -10037,9 +10997,10 @@ function helloFrame(settings) {
10037
10997
  maxEntityIds: MAX_ENTITY_IDS_PER_SOCKET,
10038
10998
  maxScopes: MAX_SCOPES_PER_SOCKET,
10039
10999
  maxTopics: MAX_TOPICS_PER_SOCKET
10040
- })
11000
+ }),
11001
+ ...settings.peers === true ? { peers: PEER_LIMITS } : {}
10041
11002
  }),
10042
- features: Object.freeze(settings.binary ? ["binary"] : [])
11003
+ features: featuresOf(settings)
10043
11004
  });
10044
11005
  }
10045
11006
  function createSocketServer(httpServer, settings) {
@@ -10126,10 +11087,15 @@ function checkOptions(options) {
10126
11087
  if (timeout !== void 0 && !(Number.isSafeInteger(timeout) && timeout >= 0)) {
10127
11088
  throw new TypeError("createServer: shutdownTimeoutMs must be a whole number of milliseconds");
10128
11089
  }
10129
- if (options.onRoomLeave !== void 0 && typeof options.onRoomLeave !== "function") {
10130
- throw new TypeError(
10131
- "createServer: onRoomLeave must be a function of the leave and a run context"
10132
- );
11090
+ if (options.peers !== void 0 && typeof options.peers !== "boolean") {
11091
+ throw new TypeError("createServer: peers must be true or false");
11092
+ }
11093
+ for (const hook of ["onRoomLeave", "onHostLeave"]) {
11094
+ if (options[hook] !== void 0 && typeof options[hook] !== "function") {
11095
+ throw new TypeError(
11096
+ `createServer: ${hook} must be a function of the leave and a run context`
11097
+ );
11098
+ }
10133
11099
  }
10134
11100
  const socket = options.socket;
10135
11101
  if (socket?.connectionStateRecovery) {
@@ -10138,18 +11104,40 @@ function checkOptions(options) {
10138
11104
  );
10139
11105
  }
10140
11106
  }
10141
- function listenForLeaves(dispatcher, handler) {
11107
+ function checkHostMode(services, peers) {
11108
+ for (const service of peers ? [] : services) {
11109
+ const { channels, streams } = service.contract;
11110
+ const hosted = [
11111
+ ...Object.entries(channels).map(([name, def]) => [`channel "${name}"`, def]),
11112
+ ...Object.entries(streams).map(([name, def]) => [`stream "${name}"`, def])
11113
+ ].find(([, def]) => def.serve === "host");
11114
+ if (hosted !== void 0) {
11115
+ throw new TypeError(
11116
+ `createServer: ${service.name}'s ${hosted[0]} is served by the room's host (serve: "host"), which runs over peer links; create the server with peers: true`
11117
+ );
11118
+ }
11119
+ }
11120
+ }
11121
+ function listenForLeaves(dispatcher, handlers) {
10142
11122
  const services = [...dispatcher.registry.services.values()];
10143
- const hooks = services.flatMap(
11123
+ const rooms = services.flatMap(
10144
11124
  (service) => service.onRoomLeave === void 0 ? [] : [{ owner: service.name, handler: service.onRoomLeave }]
10145
11125
  );
10146
- if (handler !== void 0) {
10147
- hooks.push({ owner: "createServer", handler });
10148
- }
10149
- liveOf(dispatcher)?.realtime.onRoomLeave(
10150
- hooks,
10151
- async (fn) => await dispatcher.run(fn, { detached: true })
11126
+ const hosts = services.flatMap(
11127
+ (service) => service.onHostLeave === void 0 ? [] : [{ owner: service.name, handler: service.onHostLeave }]
10152
11128
  );
11129
+ if (handlers.room !== void 0) {
11130
+ rooms.push({ owner: "createServer", handler: handlers.room });
11131
+ }
11132
+ if (handlers.host !== void 0) {
11133
+ hosts.push({ owner: "createServer", handler: handlers.host });
11134
+ }
11135
+ const run2 = async (fn) => {
11136
+ await dispatcher.run(fn, { detached: true });
11137
+ };
11138
+ const realtime = liveOf(dispatcher)?.realtime;
11139
+ realtime?.onRoomLeave(rooms, run2);
11140
+ realtime?.onHostLeave(hosts, run2);
10153
11141
  }
10154
11142
  function mountRouter(options, base) {
10155
11143
  if (options.http === false) {
@@ -10160,6 +11148,25 @@ function mountRouter(options, base) {
10160
11148
  options.app?.use(router);
10161
11149
  return router;
10162
11150
  }
11151
+ function serveSockets(options, created, httpServer, base) {
11152
+ const live = liveOf(created);
11153
+ const peers = options.peers === true ? live?.peers : void 0;
11154
+ const sockets = createSocketServer(httpServer, {
11155
+ ...base,
11156
+ loadServiceAccess: options.auth?.loadServiceAccess,
11157
+ binary: options.binary === true,
11158
+ cors: options.cors,
11159
+ socket: options.socket,
11160
+ rateLimit: options.rateLimit ?? {},
11161
+ extensions: peers === void 0 ? [] : [peers.extension],
11162
+ live,
11163
+ loops: loopsOf(created),
11164
+ cluster: options.cluster,
11165
+ peers: peers !== void 0
11166
+ });
11167
+ peers?.listen();
11168
+ return sockets;
11169
+ }
10163
11170
  function notFound2(_req, res) {
10164
11171
  res.statusCode = 404;
10165
11172
  res.end();
@@ -10191,6 +11198,7 @@ function withGrants(options, grants, grantsSource) {
10191
11198
  }
10192
11199
  function createServer(options) {
10193
11200
  checkOptions(options);
11201
+ checkHostMode(options.services, options.peers === true);
10194
11202
  const legacyWire = resolveLegacyWire(options.legacyWire, options.services);
10195
11203
  const logger = options.logger ?? consoleLogger;
10196
11204
  warnOnAuth(options.services, options.auth, logger);
@@ -10203,24 +11211,15 @@ function createServer(options) {
10203
11211
  const calls = trackCalls(created);
10204
11212
  const { dispatcher } = calls;
10205
11213
  startStartupChecks(dispatcher, options, logger);
10206
- listenForLeaves(created, options.onRoomLeave);
11214
+ listenForLeaves(created, { room: options.onRoomLeave, host: options.onHostLeave });
10207
11215
  const resolvePrincipal = createPrincipalResolver(options.auth);
10208
11216
  const router = mountRouter(options, { call: dispatcher.call, resolvePrincipal, logger });
10209
11217
  const httpServer = options.httpServer ?? createHttpServer(options.app ?? router ?? notFound2);
10210
- const sockets = createSocketServer(httpServer, {
11218
+ const sockets = serveSockets(options, created, httpServer, {
10211
11219
  dispatcher,
10212
11220
  logger,
10213
11221
  resolvePrincipal,
10214
- loadServiceAccess: options.auth?.loadServiceAccess,
10215
- binary: options.binary === true,
10216
- legacyWire,
10217
- cors: options.cors,
10218
- socket: options.socket,
10219
- rateLimit: options.rateLimit ?? {},
10220
- extensions: [],
10221
- live: liveOf(created),
10222
- loops: loopsOf(created),
10223
- cluster: options.cluster
11222
+ legacyWire
10224
11223
  });
10225
11224
  refresh = (userId) => sockets.refresh(userId);
10226
11225
  const load = options.auth?.loadServiceAccess;
@@ -10298,6 +11297,7 @@ export {
10298
11297
  CHANNEL_ABUSE_MULTIPLIER,
10299
11298
  PRESENCE_MAX_LAST_SEEN,
10300
11299
  MAX_APP_ROOMS,
11300
+ seedRoomHosts,
10301
11301
  STREAM_MAX_SCOPES,
10302
11302
  MAX_STREAMS_PER_SOCKET,
10303
11303
  createBasicAccessEngine,
@@ -10319,4 +11319,4 @@ export {
10319
11319
  watchAdapterClients,
10320
11320
  createServer
10321
11321
  };
10322
- //# sourceMappingURL=chunk-6K5BY47X.js.map
11322
+ //# sourceMappingURL=chunk-JVTONQFI.js.map