@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.
- package/README.md +793 -57
- package/dist/{admin-DWWrT1h6.d.ts → admin-CS7JB_UH.d.ts} +2 -2
- package/dist/{adminSchemas-D4bFKsrb.d.ts → adminSchemas-29cGgrg5.d.ts} +1 -1
- package/dist/{auth-DXiSJZaf.d.ts → auth-DtdYjrZ9.d.ts} +11 -7
- package/dist/{caller-BT-Mv4bD.d.ts → caller-BTwGl7vm.d.ts} +183 -15
- package/dist/chunk-2FDSKLBO.js +82 -0
- package/dist/chunk-2FDSKLBO.js.map +1 -0
- package/dist/{chunk-GSIPUNJL.js → chunk-64I6MYBC.js} +139 -792
- package/dist/chunk-64I6MYBC.js.map +1 -0
- package/dist/chunk-7473HK24.js +17 -0
- package/dist/chunk-7473HK24.js.map +1 -0
- package/dist/chunk-7Y5ROPXL.js +33 -0
- package/dist/chunk-7Y5ROPXL.js.map +1 -0
- package/dist/chunk-A2RBQD66.js +7 -0
- package/dist/chunk-A2RBQD66.js.map +1 -0
- package/dist/chunk-ASAMUGHG.js +651 -0
- package/dist/chunk-ASAMUGHG.js.map +1 -0
- package/dist/{chunk-B2P7AZ25.js → chunk-CYE4UBOB.js} +6 -4
- package/dist/chunk-CYE4UBOB.js.map +1 -0
- package/dist/chunk-DNP65TD5.js +832 -0
- package/dist/chunk-DNP65TD5.js.map +1 -0
- package/dist/{chunk-5CGO426C.js → chunk-GYLWEJHW.js} +5 -1
- package/dist/chunk-GYLWEJHW.js.map +1 -0
- package/dist/{chunk-AONAFOYU.js → chunk-I3JCFS7H.js} +25 -13
- package/dist/chunk-I3JCFS7H.js.map +1 -0
- package/dist/{chunk-X5CWPX7H.js → chunk-IIQ3WMJK.js} +13 -2
- package/dist/chunk-IIQ3WMJK.js.map +1 -0
- package/dist/{chunk-6K5BY47X.js → chunk-JVTONQFI.js} +1277 -277
- package/dist/chunk-JVTONQFI.js.map +1 -0
- package/dist/{chunk-RL4VKLBX.js → chunk-KKQANRLG.js} +2 -2
- package/dist/{chunk-RL4VKLBX.js.map → chunk-KKQANRLG.js.map} +1 -1
- package/dist/{chunk-GKH3LJ7N.js → chunk-MPQAT55F.js} +38 -205
- package/dist/chunk-MPQAT55F.js.map +1 -0
- package/dist/chunk-NX2V6SIL.js +202 -0
- package/dist/chunk-NX2V6SIL.js.map +1 -0
- package/dist/{chunk-7QCUX4TK.js → chunk-NZHA4TZD.js} +72 -175
- package/dist/chunk-NZHA4TZD.js.map +1 -0
- package/dist/{chunk-MBH6IDSM.js → chunk-OCZJ4EYV.js} +40 -17
- package/dist/chunk-OCZJ4EYV.js.map +1 -0
- package/dist/{chunk-RJ5I2VO3.js → chunk-PHCURHC7.js} +6 -6
- package/dist/chunk-PHCURHC7.js.map +1 -0
- package/dist/{chunk-4YO5H5ON.js → chunk-Q4OGQM5I.js} +4 -10
- package/dist/chunk-Q4OGQM5I.js.map +1 -0
- package/dist/chunk-QSU47XEX.js +13 -0
- package/dist/chunk-QSU47XEX.js.map +1 -0
- package/dist/{chunk-LCC7HNFP.js → chunk-RZXJTDH5.js} +2 -2
- package/dist/{chunk-Z2LZ7IZ7.js → chunk-SN3OZVEQ.js} +5 -31
- package/dist/chunk-SN3OZVEQ.js.map +1 -0
- package/dist/{chunk-Z5ILULF2.js → chunk-SQ5VUG77.js} +4 -3
- package/dist/{chunk-Z5ILULF2.js.map → chunk-SQ5VUG77.js.map} +1 -1
- package/dist/chunk-STH5DLQP.js +1192 -0
- package/dist/chunk-STH5DLQP.js.map +1 -0
- package/dist/{chunk-NMXYENH4.js → chunk-VZFJTKIW.js} +6 -6
- package/dist/{chunk-753VJIUP.js → chunk-X4ZU4QH5.js} +43 -9
- package/dist/chunk-X4ZU4QH5.js.map +1 -0
- package/dist/chunk-XD7SRDUY.js +188 -0
- package/dist/chunk-XD7SRDUY.js.map +1 -0
- package/dist/chunk-Z7I6SONO.js +825 -0
- package/dist/chunk-Z7I6SONO.js.map +1 -0
- package/dist/cli/quickdraw-docs.js +131 -113
- package/dist/cli/quickdraw-docs.js.map +1 -1
- package/dist/client/core.d.ts +387 -0
- package/dist/client/core.js +109 -0
- package/dist/client/core.js.map +1 -0
- package/dist/client/index.d.ts +30 -193
- package/dist/client/index.js +56 -217
- package/dist/client/index.js.map +1 -1
- package/dist/{clientTypes-hWcPUVS9.d.ts → clientTypes-CUUNJwWs.d.ts} +3 -3
- package/dist/{connection-CgijdGpT.d.ts → connection-2EeQ_Et0.d.ts} +16 -9
- package/dist/{createServer-Cp-Wk500.d.ts → createServer-Bx5HOcYK.d.ts} +37 -9
- package/dist/{createTestApp-BB_PkqQ7.d.ts → createTestApp-DAMANT79.d.ts} +36 -9
- package/dist/{envelope-DzWyIXjj.d.ts → envelope-BMBaxppV.d.ts} +200 -8
- package/dist/{http-DotQ5bj8.d.ts → http-YzLAar3w.d.ts} +15 -50
- package/dist/index.d.ts +7 -6
- package/dist/index.js +61 -28
- package/dist/index.js.map +1 -1
- package/dist/{infer-BmoZsL9a.d.ts → infer-Br62cnyW.d.ts} +39 -1
- package/dist/{metrics-DW80Exve.d.ts → metrics-CTVpw0gu.d.ts} +2 -2
- package/dist/parser.js +2 -1
- package/dist/prisma/index.d.ts +13 -6
- package/dist/prisma/index.js +155 -9
- package/dist/prisma/index.js.map +1 -1
- package/dist/remote-N7OUAGOW.js +15 -0
- package/dist/remote-N7OUAGOW.js.map +1 -0
- package/dist/server/auth/index.d.ts +3 -3
- package/dist/server/auth/index.js +5 -3
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/index.d.ts +140 -36
- package/dist/server/index.js +162 -55
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/index.d.ts +34 -18
- package/dist/server/mcp/index.js +29 -12
- package/dist/server/mcp/index.js.map +1 -1
- package/dist/server/otel.d.ts +3 -3
- package/dist/{sharing-UDLn1049.d.ts → sharing-BrsmlDv_.d.ts} +3 -3
- package/dist/testing/client.d.ts +13 -13
- package/dist/testing/client.js +17 -10
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts +49 -12
- package/dist/testing/index.js +102 -19
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock.d.ts +5 -5
- package/dist/testing/mock.js +15 -9
- package/dist/tracked-BR5b2O-U.d.ts +43 -0
- package/dist/{types-CYliTP65.d.ts → types-CLRCQGnO.d.ts} +4 -3
- package/dist/types-tMagHbjy.d.ts +151 -0
- package/dist/utils/index.d.ts +16 -4
- package/dist/utils/index.js +6 -3
- package/package.json +5 -1
- package/dist/chunk-4YO5H5ON.js.map +0 -1
- package/dist/chunk-5CGO426C.js.map +0 -1
- package/dist/chunk-6K5BY47X.js.map +0 -1
- package/dist/chunk-753VJIUP.js.map +0 -1
- package/dist/chunk-7QCUX4TK.js.map +0 -1
- package/dist/chunk-AONAFOYU.js.map +0 -1
- package/dist/chunk-B2P7AZ25.js.map +0 -1
- package/dist/chunk-GKH3LJ7N.js.map +0 -1
- package/dist/chunk-GSIPUNJL.js.map +0 -1
- package/dist/chunk-MBH6IDSM.js.map +0 -1
- package/dist/chunk-RJ5I2VO3.js.map +0 -1
- package/dist/chunk-X5CWPX7H.js.map +0 -1
- package/dist/chunk-Z2LZ7IZ7.js.map +0 -1
- /package/dist/{chunk-LCC7HNFP.js.map → chunk-RZXJTDH5.js.map} +0 -0
- /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-
|
|
15
|
+
} from "./chunk-SQ5VUG77.js";
|
|
16
16
|
import {
|
|
17
17
|
isCallEnvelope,
|
|
18
18
|
isCancel
|
|
19
|
-
} from "./chunk-
|
|
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-
|
|
55
|
+
} from "./chunk-OCZJ4EYV.js";
|
|
56
56
|
import {
|
|
57
57
|
createRecorder,
|
|
58
58
|
describeError,
|
|
59
59
|
failureLevel
|
|
60
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
139
|
+
} from "./chunk-X4ZU4QH5.js";
|
|
108
140
|
import {
|
|
109
|
-
|
|
110
|
-
|
|
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-
|
|
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
|
|
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 &&
|
|
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 (!
|
|
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 (
|
|
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([
|
|
370
|
-
|
|
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
|
|
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 (!
|
|
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 {
|
|
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 && !
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
1429
|
-
const key =
|
|
1430
|
-
const sockets = key === void 0 ? this.#byService.get(
|
|
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 (
|
|
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) ===
|
|
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
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
-
|
|
1543
|
+
bucket2.tokens -= 1;
|
|
1490
1544
|
return true;
|
|
1491
1545
|
}
|
|
1492
|
-
function abusive(
|
|
1493
|
-
if (now -
|
|
1494
|
-
|
|
1495
|
-
|
|
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
|
-
|
|
1498
|
-
return
|
|
1551
|
+
bucket2.dropped += 1;
|
|
1552
|
+
return bucket2.dropped > channel.ratePerSecond * CHANNEL_ABUSE_MULTIPLIER;
|
|
1499
1553
|
}
|
|
1500
1554
|
function bucketOf(state, channel, now) {
|
|
1501
|
-
let
|
|
1502
|
-
if (
|
|
1503
|
-
|
|
1504
|
-
state.buckets.set(channel,
|
|
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
|
|
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,
|
|
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:
|
|
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
|
|
1614
|
-
if (!take(
|
|
1615
|
-
if (abusive(
|
|
1616
|
-
flooded(deps, state, channel,
|
|
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,
|
|
2030
|
-
const running =
|
|
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,
|
|
2096
|
+
return (key, ask2) => {
|
|
2042
2097
|
const slot = slots.get(key);
|
|
2043
2098
|
if (slot === void 0) {
|
|
2044
|
-
return start(key,
|
|
2099
|
+
return start(key, ask2);
|
|
2045
2100
|
}
|
|
2046
2101
|
slot.queued ??= slot.running.then(
|
|
2047
|
-
() => start(key,
|
|
2048
|
-
() => start(key,
|
|
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
|
|
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}`, () =>
|
|
2243
|
+
users: (room) => coalesced(`r\0${room}`, () => ask2({ room }, idsOf, () => readUsers(hub, room))),
|
|
2143
2244
|
online: (userId) => coalesced(
|
|
2144
2245
|
`u\0${userId}`,
|
|
2145
|
-
() =>
|
|
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
|
|
2827
|
-
return { stale, fresh:
|
|
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 (
|
|
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,
|
|
2836
|
-
for (const id of
|
|
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(
|
|
3104
|
-
const key =
|
|
3105
|
-
const sockets = key === void 0 ? this.#byService.get(
|
|
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 (
|
|
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) ===
|
|
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,
|
|
3228
|
-
const { service, id } =
|
|
3337
|
+
function withRowState(hub, change2) {
|
|
3338
|
+
const { service, id } = change2;
|
|
3229
3339
|
if (id === void 0) {
|
|
3230
|
-
return
|
|
3340
|
+
return change2;
|
|
3231
3341
|
}
|
|
3232
3342
|
return {
|
|
3233
|
-
...
|
|
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,
|
|
3239
|
-
await hub.broadcasts?.broadcast(ACCESS_CHANGED_EVENT,
|
|
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(
|
|
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,
|
|
3246
|
-
const trustLog = !remote ||
|
|
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(
|
|
3250
|
-
...hooks.map(async (hook) => await hook.changed(
|
|
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,
|
|
3292
|
-
if (
|
|
3293
|
-
hub.changeLog.record(
|
|
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,
|
|
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
|
|
3302
|
-
if (
|
|
3411
|
+
const change2 = readChange(broadcast);
|
|
3412
|
+
if (change2 === void 0) {
|
|
3303
3413
|
answer2(false);
|
|
3304
3414
|
return;
|
|
3305
3415
|
}
|
|
3306
|
-
recordRowState(hub,
|
|
3307
|
-
|
|
3308
|
-
revocation.changed(
|
|
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:
|
|
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 (
|
|
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(
|
|
3881
|
-
const key =
|
|
3882
|
-
const sockets = key === void 0 ? this.#byService.get(
|
|
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 (
|
|
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) ===
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
5099
|
+
signal2?.addEventListener("abort", onAbort, { once: true });
|
|
4977
5100
|
lane.queue.push(waiter);
|
|
4978
5101
|
});
|
|
4979
5102
|
}
|
|
4980
5103
|
return {
|
|
4981
|
-
acquire(connectionId,
|
|
4982
|
-
if (
|
|
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,
|
|
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
|
|
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 (!
|
|
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.
|
|
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,
|
|
5612
|
-
const own =
|
|
5613
|
-
return hub.collections.scopes.scopes(collection.service.name, collection.name).filter((scope) =>
|
|
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,
|
|
5618
|
-
if (
|
|
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(
|
|
5747
|
+
if (!policyServices(hub, collection.service).has(change2.service)) {
|
|
5625
5748
|
continue;
|
|
5626
5749
|
}
|
|
5627
|
-
raiseFloor(hub, collection,
|
|
5750
|
+
raiseFloor(hub, collection, change2.userId);
|
|
5628
5751
|
if (io !== void 0) {
|
|
5629
|
-
work.push(...recheckScopes(hub, io, collection,
|
|
5752
|
+
work.push(...recheckScopes(hub, io, collection, change2));
|
|
5630
5753
|
}
|
|
5631
5754
|
}
|
|
5632
5755
|
await Promise.all(work);
|
|
5633
5756
|
}
|
|
5634
|
-
|
|
5635
|
-
for (const collection of
|
|
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 (
|
|
5848
|
+
changed: async (change2) => {
|
|
5721
5849
|
await settleAll([
|
|
5722
|
-
reauthorizeAll2(hub, hub.collections.scopes.matching(
|
|
5723
|
-
recheckSelf(hub,
|
|
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
|
-
|
|
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
|
|
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 =
|
|
5770
|
-
if (!
|
|
5771
|
-
const name =
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
6271
|
-
|
|
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(
|
|
6644
|
-
const key =
|
|
6645
|
-
const sockets = key === void 0 ? this.#byAnchorService.get(
|
|
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 (
|
|
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) ===
|
|
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 (
|
|
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
|
|
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 (!
|
|
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
|
|
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((
|
|
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, (
|
|
7056
|
-
options.policies.forget(
|
|
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
|
-
|
|
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(
|
|
8186
|
+
add(change2) {
|
|
7281
8187
|
this.#changes.set(
|
|
7282
|
-
`${
|
|
7283
|
-
|
|
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
|
|
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
|
|
8228
|
+
for (const change2 of changes) {
|
|
7323
8229
|
for (const listener of listeners) {
|
|
7324
8230
|
running.push(
|
|
7325
8231
|
(async () => {
|
|
7326
|
-
await listener(
|
|
8232
|
+
await listener(change2);
|
|
7327
8233
|
})().catch((error) => {
|
|
7328
8234
|
logger.error("An onAccessChanged listener failed", {
|
|
7329
8235
|
category: "quickdraw.access",
|
|
7330
|
-
service:
|
|
7331
|
-
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
|
-
|
|
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 = (
|
|
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 (
|
|
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
|
|
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(
|
|
7872
|
-
return { ok: false, error: abortError(
|
|
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,
|
|
7906
|
-
const invoke = () => options.invoke(
|
|
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,
|
|
7932
|
-
if (
|
|
7933
|
-
return Promise.resolve(stopped(
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
8854
|
+
signal2.addEventListener("abort", onAbort, { once: true });
|
|
7949
8855
|
void state.outcome.then((result) => {
|
|
7950
|
-
|
|
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: (
|
|
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: (
|
|
8224
|
-
const runCtx = withSignal(ctx,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
8269
|
-
if (
|
|
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(
|
|
8274
|
-
if (
|
|
9179
|
+
const onAbort = () => reject(abortError(signal2));
|
|
9180
|
+
if (signal2.aborted) {
|
|
8275
9181
|
onAbort();
|
|
8276
9182
|
}
|
|
8277
|
-
|
|
9183
|
+
signal2.addEventListener("abort", onAbort, { once: true });
|
|
8278
9184
|
work.then(
|
|
8279
9185
|
(value) => {
|
|
8280
|
-
|
|
9186
|
+
signal2.removeEventListener("abort", onAbort);
|
|
8281
9187
|
resolve(value);
|
|
8282
9188
|
},
|
|
8283
9189
|
(error) => {
|
|
8284
|
-
|
|
9190
|
+
signal2.removeEventListener("abort", onAbort);
|
|
8285
9191
|
reject(error);
|
|
8286
9192
|
}
|
|
8287
9193
|
);
|
|
8288
9194
|
});
|
|
8289
9195
|
}
|
|
8290
|
-
function contextFor(settings, request, requestId, target2,
|
|
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:
|
|
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
|
|
8309
|
-
// dispatcher; their units of work join
|
|
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
|
|
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 = (
|
|
9253
|
-
logger.info(`Received ${
|
|
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
|
-
|
|
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
|
|
9574
|
-
context.logger[
|
|
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
|
|
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
|
|
9656
|
-
if (typeof
|
|
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(
|
|
9662
|
-
if (
|
|
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:
|
|
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.
|
|
10130
|
-
throw new TypeError(
|
|
10131
|
-
|
|
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
|
|
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
|
|
11123
|
+
const rooms = services.flatMap(
|
|
10144
11124
|
(service) => service.onRoomLeave === void 0 ? [] : [{ owner: service.name, handler: service.onRoomLeave }]
|
|
10145
11125
|
);
|
|
10146
|
-
|
|
10147
|
-
|
|
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 =
|
|
11218
|
+
const sockets = serveSockets(options, created, httpServer, {
|
|
10211
11219
|
dispatcher,
|
|
10212
11220
|
logger,
|
|
10213
11221
|
resolvePrincipal,
|
|
10214
|
-
|
|
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-
|
|
11322
|
+
//# sourceMappingURL=chunk-JVTONQFI.js.map
|