@mcp-b/do-runtime 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +27 -4
- package/dist/chunks/{io-context-RmmjNtwm.js → io-context-BBgKEsdR.js} +12 -3
- package/dist/chunks/{io-context-RmmjNtwm.js.map → io-context-BBgKEsdR.js.map} +1 -1
- package/dist/conformance/host.d.ts +16 -2
- package/dist/conformance.js.map +1 -1
- package/dist/gate.js +1 -1
- package/dist/index.js +740 -206
- package/dist/index.js.map +1 -1
- package/dist/src/api/actor-state.d.ts +30 -21
- package/dist/src/api/global-scope.d.ts +12 -3
- package/dist/src/api/sql.d.ts +17 -6
- package/dist/src/api/web-socket.d.ts +88 -73
- package/dist/src/index.d.ts +4 -4
- package/dist/src/io/io-context.d.ts +1 -0
- package/dist/src/server/actor-container.d.ts +16 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as hasUserErrorDetail, c as setUserErrorDetail, d as tryCurrentSlice, f as CanceledError, i as captureGateStack, m as OutputGate, n as IoContext, o as isExceptionFromInputGateBroken, p as InputGate, r as atCheckpointEnd, s as requireInputLock, t as BrokenActorError, u as tryCurrentIoContext } from "./chunks/io-context-
|
|
1
|
+
import { a as hasUserErrorDetail, c as setUserErrorDetail, d as tryCurrentSlice, f as CanceledError, i as captureGateStack, m as OutputGate, n as IoContext, o as isExceptionFromInputGateBroken, p as InputGate, r as atCheckpointEnd, s as requireInputLock, t as BrokenActorError, u as tryCurrentIoContext } from "./chunks/io-context-BBgKEsdR.js";
|
|
2
2
|
import { RpcTarget as RpcTarget$1 } from "./cloudflare-workers.js";
|
|
3
3
|
import { a as SqliteDatabase, c as getText, l as hasCurrentSqliteTable, o as getBlob, s as getInt64, t as ensureRuntimeStorageVersion, u as isNull } from "./chunks/sqlite-migrations-DsWmLP_B.js";
|
|
4
4
|
import { ALARM_RETRY_MAX_TRIES, ALARM_RETRY_START_SECONDS, AlarmScheduler, RETRY_BACKOFF_MAX, RETRY_JITTER_FACTOR, alarmRetryDelayMs } from "./server/alarm-scheduler.js";
|
|
@@ -46,10 +46,10 @@ var ColoLocalActorNamespace = class {
|
|
|
46
46
|
return this.#channel.getColoLocalActor({ actorId });
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
|
-
var textEncoder$
|
|
49
|
+
var textEncoder$2 = new TextEncoder();
|
|
50
50
|
/** `kj::String::size()` is bytes; `String.prototype.length` is UTF-16 code units. */
|
|
51
51
|
function utf8Length(value) {
|
|
52
|
-
return textEncoder$
|
|
52
|
+
return textEncoder$2.encode(value).length;
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
55
|
* ← `DurableObjectId` (`actor.h:42-84`). "DurableObjectId type seen by
|
|
@@ -999,14 +999,24 @@ function notSerializableType(value) {
|
|
|
999
999
|
* `abstract` in the types — JSG nested types have no JS constructor — so the
|
|
1000
1000
|
* faithful shape is a constructor that refuses. `sql.Cursor` exists for
|
|
1001
1001
|
* `instanceof`, which is all upstream exposes it for.
|
|
1002
|
-
* 3. **The regulator is ported whole
|
|
1003
|
-
*
|
|
1002
|
+
* 3. **The regulator is ported whole, and that is not the whole authorizer.**
|
|
1003
|
+
* Four of its five members are here as callbacks, and none needed the
|
|
1004
1004
|
* authorizer to compute anything — `isAllowedName` is a prefix test,
|
|
1005
|
-
* `isAllowedTrigger` is `return true`, `allowTransactions` throws
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1005
|
+
* `isAllowedTrigger` is `return true`, `allowTransactions` throws,
|
|
1006
|
+
* `shouldAddQueryStats` is a constant. The fifth, `onError`, is not a
|
|
1007
|
+
* callback at all in this port: it is every `throw new Error(message)`
|
|
1008
|
+
* below, which is all its upstream body does with a refusal message.
|
|
1009
|
+
* For those, what the authorizer supplied was the *identifiers*:
|
|
1010
|
+
* with none, the statement text is the only source, so `exec` tokenizes it
|
|
1008
1011
|
* and runs `isAllowedName` over every identifier-shaped token. That is
|
|
1009
1012
|
* deliberately STRICTER than upstream — see `SQL_RESERVED_PREFIX_MESSAGE`.
|
|
1013
|
+
*
|
|
1014
|
+
* But the authorizer also makes decisions no callback ever sees, and those
|
|
1015
|
+
* do NOT arrive with the regulator: `SQLITE_ATTACH` / `SQLITE_DETACH`, the
|
|
1016
|
+
* `SQLITE_CREATE_TEMP_*` family and the `temp` schema, `SQLITE_PRAGMA`, and
|
|
1017
|
+
* the `SQLITE_CREATE_VTABLE` module list. Each is refused from the text in
|
|
1018
|
+
* `refuseUnauthorizedForms` and `requireAllowedPragmas`. `SQLITE_FUNCTION`
|
|
1019
|
+
* is the one still unported — see the README divergence row.
|
|
1010
1020
|
* 4. **`ingest` stays at upstream's SQLite seam.** `SqliteDatabase.ingest()`
|
|
1011
1021
|
* executes every complete statement and returns the partial tail, using the
|
|
1012
1022
|
* same compiled boundaries and regulator as `exec`.
|
|
@@ -1039,8 +1049,15 @@ var SQL_RESERVED_PREFIX_MESSAGE = "not authorized: a SQL statement may not name
|
|
|
1039
1049
|
* `SQLITE_SAVEPOINT`. The same set `util/sqlite.ts` classifies, read here from
|
|
1040
1050
|
* the leading keyword because the untrusted path has to refuse them before the
|
|
1041
1051
|
* trusted one applies them.
|
|
1052
|
+
*
|
|
1053
|
+
* `;` counts as leading trivia here and in every other leading-keyword refusal
|
|
1054
|
+
* below. A statement boundary comes from the backend, and `node:sqlite` reports
|
|
1055
|
+
* an empty leading statement as part of the span it compiled: the `sourceSQL`
|
|
1056
|
+
* for `;ATTACH ...` is the whole string, so an anchor of `^\s*` would read the
|
|
1057
|
+
* keyword as `;` and let the compiled ATTACH through. The browser backend cuts
|
|
1058
|
+
* the same input at the first `;` and refuses the empty statement instead.
|
|
1042
1059
|
*/
|
|
1043
|
-
var TRANSACTION_CONTROL =
|
|
1060
|
+
var TRANSACTION_CONTROL = /^[\s;]*(?:BEGIN|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i;
|
|
1044
1061
|
/** Cheap pre-test, so the tokenizer below runs only on a statement that could fail it. */
|
|
1045
1062
|
var RESERVED_PREFIX_HINT = /_cf_/i;
|
|
1046
1063
|
/** A SQL identifier. Double-quoted and bracketed forms are still identifiers, so only the
|
|
@@ -1057,7 +1074,15 @@ var IDENTIFIER = /[A-Za-z_][A-Za-z0-9_$]*/g;
|
|
|
1057
1074
|
*/
|
|
1058
1075
|
var NOT_CODE = /'(?:[^']|'')*'|--[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
1059
1076
|
/**
|
|
1060
|
-
*
|
|
1077
|
+
* Comments are never code. String literals STAY, for both of this regex's callers: pragma
|
|
1078
|
+
* arguments may be quoted, and SQLite's misquoting feature reads a single-quoted string as an
|
|
1079
|
+
* identifier — `CREATE TABLE 'temp'.t(x)` really creates a temp-schema table — so the checks
|
|
1080
|
+
* that read identifier positions must still see it. `NOT_CODE` blanks literals, which is right
|
|
1081
|
+
* for the token scans and would be a bypass for these callers.
|
|
1082
|
+
*/
|
|
1083
|
+
var NOT_COMMENT = /--[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
1084
|
+
/**
|
|
1085
|
+
* ← `SqlStorageRegulator` (`sql.h:15-22`, `sql.c++:141-165`), whole.
|
|
1061
1086
|
*
|
|
1062
1087
|
* Upstream reaches these through the SQLite authorizer while a statement is
|
|
1063
1088
|
* being compiled. `exec` calls them from the statement text instead, which is
|
|
@@ -1123,20 +1148,108 @@ function refuseTransactionControl(statement) {
|
|
|
1123
1148
|
* byte-identical so a caller matching on it ports unchanged.
|
|
1124
1149
|
*/
|
|
1125
1150
|
var SQL_NOT_AUTHORIZED_MESSAGE = "not authorized: SQLITE_AUTH";
|
|
1126
|
-
/**
|
|
1127
|
-
|
|
1151
|
+
/**
|
|
1152
|
+
* The forms neither the regulator nor any callback sees: the authorizer's own
|
|
1153
|
+
* action codes, plus `VACUUM`, which SQLite itself refuses by precondition.
|
|
1154
|
+
*
|
|
1155
|
+
* Porting `SqlStorageRegulator` whole (point 3 in the file header) carried over its members, but
|
|
1156
|
+
* not the authorizer's own action codes: `SQLITE_ATTACH`, `SQLITE_DETACH`, `SQLITE_CREATE_TEMP_*`
|
|
1157
|
+
* and `SQLITE_CREATE_VTABLE` consult no callback, so nothing here refused them. Every form below
|
|
1158
|
+
* was measured on real workerd through the conformance oracle, not inferred.
|
|
1159
|
+
*
|
|
1160
|
+
* `ATTACH` and `DETACH` are also the isolation boundary rather than a fidelity detail: both
|
|
1161
|
+
* backends open a real file, so on `node:sqlite` an `ATTACH` reads another actor's database and
|
|
1162
|
+
* a `VACUUM INTO` writes anywhere the process can. The reserved-name scan does not cover it,
|
|
1163
|
+
* because that scan tokenizes the SUBMITTED statement — `other._cf_KV` is caught, and every
|
|
1164
|
+
* application table in the same attached database is not.
|
|
1165
|
+
*/
|
|
1166
|
+
var DATABASE_ATTACHMENT = /^[\s;]*(?:ATTACH|DETACH)\b/i;
|
|
1167
|
+
/**
|
|
1168
|
+
* ← the `SQLITE_CREATE_TEMP_*` denials (`sqlite.c++:1323`) and the `dbName == temp` rule
|
|
1169
|
+
* (`sqlite.c++:1073`), which permits a temp-schema database name only `READ` and `UPDATE`.
|
|
1170
|
+
* Upstream's own reason to deny them applies here unchanged: a temporary table makes SQLite
|
|
1171
|
+
* open a separate temporary file that the storage engine knows nothing about.
|
|
1172
|
+
*
|
|
1173
|
+
* Two spellings, one refusal each by its own upstream path: `CREATE TEMP TABLE t(x)` is the
|
|
1174
|
+
* keyword and hits the action codes; `CREATE TABLE temp.t(x)` is the schema qualifier and hits
|
|
1175
|
+
* the `dbName` rule — and the qualifier was the live gap, where the table was created, written
|
|
1176
|
+
* and read back here while workerd refused it outright. The qualifier accepts every quoting
|
|
1177
|
+
* SQLite does, single quotes included, with or without whitespace after the keyword (measured:
|
|
1178
|
+
* workerd refuses `CREATE TABLE 'temp'.t(x)` and `CREATE TABLE"temp".t(x)` the same way), and
|
|
1179
|
+
* is matched only in the object-name position, so an application table merely NAMED `tempest`
|
|
1180
|
+
* or `temporary_log` is untouched. The keyword spelling needs no qualifier arm of its own here:
|
|
1181
|
+
* `TEMP_SCHEMA` already refuses every `CREATE TEMP…` before this pattern is consulted.
|
|
1182
|
+
*
|
|
1183
|
+
* The rest of the `dbName` rule goes unported on purpose: with no way to create a temp-schema
|
|
1184
|
+
* object, `INSERT`/`DELETE`/`DROP` against one die in SQLite as `no such table`, and the one
|
|
1185
|
+
* silent form measures identically — workerd allows `DROP TABLE IF EXISTS temp.ghost` too.
|
|
1186
|
+
*/
|
|
1187
|
+
var TEMP_SCHEMA = /^[\s;]*CREATE\s+(?:TEMP|TEMPORARY)\b/i;
|
|
1188
|
+
var TEMP_QUALIFIED = /^[\s;]*CREATE\s+(?:UNIQUE\s+|VIRTUAL\s+)?(?:TABLE|VIEW|TRIGGER|INDEX)\s*(?:IF\s+NOT\s+EXISTS\s*)?(?:"temp"|'temp'|`temp`|\[temp\]|temp)\s*\./i;
|
|
1189
|
+
/**
|
|
1190
|
+
* ← `SQLITE_CREATE_VTABLE` (`sqlite.c++:1298-1316`): a virtual table is native-code callbacks, so
|
|
1191
|
+
* upstream allows exactly four modules — FTS5 and its `fts5vocab` companion, R*Tree and its
|
|
1192
|
+
* `rtree_i32` variant — and denies every other module SQLite was compiled with.
|
|
1193
|
+
*
|
|
1194
|
+
* `dbstat` is why this is not only fidelity: it reports a row per table with page counts and
|
|
1195
|
+
* byte sizes, so `SELECT name FROM d` enumerates `_cf_KV` and the rest of the runtime's own
|
|
1196
|
+
* tables without the statement ever naming them — around `requireAllowedNames`, which can only
|
|
1197
|
+
* see the text it was given.
|
|
1198
|
+
*
|
|
1199
|
+
* The table name and the module accept every quoting SQLite does — double quotes, backticks,
|
|
1200
|
+
* brackets, and the misquoting feature's single-quoted string, with or without whitespace
|
|
1201
|
+
* before them — because the module has to be read from PAST the name, and a guessed name
|
|
1202
|
+
* boundary is a bypass in both directions: an unparseable name skipped the check, and
|
|
1203
|
+
* `"a USING fts5 b" USING dbstat` read its module out of the quoted name. Measured: workerd
|
|
1204
|
+
* refuses both, refuses `CREATE VIRTUAL TABLE"d"USING dbstat`, resolves `USING "dbstat"` to
|
|
1205
|
+
* the same denial, and allows `USING 'fts5'`. A `CREATE VIRTUAL TABLE` whose module the
|
|
1206
|
+
* pattern cannot read is refused outright — the deliberately stricter direction the
|
|
1207
|
+
* unparseable-PRAGMA fallback below already takes.
|
|
1208
|
+
*/
|
|
1209
|
+
var SQL_IDENTIFIER_SOURCE = /"(?:[^"]|"")*"|'(?:[^']|'')*'|`(?:[^`]|``)*`|\[[^\]]*\]|[A-Za-z_\u0080-\uffff][A-Za-z0-9_$\u0080-\uffff]*/.source;
|
|
1210
|
+
var VIRTUAL_TABLE = /^[\s;]*CREATE\s+VIRTUAL\s+TABLE\b/i;
|
|
1211
|
+
var VIRTUAL_TABLE_MODULE = new RegExp(String.raw`^[\s;]*CREATE\s+VIRTUAL\s+TABLE\s*(?:IF\s+NOT\s+EXISTS\s*)?(?:${SQL_IDENTIFIER_SOURCE})\s*(?:\.\s*(?:${SQL_IDENTIFIER_SOURCE})\s*)?USING\s*(${SQL_IDENTIFIER_SOURCE})`, "i");
|
|
1212
|
+
var ALLOWED_VIRTUAL_TABLE_MODULES = /* @__PURE__ */ new Set([
|
|
1213
|
+
"fts5",
|
|
1214
|
+
"fts5vocab",
|
|
1215
|
+
"rtree",
|
|
1216
|
+
"rtree_i32"
|
|
1217
|
+
]);
|
|
1218
|
+
/**
|
|
1219
|
+
* `VACUUM` has no action code of its own, so the authorizer never sees it. What refuses it
|
|
1220
|
+
* upstream is SQLite's own `cannot VACUUM from within a transaction` precondition, against the
|
|
1221
|
+
* transaction a Durable Object always has open — and that message is what the oracle returned,
|
|
1222
|
+
* for `VACUUM`, `VACUUM main` and `VACUUM INTO` alike. Byte-identical for the same reason
|
|
1223
|
+
* `SQL_NOT_AUTHORIZED_MESSAGE` is: a caller matching on it ports unchanged.
|
|
1224
|
+
*
|
|
1225
|
+
* Refused here unconditionally rather than by transaction state, which is a divergence only in
|
|
1226
|
+
* mechanism: this runtime never runs a statement where upstream would have allowed it.
|
|
1227
|
+
*/
|
|
1228
|
+
var VACUUM_STATEMENT = /^[\s;]*VACUUM\b/i;
|
|
1229
|
+
var SQL_VACUUM_REFUSED_MESSAGE = "cannot VACUUM from within a transaction: SQLITE_ERROR";
|
|
1230
|
+
/** Refuse the authorizer-only forms against one SQLite-decided statement boundary. */
|
|
1231
|
+
function refuseUnauthorizedForms(statement) {
|
|
1232
|
+
const code = statement.replace(NOT_COMMENT, " ");
|
|
1233
|
+
if (VACUUM_STATEMENT.test(code)) throw new Error(SQL_VACUUM_REFUSED_MESSAGE);
|
|
1234
|
+
if (DATABASE_ATTACHMENT.test(code) || TEMP_SCHEMA.test(code) || TEMP_QUALIFIED.test(code)) throw new Error(SQL_NOT_AUTHORIZED_MESSAGE);
|
|
1235
|
+
if (VIRTUAL_TABLE.test(code)) {
|
|
1236
|
+
const moduleName = VIRTUAL_TABLE_MODULE.exec(code)?.[1];
|
|
1237
|
+
if (moduleName === void 0 || !ALLOWED_VIRTUAL_TABLE_MODULES.has(unquoted(moduleName).toLowerCase())) throw new Error(SQL_NOT_AUTHORIZED_MESSAGE);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1128
1240
|
/** Cheap pre-test; `PRAGMA` and the `pragma_` functions both contain it. */
|
|
1129
1241
|
var PRAGMA_HINT = /pragma/i;
|
|
1130
1242
|
/** `PRAGMA [schema.]name`, then `= value`, `(argument)`, or nothing. */
|
|
1131
|
-
var PRAGMA_STATEMENT =
|
|
1243
|
+
var PRAGMA_STATEMENT = /^[\s;]*PRAGMA\s+(?:[A-Za-z_][A-Za-z0-9_$]*\s*\.\s*)?([A-Za-z_][A-Za-z0-9_$]*)(?:\s*=\s*([\s\S]+?)|\s*\(\s*([\s\S]*?)\s*\))?\s*;?\s*$/i;
|
|
1132
1244
|
/**
|
|
1133
|
-
* ← `ALLOWED_PRAGMAS`
|
|
1245
|
+
* ← `ALLOWED_PRAGMAS` (`util/sqlite.c++:543-571`) and `PragmaSignature` (`:528-535`),
|
|
1134
1246
|
* verbatim. `table_list`, `table_info`, and `table_xinfo` are special-cased
|
|
1135
1247
|
* ahead of the table in the authorizer, exactly as upstream's `SQLITE_PRAGMA`
|
|
1136
1248
|
* case does (`util/sqlite.c++:1194-1273`).
|
|
1137
1249
|
*/
|
|
1138
1250
|
var ALLOWED_PRAGMAS = /* @__PURE__ */ new Map([
|
|
1139
1251
|
["data_version", "NO_ARG"],
|
|
1252
|
+
["page_size", "NO_ARG"],
|
|
1140
1253
|
["case_sensitive_like", "BOOLEAN"],
|
|
1141
1254
|
["foreign_keys", "BOOLEAN"],
|
|
1142
1255
|
["defer_foreign_keys", "BOOLEAN"],
|
|
@@ -1228,7 +1341,11 @@ var SQLITE_PRAGMA_NAMES = /* @__PURE__ */ new Set([
|
|
|
1228
1341
|
]);
|
|
1229
1342
|
/** kj's `tryParseAs` is decimal; keep the same acceptance. */
|
|
1230
1343
|
var DECIMAL = /^[+-]?\d+$/;
|
|
1231
|
-
/**
|
|
1344
|
+
/**
|
|
1345
|
+
* One layer of SQL quoting, any of the four forms — a pragma argument, or the module token
|
|
1346
|
+
* `VIRTUAL_TABLE_MODULE` captured. Doubled inner quotes stay doubled, which cannot change a
|
|
1347
|
+
* verdict here: no allowlisted comparison target contains a quote character.
|
|
1348
|
+
*/
|
|
1232
1349
|
function unquoted(argument) {
|
|
1233
1350
|
const first = argument[0];
|
|
1234
1351
|
const last = argument[argument.length - 1];
|
|
@@ -1282,7 +1399,7 @@ function requireAllowedPragmas(statement) {
|
|
|
1282
1399
|
if (!isAllowedPragma(name, argument === "" ? void 0 : argument)) throw new Error(SQL_NOT_AUTHORIZED_MESSAGE);
|
|
1283
1400
|
return;
|
|
1284
1401
|
}
|
|
1285
|
-
if (
|
|
1402
|
+
if (/^[\s;]*PRAGMA\b/i.test(code)) throw new Error(SQL_NOT_AUTHORIZED_MESSAGE);
|
|
1286
1403
|
const literalFree = code.replace(NOT_CODE, " ");
|
|
1287
1404
|
for (const [token] of literalFree.matchAll(IDENTIFIER)) {
|
|
1288
1405
|
if (token.length <= 7 || token.slice(0, 7).toLowerCase() !== "pragma_") continue;
|
|
@@ -1294,6 +1411,7 @@ function requireAllowedPragmas(statement) {
|
|
|
1294
1411
|
/** Everything the untrusted path refuses at one statement boundary. */
|
|
1295
1412
|
function regulateUntrustedStatement(statement) {
|
|
1296
1413
|
refuseTransactionControl(statement);
|
|
1414
|
+
refuseUnauthorizedForms(statement);
|
|
1297
1415
|
requireAllowedPragmas(statement);
|
|
1298
1416
|
}
|
|
1299
1417
|
/** ← `JSG_INHERIT_INTRINSIC(v8::kIteratorPrototype)` (`jsg/iterator.h:1044`). */
|
|
@@ -1632,11 +1750,9 @@ function asRawRow(values) {
|
|
|
1632
1750
|
* `transformMaybeBackpressure` keeps the branch because
|
|
1633
1751
|
* `DeleteAllResults.backpressure` is still a promise in `io/actor-cache.ts`.
|
|
1634
1752
|
*
|
|
1635
|
-
* Not ported, because the substrate has no equivalent:
|
|
1636
|
-
*
|
|
1637
|
-
*
|
|
1638
|
-
* structured-clone encoding with the same public value semantics; the billing
|
|
1639
|
-
* counters
|
|
1753
|
+
* Not ported, because the substrate has no equivalent: V8's private wire bytes,
|
|
1754
|
+
* replaced by a browser-safe structured-clone encoding with the same public
|
|
1755
|
+
* value semantics; the billing counters
|
|
1640
1756
|
* (`billingUnits`, `ActorObserver`, `updateStorageWriteUnit`) and the trace
|
|
1641
1757
|
* spans, both already absent throughout; `enableSql`, a workerd namespace option
|
|
1642
1758
|
* that exists to simulate a non-SQLite Durable Object; and `ReplicaActorOutgoingFactory`,
|
|
@@ -1655,14 +1771,6 @@ var FACET_NAME_MAX_LENGTH = 256;
|
|
|
1655
1771
|
/** Root is at depth 0, so the deepest allowed facet is at depth 3. */
|
|
1656
1772
|
var FACET_TREE_MAX_DEPTH = 4;
|
|
1657
1773
|
/**
|
|
1658
|
-
* The substrate boundary named in the package README: Hibernatable WebSockets
|
|
1659
|
-
* exist so the platform can evict an actor while keeping its sockets open, and
|
|
1660
|
-
* Chrome exposes no equivalent lifecycle. Under this repo's fail-closed tenet
|
|
1661
|
-
* the throw IS the specified behaviour, which is why §2.5 orders the four
|
|
1662
|
-
* silent no-op stubs beside it replaced.
|
|
1663
|
-
*/
|
|
1664
|
-
var HIBERNATION_UNIMPLEMENTED_MESSAGE = "Hibernatable WebSockets are not available in this runtime: they exist so the platform can evict a Durable Object while keeping its sockets open, and there is no equivalent lifecycle to be faithful to.";
|
|
1665
|
-
/**
|
|
1666
1774
|
* ← what falls off the end of `DurableObjectFacets::get`'s class switch
|
|
1667
1775
|
* (`actor-state.c++:1029-1043`).
|
|
1668
1776
|
*
|
|
@@ -1686,11 +1794,31 @@ var OP_PUT_ALARM = "setAlarm()";
|
|
|
1686
1794
|
var OP_DELETE = "delete()";
|
|
1687
1795
|
var OP_DELETE_ALARM = "deleteAlarm()";
|
|
1688
1796
|
var OP_ROLLBACK = "rollback()";
|
|
1797
|
+
/** ← `actor-state.c++:455`, verbatim: the one message both overloads' misuse produces. */
|
|
1798
|
+
var PUT_OVERLOAD_MESSAGE = "put() may only be called with a single key-value pair and optional options as put(key, value, options) or with multiple key-value pairs and optional options as put(entries, options)";
|
|
1799
|
+
/**
|
|
1800
|
+
* ← the `kj::OneOf<kj::String, jsg::Dict<…>>` unwrap on put()'s first parameter: `jsg::Dict`
|
|
1801
|
+
* takes any JS object except an Array — functions and Maps included — and `kj::String` takes
|
|
1802
|
+
* everything else by coercion. A type predicate, so the overload split narrows without a cast.
|
|
1803
|
+
*/
|
|
1804
|
+
function isEntriesArgument(value) {
|
|
1805
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && !Array.isArray(value);
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* ← the struct wrapper (`jsg/struct.h:246-258`), which is NOT the Dict wrapper: `PutOptions` is
|
|
1809
|
+
* all-optional fields, so `null` unwraps to default options, and any object does — arrays and
|
|
1810
|
+
* functions included, because the wrapper checks `IsObject()` with no Array exclusion. Only a
|
|
1811
|
+
* non-null primitive fails to unwrap. Measured on real workerd: `put({k: 1}, null)`, `…, [])`
|
|
1812
|
+
* and `…, function () {})` all write, and `put({k: 1}, "v")` alone is the overload error.
|
|
1813
|
+
*/
|
|
1814
|
+
function isPutOptions(value) {
|
|
1815
|
+
return value === null || typeof value === "object" || typeof value === "function";
|
|
1816
|
+
}
|
|
1689
1817
|
/** The key immediately after `k` in byte order is `k` plus this. */
|
|
1690
1818
|
var NULL_CHARACTER = "\0";
|
|
1691
1819
|
/** ← the `0xff` upstream strips from the tail of a prefix, in UTF-16 code units. */
|
|
1692
1820
|
var MAX_CODE_UNIT = 65535;
|
|
1693
|
-
var textEncoder = new TextEncoder();
|
|
1821
|
+
var textEncoder$1 = new TextEncoder();
|
|
1694
1822
|
var textDecoder = new TextDecoder();
|
|
1695
1823
|
/** A byte JSON could never begin with, `DO`, and the local codec version. */
|
|
1696
1824
|
var VALUE_CODEC_HEADER = new Uint8Array([
|
|
@@ -1706,7 +1834,7 @@ var VALUE_CODEC_HEADER = new Uint8Array([
|
|
|
1706
1834
|
* remain readable.
|
|
1707
1835
|
*/
|
|
1708
1836
|
function serializeValue(value) {
|
|
1709
|
-
const body = textEncoder.encode(JSON.stringify(serialize(value)));
|
|
1837
|
+
const body = textEncoder$1.encode(JSON.stringify(serialize(value)));
|
|
1710
1838
|
const encoded = new Uint8Array(VALUE_CODEC_HEADER.byteLength + body.byteLength);
|
|
1711
1839
|
encoded.set(VALUE_CODEC_HEADER);
|
|
1712
1840
|
encoded.set(body, VALUE_CODEC_HEADER.byteLength);
|
|
@@ -1889,10 +2017,11 @@ var DurableObjectStorageOperations = class {
|
|
|
1889
2017
|
}
|
|
1890
2018
|
put(keyOrEntries, valueOrOptions, maybeOptions) {
|
|
1891
2019
|
requireInputLock(this.ctx, OP_PUT);
|
|
1892
|
-
if (
|
|
2020
|
+
if (!isEntriesArgument(keyOrEntries)) {
|
|
1893
2021
|
if (valueOrOptions === void 0) throw new TypeError("put() called with undefined value.");
|
|
1894
|
-
return this.#putOne(keyOrEntries
|
|
2022
|
+
return this.#putOne(`${keyOrEntries}`, valueOrOptions, { ...maybeOptions });
|
|
1895
2023
|
}
|
|
2024
|
+
if (valueOrOptions !== void 0 && !isPutOptions(valueOrOptions)) throw new TypeError(PUT_OVERLOAD_MESSAGE);
|
|
1896
2025
|
return this.#putMultiple(keyOrEntries, { ...valueOrOptions });
|
|
1897
2026
|
}
|
|
1898
2027
|
delete(keyOrKeys, maybeOptions) {
|
|
@@ -2152,7 +2281,7 @@ var DurableObjectTransaction = class extends DurableObjectStorageOperations {
|
|
|
2152
2281
|
* refuses, which is a bound a caller can hit.
|
|
2153
2282
|
*/
|
|
2154
2283
|
function requireValidFacetName(name) {
|
|
2155
|
-
if (textEncoder.encode(name).length > 256) throw new TypeError(`Facet name is too long (max 256 characters).`);
|
|
2284
|
+
if (textEncoder$1.encode(name).length > 256) throw new TypeError(`Facet name is too long (max 256 characters).`);
|
|
2156
2285
|
}
|
|
2157
2286
|
/**
|
|
2158
2287
|
* ← the `KJ_SWITCH_ONEOF(options.$class)` lambda (`actor-state.c++:1029-1043`).
|
|
@@ -2335,29 +2464,29 @@ var DurableObjectState = class {
|
|
|
2335
2464
|
if (options.mode !== "auto" && options.mode !== "disabled") throw new TypeError(`configureReadReplication() called with unknown mode setting: ${options.mode}.`);
|
|
2336
2465
|
return this.#ctx.awaitIo(storage.getActorCacheInterface().configureReadReplication(options.mode === "auto"));
|
|
2337
2466
|
}
|
|
2338
|
-
acceptWebSocket(
|
|
2339
|
-
|
|
2467
|
+
acceptWebSocket(ws, tags) {
|
|
2468
|
+
this.#options.webSockets.acceptWebSocket(ws, tags);
|
|
2340
2469
|
}
|
|
2341
|
-
getWebSockets(
|
|
2342
|
-
|
|
2470
|
+
getWebSockets(tag) {
|
|
2471
|
+
return tag === void 0 ? this.#options.webSockets.getWebSockets() : this.#options.webSockets.getWebSockets(tag);
|
|
2343
2472
|
}
|
|
2344
|
-
setWebSocketAutoResponse(
|
|
2345
|
-
|
|
2473
|
+
setWebSocketAutoResponse(maybeReqResp) {
|
|
2474
|
+
this.#options.webSockets.setWebSocketAutoResponse(maybeReqResp);
|
|
2346
2475
|
}
|
|
2347
2476
|
getWebSocketAutoResponse() {
|
|
2348
|
-
|
|
2477
|
+
return this.#options.webSockets.getWebSocketAutoResponse();
|
|
2349
2478
|
}
|
|
2350
|
-
getWebSocketAutoResponseTimestamp(
|
|
2351
|
-
|
|
2479
|
+
getWebSocketAutoResponseTimestamp(ws) {
|
|
2480
|
+
return this.#options.webSockets.getWebSocketAutoResponseTimestamp(ws);
|
|
2352
2481
|
}
|
|
2353
|
-
setHibernatableWebSocketEventTimeout(
|
|
2354
|
-
|
|
2482
|
+
setHibernatableWebSocketEventTimeout(timeoutMs) {
|
|
2483
|
+
this.#options.webSockets.setHibernatableWebSocketEventTimeout(timeoutMs);
|
|
2355
2484
|
}
|
|
2356
2485
|
getHibernatableWebSocketEventTimeout() {
|
|
2357
|
-
|
|
2486
|
+
return this.#options.webSockets.getHibernatableWebSocketEventTimeout();
|
|
2358
2487
|
}
|
|
2359
|
-
getTags(
|
|
2360
|
-
|
|
2488
|
+
getTags(ws) {
|
|
2489
|
+
return this.#options.webSockets.getTags(ws);
|
|
2361
2490
|
}
|
|
2362
2491
|
};
|
|
2363
2492
|
//#endregion
|
|
@@ -2627,6 +2756,528 @@ function gateReader(ctx, reader) {
|
|
|
2627
2756
|
} });
|
|
2628
2757
|
}
|
|
2629
2758
|
//#endregion
|
|
2759
|
+
//#region src/api/web-socket.ts
|
|
2760
|
+
var WebSocketRequestResponsePairImpl = class {
|
|
2761
|
+
#request;
|
|
2762
|
+
#response;
|
|
2763
|
+
constructor(request, response) {
|
|
2764
|
+
this.#request = String(request);
|
|
2765
|
+
this.#response = String(response);
|
|
2766
|
+
}
|
|
2767
|
+
get request() {
|
|
2768
|
+
return this.#request;
|
|
2769
|
+
}
|
|
2770
|
+
get response() {
|
|
2771
|
+
return this.#response;
|
|
2772
|
+
}
|
|
2773
|
+
};
|
|
2774
|
+
var RuntimeWebSocketRequestResponsePair = new Proxy(WebSocketRequestResponsePairImpl, { apply() {
|
|
2775
|
+
throw new TypeError("Failed to construct 'WebSocketRequestResponsePair': Please use the 'new' operator, this DOM object constructor cannot be called as a function.");
|
|
2776
|
+
} });
|
|
2777
|
+
/** ← the `JSG_REQUIRE(!native.state.is<Accepted>(), ...)` at the head of `accept()`. */
|
|
2778
|
+
var ALREADY_ACCEPTED_MESSAGE = "acceptWebSocket(): this socket has already been accepted by an actor. A socket's frames are delivered by exactly one read loop, and a second accept would deliver them under two gates.";
|
|
2779
|
+
var HIBERNATION_ALREADY_ACCEPTED_MESSAGE = "Cannot call `acceptWebSocket()` if the WebSocket was already accepted via `accept()`";
|
|
2780
|
+
var HIBERNATION_AFTER_ACCEPT_MESSAGE = "Can't accept() WebSocket after enabling hibernation.";
|
|
2781
|
+
var HIBERNATION_PAIR_USED_MESSAGE = "Cannot call `acceptWebSocket()` on this WebSocket because its pair has already been accepted or used in a Response.";
|
|
2782
|
+
var MAX_HIBERNATABLE_SOCKETS = 32768;
|
|
2783
|
+
var MAX_TAGS = 10;
|
|
2784
|
+
var MAX_TAG_LENGTH = 256;
|
|
2785
|
+
var MAX_ATTACHMENT_BYTES = 16384;
|
|
2786
|
+
var MAX_AUTO_RESPONSE_BYTES = 2048;
|
|
2787
|
+
var MAX_EVENT_TIMEOUT = 6048e5;
|
|
2788
|
+
var MAX_CLOSE_REASON_BYTES = 123;
|
|
2789
|
+
var WEB_SOCKET_READY_STATES = {
|
|
2790
|
+
READY_STATE_CONNECTING: 0,
|
|
2791
|
+
READY_STATE_OPEN: 1,
|
|
2792
|
+
READY_STATE_CLOSING: 2,
|
|
2793
|
+
READY_STATE_CLOSED: 3,
|
|
2794
|
+
CONNECTING: 0,
|
|
2795
|
+
OPEN: 1,
|
|
2796
|
+
CLOSING: 2,
|
|
2797
|
+
CLOSED: 3
|
|
2798
|
+
};
|
|
2799
|
+
var textEncoder = new TextEncoder();
|
|
2800
|
+
var SOCKET_EVENTS = [
|
|
2801
|
+
"open",
|
|
2802
|
+
"message",
|
|
2803
|
+
"close",
|
|
2804
|
+
"error"
|
|
2805
|
+
];
|
|
2806
|
+
var metadata = /* @__PURE__ */ new WeakMap();
|
|
2807
|
+
function socketMetadata(socket) {
|
|
2808
|
+
let value = metadata.get(socket);
|
|
2809
|
+
if (value === void 0) {
|
|
2810
|
+
value = {};
|
|
2811
|
+
metadata.set(socket, value);
|
|
2812
|
+
}
|
|
2813
|
+
return value;
|
|
2814
|
+
}
|
|
2815
|
+
function isRawWebSocket(value) {
|
|
2816
|
+
return typeof value === "object" && value !== null && "addEventListener" in value && typeof value.addEventListener === "function" && "send" in value && typeof value.send === "function" && "close" in value && typeof value.close === "function";
|
|
2817
|
+
}
|
|
2818
|
+
function requireWebSocket(value, operation) {
|
|
2819
|
+
if (!isRawWebSocket(value)) throw new TypeError(`Failed to execute '${operation}' on 'WebSocket': parameter 1 is not of type 'WebSocket'.`);
|
|
2820
|
+
return value;
|
|
2821
|
+
}
|
|
2822
|
+
function serializeAttachment(socket, value) {
|
|
2823
|
+
try {
|
|
2824
|
+
structuredClone(value);
|
|
2825
|
+
} catch (error) {
|
|
2826
|
+
if (error instanceof DOMException && error.name === "DataCloneError") throw new DOMException(error.message.replace(/^Failed to execute 'structuredClone' on '[^']+': /, ""), "DataCloneError");
|
|
2827
|
+
throw error;
|
|
2828
|
+
}
|
|
2829
|
+
const bytes = serializeValue(value);
|
|
2830
|
+
const measuredBytes = typeof value === "string" ? textEncoder.encode(value).byteLength + 5 : bytes.byteLength;
|
|
2831
|
+
if (measuredBytes > MAX_ATTACHMENT_BYTES) throw new Error(`A WebSocket 'attachment' cannot be larger than ${MAX_ATTACHMENT_BYTES} bytes.'attachment' was ${measuredBytes} bytes.`);
|
|
2832
|
+
const state = socketMetadata(socket);
|
|
2833
|
+
state.attachment = bytes;
|
|
2834
|
+
if (state.accepted?.mode === "hibernatable") state.accepted.registry.attachmentChanged(socket, bytes);
|
|
2835
|
+
}
|
|
2836
|
+
function deserializeAttachment(socket) {
|
|
2837
|
+
const attachment = socketMetadata(socket).attachment;
|
|
2838
|
+
if (attachment === void 0) return null;
|
|
2839
|
+
return deserializeValue("WebSocket attachment", attachment);
|
|
2840
|
+
}
|
|
2841
|
+
function serializeAttachmentMethod(value) {
|
|
2842
|
+
if (arguments.length === 0) throw new TypeError("Failed to execute 'serializeAttachment' on 'WebSocket': parameter 1 is not of type 'Value'.");
|
|
2843
|
+
serializeAttachment(requireWebSocket(this, "serializeAttachment"), value);
|
|
2844
|
+
}
|
|
2845
|
+
function deserializeAttachmentMethod() {
|
|
2846
|
+
return deserializeAttachment(requireWebSocket(this, "deserializeAttachment"));
|
|
2847
|
+
}
|
|
2848
|
+
function cloneMessageData(data) {
|
|
2849
|
+
if (typeof data === "string" || data instanceof Blob) return data;
|
|
2850
|
+
if (data instanceof ArrayBuffer) return data.slice(0);
|
|
2851
|
+
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice().buffer;
|
|
2852
|
+
return String(data);
|
|
2853
|
+
}
|
|
2854
|
+
var MemoryWebSocketEndpoint = class extends EventTarget {
|
|
2855
|
+
peer;
|
|
2856
|
+
#sentClose = false;
|
|
2857
|
+
send(data) {
|
|
2858
|
+
this.peer.dispatchEvent(new MessageEvent("message", { data: cloneMessageData(data) }));
|
|
2859
|
+
}
|
|
2860
|
+
close(code = 1e3, reason = "") {
|
|
2861
|
+
if (this.#sentClose) return;
|
|
2862
|
+
this.#sentClose = true;
|
|
2863
|
+
this.peer.dispatchEvent(new CloseEvent("close", {
|
|
2864
|
+
code,
|
|
2865
|
+
reason,
|
|
2866
|
+
wasClean: true
|
|
2867
|
+
}));
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2870
|
+
/** One public socket identity, in classic or hibernatable mode after acceptance. */
|
|
2871
|
+
var AcceptedWebSocket = class AcceptedWebSocket extends EventTarget {
|
|
2872
|
+
static READY_STATE_CONNECTING = WEB_SOCKET_READY_STATES.READY_STATE_CONNECTING;
|
|
2873
|
+
static READY_STATE_OPEN = WEB_SOCKET_READY_STATES.READY_STATE_OPEN;
|
|
2874
|
+
static READY_STATE_CLOSING = WEB_SOCKET_READY_STATES.READY_STATE_CLOSING;
|
|
2875
|
+
static READY_STATE_CLOSED = WEB_SOCKET_READY_STATES.READY_STATE_CLOSED;
|
|
2876
|
+
static CONNECTING = WEB_SOCKET_READY_STATES.CONNECTING;
|
|
2877
|
+
static OPEN = WEB_SOCKET_READY_STATES.OPEN;
|
|
2878
|
+
static CLOSING = WEB_SOCKET_READY_STATES.CLOSING;
|
|
2879
|
+
static CLOSED = WEB_SOCKET_READY_STATES.CLOSED;
|
|
2880
|
+
bufferedAmount = 0;
|
|
2881
|
+
extensions = "";
|
|
2882
|
+
protocol = "";
|
|
2883
|
+
url = "";
|
|
2884
|
+
#ctx;
|
|
2885
|
+
#socket;
|
|
2886
|
+
#pairState;
|
|
2887
|
+
#delivery = { mode: "pending" };
|
|
2888
|
+
#pump = Promise.resolve();
|
|
2889
|
+
#pending = [];
|
|
2890
|
+
#readyState = AcceptedWebSocket.OPEN;
|
|
2891
|
+
#ownClose = false;
|
|
2892
|
+
#peerClose = false;
|
|
2893
|
+
#binaryType = "blob";
|
|
2894
|
+
onopen = null;
|
|
2895
|
+
onmessage = null;
|
|
2896
|
+
onclose = null;
|
|
2897
|
+
onerror = null;
|
|
2898
|
+
constructor(ctx, socket, pairState) {
|
|
2899
|
+
super();
|
|
2900
|
+
this.#ctx = ctx;
|
|
2901
|
+
this.#socket = socket;
|
|
2902
|
+
this.#pairState = pairState;
|
|
2903
|
+
if (pairState === void 0) this.#enableClassic();
|
|
2904
|
+
for (const type of SOCKET_EVENTS) socket.addEventListener(type, (event) => {
|
|
2905
|
+
this.#receive(type, event);
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
get readyState() {
|
|
2909
|
+
return this.#readyState;
|
|
2910
|
+
}
|
|
2911
|
+
get binaryType() {
|
|
2912
|
+
return this.#binaryType;
|
|
2913
|
+
}
|
|
2914
|
+
set binaryType(value) {
|
|
2915
|
+
this.#binaryType = value;
|
|
2916
|
+
}
|
|
2917
|
+
accept() {
|
|
2918
|
+
if (this.#delivery.mode === "hibernatable") throw new TypeError(HIBERNATION_AFTER_ACCEPT_MESSAGE);
|
|
2919
|
+
if (this.#delivery.mode === "classic") throw new Error(ALREADY_ACCEPTED_MESSAGE);
|
|
2920
|
+
if (this.#pairState !== void 0) this.#pairState.used = true;
|
|
2921
|
+
this.#enableClassic();
|
|
2922
|
+
}
|
|
2923
|
+
send(data) {
|
|
2924
|
+
if (this.#delivery.mode === "hibernatable" && this.#ownClose) throw new TypeError("Can't call WebSocket send() after close().");
|
|
2925
|
+
if (this.#peerClose || this.#readyState === AcceptedWebSocket.CLOSED) return;
|
|
2926
|
+
this.#markPairUsed();
|
|
2927
|
+
this.#enqueue(() => this.#socket.send(data));
|
|
2928
|
+
}
|
|
2929
|
+
close(code, reason = "") {
|
|
2930
|
+
if (this.#readyState === AcceptedWebSocket.CLOSED || this.#ownClose) return;
|
|
2931
|
+
if (this.#delivery.mode === "hibernatable" || this.#pairState !== void 0) validateClose(code, reason);
|
|
2932
|
+
this.#markPairUsed();
|
|
2933
|
+
this.#ownClose = true;
|
|
2934
|
+
this.#readyState = this.#peerClose ? AcceptedWebSocket.CLOSED : AcceptedWebSocket.CLOSING;
|
|
2935
|
+
this.#enqueue(() => this.#socket.close(code, reason));
|
|
2936
|
+
}
|
|
2937
|
+
serializeAttachment(value) {
|
|
2938
|
+
if (arguments.length === 0) serializeAttachmentMethod.call(this);
|
|
2939
|
+
else serializeAttachment(this, value);
|
|
2940
|
+
}
|
|
2941
|
+
deserializeAttachment() {
|
|
2942
|
+
return deserializeAttachment(this);
|
|
2943
|
+
}
|
|
2944
|
+
acceptHibernation(registry) {
|
|
2945
|
+
if (this.#delivery.mode !== "pending") throw new Error(HIBERNATION_ALREADY_ACCEPTED_MESSAGE);
|
|
2946
|
+
if (this.#pairState?.used === true && !this.#pairState.hibernationAccepted) throw new Error(HIBERNATION_PAIR_USED_MESSAGE);
|
|
2947
|
+
if (this.#pairState !== void 0) {
|
|
2948
|
+
this.#pairState.used = true;
|
|
2949
|
+
this.#pairState.hibernationAccepted = true;
|
|
2950
|
+
}
|
|
2951
|
+
this.#activateHibernation(registry, this.#ctx);
|
|
2952
|
+
}
|
|
2953
|
+
rehydrateHibernation(registry, ctx) {
|
|
2954
|
+
this.#activateHibernation(registry, ctx);
|
|
2955
|
+
}
|
|
2956
|
+
#activateHibernation(registry, ctx) {
|
|
2957
|
+
this.#ctx = ctx;
|
|
2958
|
+
this.#delivery = {
|
|
2959
|
+
mode: "hibernatable",
|
|
2960
|
+
registry
|
|
2961
|
+
};
|
|
2962
|
+
this.#pending = [];
|
|
2963
|
+
}
|
|
2964
|
+
markPairUsed() {
|
|
2965
|
+
this.#markPairUsed();
|
|
2966
|
+
}
|
|
2967
|
+
#markPairUsed() {
|
|
2968
|
+
if (this.#pairState !== void 0) this.#pairState.used = true;
|
|
2969
|
+
}
|
|
2970
|
+
#enableClassic() {
|
|
2971
|
+
const delivery = {
|
|
2972
|
+
mode: "classic",
|
|
2973
|
+
criticalSection: this.#ctx.getCriticalSection()
|
|
2974
|
+
};
|
|
2975
|
+
this.#delivery = delivery;
|
|
2976
|
+
socketMetadata(this).accepted = { mode: "classic" };
|
|
2977
|
+
const pending = this.#pending;
|
|
2978
|
+
this.#pending = [];
|
|
2979
|
+
for (const item of pending) this.#deliverClassic(item.type, item.event, delivery.criticalSection);
|
|
2980
|
+
}
|
|
2981
|
+
#receive(type, event) {
|
|
2982
|
+
if (type === "close") {
|
|
2983
|
+
this.#receiveClose(event);
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
const delivery = this.#delivery;
|
|
2987
|
+
if (delivery.mode === "pending") this.#pending.push({
|
|
2988
|
+
type,
|
|
2989
|
+
event
|
|
2990
|
+
});
|
|
2991
|
+
else if (delivery.mode === "classic") this.#deliverClassic(type, event, delivery.criticalSection);
|
|
2992
|
+
else delivery.registry.receive(this, type, event);
|
|
2993
|
+
}
|
|
2994
|
+
#receiveClose(event) {
|
|
2995
|
+
if (this.#ownClose) this.#readyState = AcceptedWebSocket.CLOSED;
|
|
2996
|
+
else {
|
|
2997
|
+
this.#peerClose = true;
|
|
2998
|
+
this.#readyState = AcceptedWebSocket.CLOSING;
|
|
2999
|
+
if (this.#delivery.mode === "classic" && this.#pairState !== void 0) {
|
|
3000
|
+
if (event.code === 1005 || event.code === 1006 || event.code === 1015) this.#readyState = AcceptedWebSocket.CLOSED;
|
|
3001
|
+
else this.close(event.code, event.reason);
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
const delivery = this.#delivery;
|
|
3005
|
+
if (delivery.mode === "pending") this.#pending.push({
|
|
3006
|
+
type: "close",
|
|
3007
|
+
event
|
|
3008
|
+
});
|
|
3009
|
+
else if (delivery.mode === "classic") this.#deliverClassic("close", event, delivery.criticalSection);
|
|
3010
|
+
else delivery.registry.receive(this, "close", event);
|
|
3011
|
+
}
|
|
3012
|
+
#deliverClassic(type, event, criticalSection) {
|
|
3013
|
+
this.#ctx.addWaitUntil(this.#ctx.run(() => {
|
|
3014
|
+
const delivered = cloneEventFor(type, event);
|
|
3015
|
+
this.dispatchEvent(delivered);
|
|
3016
|
+
const handler = this[`on${type}`];
|
|
3017
|
+
handler?.(delivered);
|
|
3018
|
+
}, { input: criticalSection }));
|
|
3019
|
+
}
|
|
3020
|
+
#enqueue(write) {
|
|
3021
|
+
const outputLock = this.#ctx.waitForOutputLocks();
|
|
3022
|
+
this.#pump = this.#pump.then(async () => {
|
|
3023
|
+
await outputLock;
|
|
3024
|
+
write();
|
|
3025
|
+
});
|
|
3026
|
+
this.#ctx.addWaitUntil(this.#pump);
|
|
3027
|
+
}
|
|
3028
|
+
};
|
|
3029
|
+
for (const [name, value] of Object.entries(WEB_SOCKET_READY_STATES)) Object.defineProperty(AcceptedWebSocket.prototype, name, {
|
|
3030
|
+
value,
|
|
3031
|
+
enumerable: true
|
|
3032
|
+
});
|
|
3033
|
+
var HibernatableWebSocketRegistry = class {
|
|
3034
|
+
#ctx;
|
|
3035
|
+
#dispatch;
|
|
3036
|
+
#host;
|
|
3037
|
+
#entries = [];
|
|
3038
|
+
#autoResponse = null;
|
|
3039
|
+
#eventTimeout = null;
|
|
3040
|
+
#pairConstructor;
|
|
3041
|
+
constructor(ctx, dispatch, host, rehydrated = []) {
|
|
3042
|
+
this.#ctx = ctx;
|
|
3043
|
+
this.#dispatch = dispatch;
|
|
3044
|
+
this.#host = host;
|
|
3045
|
+
for (const value of rehydrated) this.#rehydrate(value);
|
|
3046
|
+
}
|
|
3047
|
+
get WebSocketPair() {
|
|
3048
|
+
const registry = this;
|
|
3049
|
+
this.#pairConstructor ??= new Proxy(class WebSocketPair {}, { construct: () => registry.#createPair() });
|
|
3050
|
+
return this.#pairConstructor;
|
|
3051
|
+
}
|
|
3052
|
+
acceptWebSocket(socket, tags) {
|
|
3053
|
+
if (!isRawWebSocket(socket)) throw new TypeError("Failed to execute 'acceptWebSocket' on 'DurableObjectState': parameter 1 is not of type 'WebSocket'.");
|
|
3054
|
+
const state = socketMetadata(socket);
|
|
3055
|
+
if (state.accepted !== void 0) throw new Error(HIBERNATION_ALREADY_ACCEPTED_MESSAGE);
|
|
3056
|
+
if (this.#entries.length >= MAX_HIBERNATABLE_SOCKETS) throw new Error(`only ${MAX_HIBERNATABLE_SOCKETS} websockets can be accepted on a single Durable Object instance`);
|
|
3057
|
+
const normalizedTags = normalizeTags(tags);
|
|
3058
|
+
if (socket instanceof AcceptedWebSocket) socket.acceptHibernation(this);
|
|
3059
|
+
else this.#listenRaw(socket);
|
|
3060
|
+
state.accepted = {
|
|
3061
|
+
mode: "hibernatable",
|
|
3062
|
+
registry: this
|
|
3063
|
+
};
|
|
3064
|
+
this.#entries.push({
|
|
3065
|
+
socket,
|
|
3066
|
+
tags: normalizedTags
|
|
3067
|
+
});
|
|
3068
|
+
this.#host?.accepted(socket, normalizedTags);
|
|
3069
|
+
}
|
|
3070
|
+
getWebSockets(tag) {
|
|
3071
|
+
if (arguments.length > 0) {
|
|
3072
|
+
if (typeof tag !== "string") return [];
|
|
3073
|
+
return this.#entries.filter((entry) => entry.tags.includes(tag)).map((entry) => entry.socket);
|
|
3074
|
+
}
|
|
3075
|
+
return this.#entries.map((entry) => entry.socket).reverse();
|
|
3076
|
+
}
|
|
3077
|
+
getTags(socket) {
|
|
3078
|
+
const state = isRawWebSocket(socket) ? metadata.get(socket) : void 0;
|
|
3079
|
+
if (state?.accepted === void 0) throw new Error("you must call 'acceptWebSocket()' before attempting to access the tags of a WebSocket.");
|
|
3080
|
+
if (state.accepted.mode !== "hibernatable") throw new Error("only hibernatable websockets can have tags.");
|
|
3081
|
+
const entry = this.#entries.find((candidate) => candidate.socket === socket);
|
|
3082
|
+
if (entry === void 0) throw new Error("you must call 'acceptWebSocket()' before attempting to access the tags of a WebSocket.");
|
|
3083
|
+
return [...entry.tags];
|
|
3084
|
+
}
|
|
3085
|
+
setWebSocketAutoResponse(pair) {
|
|
3086
|
+
if (pair === void 0) {
|
|
3087
|
+
this.#autoResponse = null;
|
|
3088
|
+
this.#host?.autoResponse(null);
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
if (!(pair instanceof WebSocketRequestResponsePairImpl)) throw new TypeError("Failed to execute 'setWebSocketAutoResponse' on 'DurableObjectState': parameter 1 is not of type 'WebSocketRequestResponsePair'.");
|
|
3092
|
+
validateAutoResponseSize("Request", pair.request);
|
|
3093
|
+
validateAutoResponseSize("Response", pair.response);
|
|
3094
|
+
this.#autoResponse = pair;
|
|
3095
|
+
this.#host?.autoResponse({
|
|
3096
|
+
request: pair.request,
|
|
3097
|
+
response: pair.response
|
|
3098
|
+
});
|
|
3099
|
+
}
|
|
3100
|
+
getWebSocketAutoResponse() {
|
|
3101
|
+
const pair = this.#autoResponse;
|
|
3102
|
+
return pair === null ? null : new RuntimeWebSocketRequestResponsePair(pair.request, pair.response);
|
|
3103
|
+
}
|
|
3104
|
+
getWebSocketAutoResponseTimestamp(socket) {
|
|
3105
|
+
if (!isRawWebSocket(socket)) throw new TypeError("Failed to execute 'getWebSocketAutoResponseTimestamp' on 'DurableObjectState': parameter 1 is not of type 'WebSocket'.");
|
|
3106
|
+
const timestamp = this.#entries.find((entry) => entry.socket === socket)?.autoResponseTimestamp;
|
|
3107
|
+
return timestamp === void 0 ? null : new Date(timestamp);
|
|
3108
|
+
}
|
|
3109
|
+
setHibernatableWebSocketEventTimeout(value) {
|
|
3110
|
+
if (value === void 0 || Number(value) === 0) {
|
|
3111
|
+
this.#eventTimeout = null;
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
3114
|
+
const number = Number(value);
|
|
3115
|
+
if (Number.isNaN(number)) throw new TypeError("The value cannot be converted because it is not an integer.");
|
|
3116
|
+
if (number < 0) throw new TypeError("The value cannot be converted because it is negative and this API expects a positive number.");
|
|
3117
|
+
if (number > 4294967295) throw new TypeError("Value out of range. Must be less than or equal to 4294967295.");
|
|
3118
|
+
const timeout = Math.trunc(number);
|
|
3119
|
+
if (timeout > MAX_EVENT_TIMEOUT) throw new Error(`Event timeout should not exceed ${MAX_EVENT_TIMEOUT} ms.`);
|
|
3120
|
+
this.#eventTimeout = timeout;
|
|
3121
|
+
}
|
|
3122
|
+
getHibernatableWebSocketEventTimeout() {
|
|
3123
|
+
return this.#eventTimeout;
|
|
3124
|
+
}
|
|
3125
|
+
attachmentChanged(socket, bytes) {
|
|
3126
|
+
if (this.#entries.some((entry) => entry.socket === socket)) this.#host?.attachment(socket, bytes);
|
|
3127
|
+
}
|
|
3128
|
+
receive(socket, type, event) {
|
|
3129
|
+
const entry = this.#entries.find((candidate) => candidate.socket === socket);
|
|
3130
|
+
if (entry === void 0) return;
|
|
3131
|
+
if (type === "message") {
|
|
3132
|
+
const data = event.data;
|
|
3133
|
+
if (typeof data === "string" && data === this.#autoResponse?.request) {
|
|
3134
|
+
entry.autoResponseTimestamp = this.#ctx.now();
|
|
3135
|
+
socket.send(this.#autoResponse.response);
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
3138
|
+
const message = cloneMessageData(data);
|
|
3139
|
+
if (message instanceof Blob) {
|
|
3140
|
+
this.#ctx.addWaitUntil(message.arrayBuffer().then((buffer) => {
|
|
3141
|
+
this.#schedule(() => this.#dispatch.message(socket, buffer));
|
|
3142
|
+
}));
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
this.#schedule(() => this.#dispatch.message(socket, message));
|
|
3146
|
+
return;
|
|
3147
|
+
}
|
|
3148
|
+
if (type === "close") {
|
|
3149
|
+
this.#remove(entry);
|
|
3150
|
+
const close = event;
|
|
3151
|
+
this.#schedule(() => this.#dispatch.close(socket, close.code, close.reason, close.wasClean));
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
if (type === "error") this.#schedule(() => this.#dispatch.error(socket, event));
|
|
3155
|
+
}
|
|
3156
|
+
#schedule(handler) {
|
|
3157
|
+
this.#ctx.addWaitUntil(this.#ctx.run(handler).then(() => {}));
|
|
3158
|
+
}
|
|
3159
|
+
#remove(entry) {
|
|
3160
|
+
const index = this.#entries.indexOf(entry);
|
|
3161
|
+
if (index === -1) return;
|
|
3162
|
+
this.#entries.splice(index, 1);
|
|
3163
|
+
this.#host?.closed(entry.socket);
|
|
3164
|
+
}
|
|
3165
|
+
#listenRaw(socket) {
|
|
3166
|
+
const state = socketMetadata(socket);
|
|
3167
|
+
if (state.rawListenersInstalled === true) return;
|
|
3168
|
+
state.rawListenersInstalled = true;
|
|
3169
|
+
for (const type of [
|
|
3170
|
+
"message",
|
|
3171
|
+
"close",
|
|
3172
|
+
"error"
|
|
3173
|
+
]) socket.addEventListener(type, (event) => {
|
|
3174
|
+
const accepted = socketMetadata(socket).accepted;
|
|
3175
|
+
if (accepted?.mode === "hibernatable") accepted.registry.receive(socket, type, event);
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
#rehydrate(value) {
|
|
3179
|
+
const socket = value.socket;
|
|
3180
|
+
if (!isRawWebSocket(socket)) throw new TypeError("ActorContainerOptions.webSockets contains a non-WebSocket value.");
|
|
3181
|
+
const tags = normalizeTags(value.tags);
|
|
3182
|
+
const state = socketMetadata(socket);
|
|
3183
|
+
state.accepted = {
|
|
3184
|
+
mode: "hibernatable",
|
|
3185
|
+
registry: this
|
|
3186
|
+
};
|
|
3187
|
+
if (value.attachment !== void 0) state.attachment = value.attachment.slice();
|
|
3188
|
+
if (socket instanceof AcceptedWebSocket) socket.rehydrateHibernation(this, this.#ctx);
|
|
3189
|
+
else this.#listenRaw(socket);
|
|
3190
|
+
const entry = {
|
|
3191
|
+
socket,
|
|
3192
|
+
tags
|
|
3193
|
+
};
|
|
3194
|
+
if (value.autoResponseTimestamp !== void 0) entry.autoResponseTimestamp = value.autoResponseTimestamp;
|
|
3195
|
+
this.#entries.push(entry);
|
|
3196
|
+
}
|
|
3197
|
+
#createPair() {
|
|
3198
|
+
const pairState = {
|
|
3199
|
+
used: false,
|
|
3200
|
+
hibernationAccepted: false
|
|
3201
|
+
};
|
|
3202
|
+
const left = new MemoryWebSocketEndpoint();
|
|
3203
|
+
const right = new MemoryWebSocketEndpoint();
|
|
3204
|
+
left.peer = right;
|
|
3205
|
+
right.peer = left;
|
|
3206
|
+
return {
|
|
3207
|
+
0: new AcceptedWebSocket(this.#ctx, left, pairState),
|
|
3208
|
+
1: new AcceptedWebSocket(this.#ctx, right, pairState)
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
};
|
|
3212
|
+
function acceptWebSocket(ctx, socket) {
|
|
3213
|
+
const state = socketMetadata(socket);
|
|
3214
|
+
if (state.accepted !== void 0) throw new Error(ALREADY_ACCEPTED_MESSAGE);
|
|
3215
|
+
if (socket instanceof AcceptedWebSocket) {
|
|
3216
|
+
socket.accept();
|
|
3217
|
+
return socket;
|
|
3218
|
+
}
|
|
3219
|
+
state.accepted = { mode: "classic" };
|
|
3220
|
+
return new AcceptedWebSocket(ctx, socket);
|
|
3221
|
+
}
|
|
3222
|
+
function markWebSocketUsed(socket) {
|
|
3223
|
+
if (socket instanceof AcceptedWebSocket) socket.markPairUsed();
|
|
3224
|
+
}
|
|
3225
|
+
function installWebSocketGlobals(target, pairConstructor) {
|
|
3226
|
+
const constructor = globalThis.WebSocket;
|
|
3227
|
+
for (const [name, value] of Object.entries(WEB_SOCKET_READY_STATES)) {
|
|
3228
|
+
defineValue(constructor, name, value);
|
|
3229
|
+
defineValue(constructor.prototype, name, value);
|
|
3230
|
+
}
|
|
3231
|
+
defineValue(constructor.prototype, "serializeAttachment", serializeAttachmentMethod);
|
|
3232
|
+
defineValue(constructor.prototype, "deserializeAttachment", deserializeAttachmentMethod);
|
|
3233
|
+
defineValue(target, "WebSocket", constructor);
|
|
3234
|
+
defineValue(target, "WebSocketPair", pairConstructor);
|
|
3235
|
+
defineValue(target, "WebSocketRequestResponsePair", RuntimeWebSocketRequestResponsePair);
|
|
3236
|
+
}
|
|
3237
|
+
function defineValue(target, name, value) {
|
|
3238
|
+
if (Object.getOwnPropertyDescriptor(target, name)?.configurable === false) return;
|
|
3239
|
+
Object.defineProperty(target, name, {
|
|
3240
|
+
configurable: true,
|
|
3241
|
+
writable: true,
|
|
3242
|
+
value
|
|
3243
|
+
});
|
|
3244
|
+
}
|
|
3245
|
+
function normalizeTags(tags) {
|
|
3246
|
+
if (tags === void 0) return [];
|
|
3247
|
+
if (!Array.isArray(tags)) throw new TypeError("Failed to execute 'acceptWebSocket' on 'DurableObjectState': parameter 2 is not of type 'Array'.");
|
|
3248
|
+
if (tags.length > MAX_TAGS) throw new Error(`a Hibernatable WebSocket cannot have more than ${MAX_TAGS} tags`);
|
|
3249
|
+
const normalized = [...new Set(tags.map(String))];
|
|
3250
|
+
for (const tag of normalized) if (tag.length > MAX_TAG_LENGTH) throw new Error(`"${tag}" is longer than the max tag length (${MAX_TAG_LENGTH} characters).`);
|
|
3251
|
+
return normalized;
|
|
3252
|
+
}
|
|
3253
|
+
function validateAutoResponseSize(side, value) {
|
|
3254
|
+
const bytes = textEncoder.encode(value).byteLength;
|
|
3255
|
+
if (bytes > MAX_AUTO_RESPONSE_BYTES) throw new RangeError(`${side} cannot be larger than ${MAX_AUTO_RESPONSE_BYTES} bytes. A ${side.toLowerCase()} of size ${bytes} was provided.`);
|
|
3256
|
+
}
|
|
3257
|
+
function validateClose(code, reason) {
|
|
3258
|
+
if (code !== void 0 && code !== 1e3 && (code < 3e3 || code > 4999)) throw new DOMException(`Invalid WebSocket close code: ${code}.`, "InvalidAccessError");
|
|
3259
|
+
if (textEncoder.encode(reason).byteLength > MAX_CLOSE_REASON_BYTES) throw new DOMException(`WebSocket close reason must not be longer than ${MAX_CLOSE_REASON_BYTES} bytes when UTF-8 encoded.`, "SyntaxError");
|
|
3260
|
+
}
|
|
3261
|
+
function cloneEventFor(type, event) {
|
|
3262
|
+
if (type === "message") {
|
|
3263
|
+
const source = event;
|
|
3264
|
+
return new MessageEvent("message", {
|
|
3265
|
+
data: source.data,
|
|
3266
|
+
origin: source.origin,
|
|
3267
|
+
lastEventId: source.lastEventId
|
|
3268
|
+
});
|
|
3269
|
+
}
|
|
3270
|
+
if (type === "close") {
|
|
3271
|
+
const source = event;
|
|
3272
|
+
return new CloseEvent("close", {
|
|
3273
|
+
code: source.code,
|
|
3274
|
+
reason: source.reason,
|
|
3275
|
+
wasClean: source.wasClean
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
return new Event(type);
|
|
3279
|
+
}
|
|
3280
|
+
//#endregion
|
|
2630
3281
|
//#region src/api/global-scope.ts
|
|
2631
3282
|
/**
|
|
2632
3283
|
* ← workerd `src/workerd/api/global-scope.{h,c++}` — the alarm half, and the
|
|
@@ -2880,10 +3531,11 @@ var ActorGlobalScope = class {
|
|
|
2880
3531
|
#readCurrentExternalEntry;
|
|
2881
3532
|
scheduler;
|
|
2882
3533
|
crypto;
|
|
2883
|
-
constructor(ctx, options
|
|
3534
|
+
constructor(ctx, options) {
|
|
2884
3535
|
this.#ctx = ctx;
|
|
2885
3536
|
this.#fetch = options.fetch;
|
|
2886
3537
|
this.#readCurrentExternalEntry = options.currentExternalEntry;
|
|
3538
|
+
installWebSocketGlobals(this, options.webSockets.WebSocketPair);
|
|
2887
3539
|
this.scheduler = new Scheduler(this);
|
|
2888
3540
|
this.crypto = new GatedCrypto((op) => {
|
|
2889
3541
|
this.#requireOwnSlice(op);
|
|
@@ -2988,6 +3640,7 @@ var ActorGlobalScope = class {
|
|
|
2988
3640
|
* scope instead. A single-actor host simply writes `() => scope`.
|
|
2989
3641
|
*/
|
|
2990
3642
|
function actorScopeBindings(resolve) {
|
|
3643
|
+
const BoundWebSocketPair = new Proxy(class WebSocketPair {}, { construct: () => new (resolve()).WebSocketPair() });
|
|
2991
3644
|
return {
|
|
2992
3645
|
awaitIo: (promise) => resolve().awaitIo(promise),
|
|
2993
3646
|
scheduler: {
|
|
@@ -3004,6 +3657,9 @@ function actorScopeBindings(resolve) {
|
|
|
3004
3657
|
},
|
|
3005
3658
|
fetch: (input, init) => resolve().fetch(input, init),
|
|
3006
3659
|
crypto: scopeCrypto(resolve),
|
|
3660
|
+
WebSocket: globalThis.WebSocket,
|
|
3661
|
+
WebSocketPair: BoundWebSocketPair,
|
|
3662
|
+
WebSocketRequestResponsePair: RuntimeWebSocketRequestResponsePair,
|
|
3007
3663
|
get currentExternalEntry() {
|
|
3008
3664
|
return resolve().currentExternalEntry;
|
|
3009
3665
|
}
|
|
@@ -3014,7 +3670,7 @@ function actorScopeBindings(resolve) {
|
|
|
3014
3670
|
* an operation actually runs.
|
|
3015
3671
|
*
|
|
3016
3672
|
* That laziness is required rather than tidy, and both lanes proved it. A facet's
|
|
3017
|
-
* module destructures its
|
|
3673
|
+
* module destructures its actor globals at module scope, which is BEFORE its container
|
|
3018
3674
|
* exists — so a `crypto` that resolved on read threw at import. And on the root
|
|
3019
3675
|
* path `globalThis.crypto` is read by things that are not the actor at all: capnweb,
|
|
3020
3676
|
* the sqlite driver, the test runner. So the binding is a pair of plain objects
|
|
@@ -3060,8 +3716,8 @@ var ASYNC_SUBTLE_METHODS = [
|
|
|
3060
3716
|
* one that is not.
|
|
3061
3717
|
*
|
|
3062
3718
|
* **A host should call this rather than assigning the names itself**, and the
|
|
3063
|
-
* reason is the failure it prevents: a host that installs
|
|
3064
|
-
*
|
|
3719
|
+
* reason is the failure it prevents: a host that installs only a subset leaves a
|
|
3720
|
+
* primitive ungated, and an ungated primitive that WORKS is invisible until
|
|
3065
3721
|
* a continuation after it touches storage — possibly never, on the path that
|
|
3066
3722
|
* matters. The set is the package's, so it can grow without every host growing
|
|
3067
3723
|
* with it.
|
|
@@ -3090,153 +3746,6 @@ function installActorScope(target, resolve) {
|
|
|
3090
3746
|
});
|
|
3091
3747
|
}
|
|
3092
3748
|
}
|
|
3093
|
-
//#endregion
|
|
3094
|
-
//#region src/api/web-socket.ts
|
|
3095
|
-
/** ← the `JSG_REQUIRE(!native.state.is<Accepted>(), ...)` at the head of `accept()`. */
|
|
3096
|
-
var ALREADY_ACCEPTED_MESSAGE = "acceptWebSocket(): this socket has already been accepted by an actor. A socket's frames are delivered by exactly one read loop, and a second accept would deliver them under two gates.";
|
|
3097
|
-
/** Sockets this runtime has accepted, so the refusal above is answerable. */
|
|
3098
|
-
var accepted = /* @__PURE__ */ new WeakSet();
|
|
3099
|
-
/** The four events a `WebSocket` dispatches, which `readLoop` and its `.then` cover upstream. */
|
|
3100
|
-
var SOCKET_EVENTS = [
|
|
3101
|
-
"open",
|
|
3102
|
-
"message",
|
|
3103
|
-
"close",
|
|
3104
|
-
"error"
|
|
3105
|
-
];
|
|
3106
|
-
/**
|
|
3107
|
-
* ← `WebSocket::Accepted` (`web-socket.h:~300-360`), reached through
|
|
3108
|
-
* `accept()` → `internalAccept(js, IoContext::current().getCriticalSection())`
|
|
3109
|
-
* → `startReadLoop` (`web-socket.c++:133`, `:426`, `:429-433`, `:507`).
|
|
3110
|
-
*
|
|
3111
|
-
* An `EventTarget`, so a consumer registers listeners the way it would on a real
|
|
3112
|
-
* socket — but on THIS object rather than on the raw one, because this is what
|
|
3113
|
-
* runs them inside a gated slice.
|
|
3114
|
-
*/
|
|
3115
|
-
var AcceptedWebSocket = class extends EventTarget {
|
|
3116
|
-
#ctx;
|
|
3117
|
-
#socket;
|
|
3118
|
-
/**
|
|
3119
|
-
* ← `readLoop`'s `cs` parameter, captured at accept and replayed for every
|
|
3120
|
-
* frame via `mapAddRef(cs)` (`web-socket.c++:1110`). A socket accepted inside
|
|
3121
|
-
* `blockConcurrencyWhile` therefore delivers its messages inside that critical
|
|
3122
|
-
* section — §1.8's second bullet, and the reason this is captured here rather
|
|
3123
|
-
* than read when a frame arrives.
|
|
3124
|
-
*/
|
|
3125
|
-
#criticalSection;
|
|
3126
|
-
/**
|
|
3127
|
-
* ← `OutgoingMessagesMap outgoingMessages` plus `ensurePumping`
|
|
3128
|
-
* (`web-socket.h:582-590`, `web-socket.c++:948-975`), as a chain.
|
|
3129
|
-
*
|
|
3130
|
-
* The table is insertion-ordered and the pump awaits each entry's own
|
|
3131
|
-
* `outputLock` before sending it, so messages leave in order and message N
|
|
3132
|
-
* waits only for the writes outstanding when IT was enqueued. A promise chain
|
|
3133
|
-
* is the same two properties with nothing to schedule.
|
|
3134
|
-
*/
|
|
3135
|
-
#pump = Promise.resolve();
|
|
3136
|
-
onopen = null;
|
|
3137
|
-
onmessage = null;
|
|
3138
|
-
onclose = null;
|
|
3139
|
-
onerror = null;
|
|
3140
|
-
constructor(ctx, socket) {
|
|
3141
|
-
super();
|
|
3142
|
-
this.#ctx = ctx;
|
|
3143
|
-
this.#socket = socket;
|
|
3144
|
-
this.#criticalSection = ctx.getCriticalSection();
|
|
3145
|
-
for (const type of SOCKET_EVENTS) socket.addEventListener(type, (event) => {
|
|
3146
|
-
this.#deliver(type, event);
|
|
3147
|
-
});
|
|
3148
|
-
}
|
|
3149
|
-
/**
|
|
3150
|
-
* ← `co_await context.run([...](auto& wLock) { dispatchEventImpl(...) }, mapAddRef(cs))`
|
|
3151
|
-
* (`web-socket.c++:1065-1110`).
|
|
3152
|
-
*
|
|
3153
|
-
* The run rides `addWaitUntil`, as upstream's read loop does ("We put the read
|
|
3154
|
-
* loop in a `waitUntil`, since there would otherwise be a race condition
|
|
3155
|
-
* between delivering the final close message and the request being canceled",
|
|
3156
|
-
* `web-socket.c++:537-541`). That is also what stops a listener's throw
|
|
3157
|
-
* becoming an unhandled rejection: it lands in `waitUntilStatus()`.
|
|
3158
|
-
*/
|
|
3159
|
-
#deliver(type, event) {
|
|
3160
|
-
this.#ctx.addWaitUntil(this.#ctx.run(() => {
|
|
3161
|
-
const delivered = cloneEventFor(type, event);
|
|
3162
|
-
this.dispatchEvent(delivered);
|
|
3163
|
-
const handler = this[`on${type}`];
|
|
3164
|
-
handler?.(delivered);
|
|
3165
|
-
}, { input: this.#criticalSection }));
|
|
3166
|
-
}
|
|
3167
|
-
/**
|
|
3168
|
-
* ← `WebSocket::send` (`web-socket.c++:~640`), which inserts a
|
|
3169
|
-
* `GatedMessage{IoContext::current().waitForOutputLocksIfNecessary(), …}`.
|
|
3170
|
-
*
|
|
3171
|
-
* Synchronous, as upstream's is: the wait is the pump's, not the caller's. The
|
|
3172
|
-
* output gate is what "blocks all outgoing messages from an actor that would
|
|
3173
|
-
* allow the rest of the world to observe the actor's state" (§1.1), and a
|
|
3174
|
-
* socket frame is exactly such a message.
|
|
3175
|
-
*
|
|
3176
|
-
* `waitForOutputLocksIfNecessary()` collapses to `waitForOutputLocks()` here
|
|
3177
|
-
* for the reason the whole file collapses `kj::Maybe<Worker::Actor&>`: its
|
|
3178
|
-
* body is `actor.map(…)` (`io-context.c++:383-386`) and every context in this
|
|
3179
|
-
* runtime is an actor context.
|
|
3180
|
-
*/
|
|
3181
|
-
send(data) {
|
|
3182
|
-
this.#enqueue(() => {
|
|
3183
|
-
this.#socket.send(data);
|
|
3184
|
-
});
|
|
3185
|
-
}
|
|
3186
|
-
/** ← `WebSocket::close`, which enqueues a `Close` through the same gate. */
|
|
3187
|
-
close(code, reason) {
|
|
3188
|
-
this.#enqueue(() => {
|
|
3189
|
-
this.#socket.close(code, reason);
|
|
3190
|
-
});
|
|
3191
|
-
}
|
|
3192
|
-
#enqueue(write) {
|
|
3193
|
-
const outputLock = this.#ctx.waitForOutputLocks();
|
|
3194
|
-
this.#pump = this.#pump.then(async () => {
|
|
3195
|
-
await outputLock;
|
|
3196
|
-
write();
|
|
3197
|
-
});
|
|
3198
|
-
this.#ctx.addWaitUntil(this.#pump);
|
|
3199
|
-
}
|
|
3200
|
-
};
|
|
3201
|
-
/**
|
|
3202
|
-
* ← `accept()` / `state.acceptWebSocket()`, as the one verb.
|
|
3203
|
-
*
|
|
3204
|
-
* Named for what upstream names it, because the critical-section capture is a
|
|
3205
|
-
* property of accepting rather than of constructing: "a socket accepted inside a
|
|
3206
|
-
* `blockConcurrencyWhile` delivers its messages inside that critical section"
|
|
3207
|
-
* (§1.8).
|
|
3208
|
-
*/
|
|
3209
|
-
function acceptWebSocket(ctx, socket) {
|
|
3210
|
-
if (accepted.has(socket)) throw new Error(ALREADY_ACCEPTED_MESSAGE);
|
|
3211
|
-
accepted.add(socket);
|
|
3212
|
-
return new AcceptedWebSocket(ctx, socket);
|
|
3213
|
-
}
|
|
3214
|
-
/**
|
|
3215
|
-
* An `Event` may be dispatched by exactly one target at a time, so the raw
|
|
3216
|
-
* socket's event object cannot be re-dispatched: `dispatchEvent` on an event
|
|
3217
|
-
* that is already dispatched throws `InvalidStateError`, and one that has
|
|
3218
|
-
* finished carries the raw socket as its `target`. Rebuilding it is what makes
|
|
3219
|
-
* `event.target` the accepted socket, which is what a listener expects.
|
|
3220
|
-
*/
|
|
3221
|
-
function cloneEventFor(type, event) {
|
|
3222
|
-
if (type === "message") {
|
|
3223
|
-
const source = event;
|
|
3224
|
-
return new MessageEvent("message", {
|
|
3225
|
-
data: source.data,
|
|
3226
|
-
origin: source.origin,
|
|
3227
|
-
lastEventId: source.lastEventId
|
|
3228
|
-
});
|
|
3229
|
-
}
|
|
3230
|
-
if (type === "close") {
|
|
3231
|
-
const source = event;
|
|
3232
|
-
return new CloseEvent("close", {
|
|
3233
|
-
code: source.code,
|
|
3234
|
-
reason: source.reason,
|
|
3235
|
-
wasClean: source.wasClean
|
|
3236
|
-
});
|
|
3237
|
-
}
|
|
3238
|
-
return new Event(type);
|
|
3239
|
-
}
|
|
3240
3749
|
/**
|
|
3241
3750
|
* ← the message every unimplemented `ActorCacheInterface` PITR method throws.
|
|
3242
3751
|
* `ActorSqlite` overrides two of the four; the other two keep this.
|
|
@@ -5469,14 +5978,16 @@ var ActorTree = class {
|
|
|
5469
5978
|
* which is the whole mechanism behind §1.10's parent↔child re-entrancy.
|
|
5470
5979
|
*/
|
|
5471
5980
|
var ActorImpl = class {
|
|
5472
|
-
#inputGate
|
|
5473
|
-
#outputGate
|
|
5981
|
+
#inputGate;
|
|
5982
|
+
#outputGate;
|
|
5474
5983
|
#isFacet;
|
|
5475
5984
|
/** Assigned after construction; `storage` is a WXT auto-import in extension bundles. */
|
|
5476
5985
|
actorStorage;
|
|
5477
5986
|
classInstance = { kind: "before-ctor" };
|
|
5478
|
-
constructor(isFacet) {
|
|
5987
|
+
constructor(isFacet, hooks = {}) {
|
|
5479
5988
|
this.#isFacet = isFacet;
|
|
5989
|
+
this.#inputGate = new InputGate(hooks.input);
|
|
5990
|
+
this.#outputGate = new OutputGate(hooks.output);
|
|
5480
5991
|
}
|
|
5481
5992
|
getInputGate() {
|
|
5482
5993
|
return this.#inputGate;
|
|
@@ -5786,6 +6297,7 @@ var ActorContainerImpl = class {
|
|
|
5786
6297
|
#facets;
|
|
5787
6298
|
#tree;
|
|
5788
6299
|
#env;
|
|
6300
|
+
#webSockets;
|
|
5789
6301
|
state;
|
|
5790
6302
|
facetTree;
|
|
5791
6303
|
globals;
|
|
@@ -5793,16 +6305,22 @@ var ActorContainerImpl = class {
|
|
|
5793
6305
|
#alarmTail = Promise.resolve();
|
|
5794
6306
|
constructor(options, db, tree, facetTree) {
|
|
5795
6307
|
const facet = options.facet;
|
|
5796
|
-
this.#actor = new ActorImpl(facet !== void 0);
|
|
6308
|
+
this.#actor = new ActorImpl(facet !== void 0, options.gateHooks);
|
|
5797
6309
|
this.#ctx = new IoContext(this.#actor, options.ports.timer);
|
|
5798
6310
|
this.#env = options.env;
|
|
5799
6311
|
this.#tree = tree;
|
|
5800
6312
|
this.#cache = new ActorSqlite(db, this.#actor.getOutputGate(), async () => {}, facet === void 0 ? options.ports.alarms : DEFAULT_ALARM_OUTLET);
|
|
5801
6313
|
this.#actor.actorStorage = this.#cache;
|
|
5802
6314
|
this.#durableStorage = new DurableObjectStorage(this.#ctx, this.#cache);
|
|
6315
|
+
this.#webSockets = new HibernatableWebSocketRegistry(this.#ctx, {
|
|
6316
|
+
message: (socket, message) => this.#runWebSocketHandler("webSocketMessage", socket, message),
|
|
6317
|
+
close: (socket, code, reason, wasClean) => this.#runWebSocketHandler("webSocketClose", socket, code, reason, wasClean),
|
|
6318
|
+
error: (socket, error) => this.#runWebSocketHandler("webSocketError", socket, error)
|
|
6319
|
+
}, options.ports.hibernation, options.webSockets);
|
|
5803
6320
|
this.globals = new ActorGlobalScope(this.#ctx, {
|
|
5804
6321
|
fetch: options.ports.fetch,
|
|
5805
|
-
currentExternalEntry: () => this.#currentExternalEntry
|
|
6322
|
+
currentExternalEntry: () => this.#currentExternalEntry,
|
|
6323
|
+
webSockets: this.#webSockets
|
|
5806
6324
|
});
|
|
5807
6325
|
this.facetTree = facetTree;
|
|
5808
6326
|
this.#facets = new FacetManagerImpl(this, options.ports.facets, facet?.id ?? 0, facet?.depth ?? 0, this.facetTree);
|
|
@@ -5816,7 +6334,8 @@ var ActorContainerImpl = class {
|
|
|
5816
6334
|
props: void 0,
|
|
5817
6335
|
storage: this.#durableStorage,
|
|
5818
6336
|
facets: this.#facets,
|
|
5819
|
-
globals: actorScopeBindings(() => this.globals)
|
|
6337
|
+
globals: actorScopeBindings(() => this.globals),
|
|
6338
|
+
webSockets: this.#webSockets
|
|
5820
6339
|
});
|
|
5821
6340
|
}
|
|
5822
6341
|
get onBroken() {
|
|
@@ -5950,6 +6469,14 @@ var ActorContainerImpl = class {
|
|
|
5950
6469
|
drainWaitUntil() {
|
|
5951
6470
|
return this.#ctx.drainWaitUntil();
|
|
5952
6471
|
}
|
|
6472
|
+
quiescence() {
|
|
6473
|
+
return {
|
|
6474
|
+
armedTimers: this.#ctx.getTimeoutCount(),
|
|
6475
|
+
pendingWaitUntil: this.#ctx.waitUntilTaskCount(),
|
|
6476
|
+
inputLockHeld: this.#ctx.hasCurrent(),
|
|
6477
|
+
outputGateBroken: this.#ctx.isOutputGateBroken()
|
|
6478
|
+
};
|
|
6479
|
+
}
|
|
5953
6480
|
/** ← `WorkerdApi::compileGlobals`'s `Global::WorkerLoader` arm. */
|
|
5954
6481
|
workerLoader(channel, options) {
|
|
5955
6482
|
return new WorkerLoader(this.#ctx, channel, options);
|
|
@@ -6043,6 +6570,13 @@ var ActorContainerImpl = class {
|
|
|
6043
6570
|
if (!hasAlarmHandler(instance.instance)) throw new TypeError("Your Durable Object class must have an alarm() handler.");
|
|
6044
6571
|
return instance.instance.alarm(new AlarmInvocationInfo(scheduledTime, retryCount));
|
|
6045
6572
|
}
|
|
6573
|
+
#runWebSocketHandler(name, socket, ...args) {
|
|
6574
|
+
const instance = this.#actor.classInstance;
|
|
6575
|
+
if (instance.kind !== "running") return void 0;
|
|
6576
|
+
const handler = Reflect.get(instance.instance, name);
|
|
6577
|
+
if (typeof handler !== "function") return void 0;
|
|
6578
|
+
return Reflect.apply(handler, instance.instance, [socket, ...args]);
|
|
6579
|
+
}
|
|
6046
6580
|
};
|
|
6047
6581
|
/**
|
|
6048
6582
|
* Builds one actor: the two gates, the `IoContext` over them, the storage engine
|
|
@@ -6113,6 +6647,6 @@ function newRpcSession(port, localMain) {
|
|
|
6113
6647
|
return newMessagePortRpcSession(port, localMain);
|
|
6114
6648
|
}
|
|
6115
6649
|
//#endregion
|
|
6116
|
-
export { ACTOR_CLASS_SERIALIZATION_UNIMPLEMENTED_MESSAGE, ALARM_RETRY_MAX_TRIES, ALARM_RETRY_START_SECONDS, ALLOW_EXPERIMENTAL_MESSAGE, ALREADY_ACCEPTED_MESSAGE, AlarmInvocationInfo, AlarmScheduler, BYOB_READER_UNGATABLE_MESSAGE, BrokenActorError, CanceledError, DEAD_LOAD_CONTEXT_MESSAGE, DEFAULT_ALARM_OUTLET, FACET_ALARM_UNIMPLEMENTED_MESSAGE, FACET_NAME_MAX_LENGTH, FACET_TREE_MAX_DEPTH, FOREIGN_SLICE_MESSAGE,
|
|
6650
|
+
export { ACTOR_CLASS_SERIALIZATION_UNIMPLEMENTED_MESSAGE, ALARM_RETRY_MAX_TRIES, ALARM_RETRY_START_SECONDS, ALLOW_EXPERIMENTAL_MESSAGE, ALREADY_ACCEPTED_MESSAGE, AlarmInvocationInfo, AlarmScheduler, BYOB_READER_UNGATABLE_MESSAGE, BrokenActorError, CanceledError, DEAD_LOAD_CONTEXT_MESSAGE, DEFAULT_ALARM_OUTLET, FACET_ALARM_UNIMPLEMENTED_MESSAGE, FACET_NAME_MAX_LENGTH, FACET_TREE_MAX_DEPTH, FOREIGN_SLICE_MESSAGE, LoopbackDurableObjectClass, NOT_BYTES_MESSAGE, NO_GLOBAL_OUTBOUND_MESSAGE, NO_MODULES_MESSAGE, PITR_UNIMPLEMENTED_MESSAGE, REPLICATION_UNIMPLEMENTED_MESSAGE, RETRY_BACKOFF_MAX, RETRY_JITTER_FACTOR, STREAMING_TAILS_EXPERIMENTAL_MESSAGE, RuntimeWebSocketRequestResponsePair as WebSocketRequestResponsePair, WorkerLoader, WorkerStub, actorScopeBindings, alarmRetryDelayMs, asLoopbackDurableObjectClass, createActorContainer, createDurableObjectNamespace, gateRequestBody, installActorScope, installWebSocketGlobals, jsModuleInPythonWorkerMessage, markWebSocketUsed, moduleFieldCountMessage, moduleNameMessage, newRpcSession, noFacets, notSerializableMessage, pythonModuleInJsWorkerMessage, typeScriptModuleNameMessage };
|
|
6117
6651
|
|
|
6118
6652
|
//# sourceMappingURL=index.js.map
|