@mcp-b/do-runtime 0.5.0 → 0.6.1
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 +18 -0
- package/README.md +30 -3
- package/dist/browser.js +55 -0
- package/dist/browser.js.map +1 -0
- 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/chunks/web-socket-W63BdFn5.js +2574 -0
- package/dist/chunks/web-socket-W63BdFn5.js.map +1 -0
- 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 +183 -2310
- 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/web-socket.d.ts +88 -73
- package/dist/src/browser.d.ts +15 -0
- package/dist/src/index.d.ts +6 -4
- package/dist/src/io/io-context.d.ts +1 -0
- package/dist/src/server/actor-container.d.ts +16 -2
- package/dist/src/server/hibernation-mirror.d.ts +16 -0
- package/package.json +5 -1
|
@@ -0,0 +1,2574 @@
|
|
|
1
|
+
import { c as setUserErrorDetail, s as requireInputLock } from "./io-context-BBgKEsdR.js";
|
|
2
|
+
import { deserialize, serialize } from "@ungap/structured-clone";
|
|
3
|
+
//#region src/api/actor.ts
|
|
4
|
+
/**
|
|
5
|
+
* ← the `[1, 2048]` bound in `ColoLocalActorNamespace::get`.
|
|
6
|
+
*
|
|
7
|
+
* Upstream compares `actorId.size()`, which for a `kj::String` is **bytes**, so
|
|
8
|
+
* this is measured in UTF-8 bytes rather than in UTF-16 code units. That costs
|
|
9
|
+
* one `TextEncoder` pass and buys an exact match on a bound a caller can hit.
|
|
10
|
+
*/
|
|
11
|
+
var MAX_COLO_LOCAL_ACTOR_ID_BYTES = 2048;
|
|
12
|
+
/**
|
|
13
|
+
* Upstream never faces this: JSG unwraps a `jsg::Ref<DurableObjectId>` parameter
|
|
14
|
+
* and throws a `TypeError` before the method body runs, so `getInner()` cannot be
|
|
15
|
+
* reached on something that is not one. Here the parameter type is
|
|
16
|
+
* workers-types' structural `DurableObjectId` interface, which any object with a
|
|
17
|
+
* `toString` and an `equals` satisfies — so the unwrap has to be written, and it
|
|
18
|
+
* fails closed rather than guessing at the string form.
|
|
19
|
+
*/
|
|
20
|
+
var FOREIGN_ACTOR_ID_MESSAGE = "This DurableObjectId was not created by this runtime, so its underlying actor id cannot be read. Ids must come from newUniqueId(), idFromName() or idFromString() on a DurableObjectNamespace.";
|
|
21
|
+
/** Substrate boundary: `jsg::Serializer`, `Frankenvalue` and channel tokens have no port. */
|
|
22
|
+
var ACTOR_CLASS_SERIALIZATION_UNIMPLEMENTED_MESSAGE = "DurableObjectClass cannot be serialized in this runtime: upstream writes a channel token through jsg::Serializer and Frankenvalue, and neither the token nor the serializer has an equivalent here.";
|
|
23
|
+
/**
|
|
24
|
+
* Replication is a named substrate boundary (`io/actor-cache.ts`), so no request
|
|
25
|
+
* this file builds asks for replica routing. Upstream reads
|
|
26
|
+
* `FeatureFlags::get(js).getReplicaRouting()` here.
|
|
27
|
+
*/
|
|
28
|
+
var ENABLE_REPLICA_ROUTING = false;
|
|
29
|
+
/**
|
|
30
|
+
* ← `ColoLocalActorNamespace` (`actor.h:25-37`). "A capability to an ephemeral
|
|
31
|
+
* Actor namespace."
|
|
32
|
+
*/
|
|
33
|
+
var ColoLocalActorNamespace = class {
|
|
34
|
+
#channel;
|
|
35
|
+
constructor(channel) {
|
|
36
|
+
this.#channel = channel;
|
|
37
|
+
}
|
|
38
|
+
/** ← `ColoLocalActorNamespace::get` (`actor.c++:116-129`). */
|
|
39
|
+
get(actorId) {
|
|
40
|
+
const bytes = utf8Length(actorId);
|
|
41
|
+
if (!(bytes > 0 && bytes <= 2048)) throw new TypeError(`Actor ID length must be in the range [1, ${MAX_COLO_LOCAL_ACTOR_ID_BYTES}].`);
|
|
42
|
+
return this.#channel.getColoLocalActor({ actorId });
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var textEncoder$2 = new TextEncoder();
|
|
46
|
+
/** `kj::String::size()` is bytes; `String.prototype.length` is UTF-16 code units. */
|
|
47
|
+
function utf8Length(value) {
|
|
48
|
+
return textEncoder$2.encode(value).length;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* ← `DurableObjectId` (`actor.h:42-84`). "DurableObjectId type seen by
|
|
52
|
+
* JavaScript."
|
|
53
|
+
*
|
|
54
|
+
* `name` and `jurisdiction` are read from the inner id **once, at construction**,
|
|
55
|
+
* where upstream's are `JSG_READONLY_INSTANCE_PROPERTY`s that re-read it on every
|
|
56
|
+
* access. That is not a preference: `@cloudflare/workers-types` declares both
|
|
57
|
+
* `readonly name?: string`, and under `exactOptionalPropertyTypes` a getter
|
|
58
|
+
* returning `string | undefined` does not satisfy an optional `string`. An own
|
|
59
|
+
* property assigned only when the value exists does, and it is what keeps this
|
|
60
|
+
* class assignable to the interface with no cast (§2.4). The one behaviour lost
|
|
61
|
+
* is `ActorIdImpl::clearName()` (`server/actor-id-impl.h`) taking effect on an
|
|
62
|
+
* already-wrapped id — a `server/`-internal that runs before the id reaches JS.
|
|
63
|
+
*/
|
|
64
|
+
var DurableObjectId = class {
|
|
65
|
+
#id;
|
|
66
|
+
name;
|
|
67
|
+
jurisdiction;
|
|
68
|
+
constructor(id) {
|
|
69
|
+
this.#id = id;
|
|
70
|
+
const name = id.getName();
|
|
71
|
+
if (name !== void 0) this.name = name;
|
|
72
|
+
const jurisdiction = id.getJurisdiction();
|
|
73
|
+
if (jurisdiction !== void 0) this.jurisdiction = jurisdiction;
|
|
74
|
+
}
|
|
75
|
+
/** ← `getInner()`. Not JS-visible upstream either; the outgoing factories take it. */
|
|
76
|
+
getInner() {
|
|
77
|
+
return this.#id;
|
|
78
|
+
}
|
|
79
|
+
/** "Converts to a string which can be passed back to the constructor to reproduce the same ID." */
|
|
80
|
+
toString() {
|
|
81
|
+
return this.#id.toString();
|
|
82
|
+
}
|
|
83
|
+
equals(other) {
|
|
84
|
+
return this.#id.equals(innerIdOf(other));
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
/** The unwrap JSG performs for a `jsg::Ref<DurableObjectId>` parameter. */
|
|
88
|
+
function requireDurableObjectId(id) {
|
|
89
|
+
if (id instanceof DurableObjectId) return id;
|
|
90
|
+
throw new TypeError(FOREIGN_ACTOR_ID_MESSAGE);
|
|
91
|
+
}
|
|
92
|
+
function innerIdOf(id) {
|
|
93
|
+
return requireDurableObjectId(id).getInner();
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* ← `DurableObject` (`actor.h:87-139`). "Stub object used to send messages to a
|
|
97
|
+
* remote durable object."
|
|
98
|
+
*
|
|
99
|
+
* Upstream's carries its whole behaviour by `JSG_INHERIT(Fetcher)` and adds
|
|
100
|
+
* exactly two readonly properties. So does this: the `Fetcher` is the transport's
|
|
101
|
+
* and everything except `id` and `name` belongs to it. `asDurableObjectStub`
|
|
102
|
+
* below is where the inheritance goes.
|
|
103
|
+
*/
|
|
104
|
+
var DurableObject = class {
|
|
105
|
+
#id;
|
|
106
|
+
#fetcher;
|
|
107
|
+
constructor(id, fetcher) {
|
|
108
|
+
this.#id = id;
|
|
109
|
+
this.#fetcher = fetcher;
|
|
110
|
+
}
|
|
111
|
+
/** ← `JSG_READONLY_INSTANCE_PROPERTY(id, getId)`. */
|
|
112
|
+
getId() {
|
|
113
|
+
return this.#id;
|
|
114
|
+
}
|
|
115
|
+
/** ← `JSG_READONLY_INSTANCE_PROPERTY(name, getName)`. */
|
|
116
|
+
getName() {
|
|
117
|
+
return this.#id.name;
|
|
118
|
+
}
|
|
119
|
+
/** The `Fetcher` upstream inherits from rather than holds. */
|
|
120
|
+
getFetcher() {
|
|
121
|
+
return this.#fetcher;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* ← `js.alloc<DurableObject>(...)` plus `JSG_INHERIT(Fetcher)` plus the
|
|
126
|
+
* `JSG_TS_OVERRIDE` that renames the resource type to `DurableObjectStub`.
|
|
127
|
+
*
|
|
128
|
+
* The named assertion is the same one `io/worker.ts`'s `asFacetStub` makes and
|
|
129
|
+
* for the same reason: `DurableObjectStub<T>` is `Fetcher<T, …> & { id, name }`,
|
|
130
|
+
* and `Fetcher<T>` for an unresolved `T` is `Rpc.Provider<T, …>`, a conditional
|
|
131
|
+
* type TypeScript defers until `T` is known — where `T` is the caller's claim
|
|
132
|
+
* about a class it named, which no value can confirm. Upstream is in the same
|
|
133
|
+
* position and answers it the same way, with the parameter living only inside a
|
|
134
|
+
* `JSG_TS_OVERRIDE`.
|
|
135
|
+
*
|
|
136
|
+
* The `Proxy` is what JSG inheritance costs in JS. Two properties have to answer
|
|
137
|
+
* from the id and every other property — `fetch`, `connect`, and every RPC method
|
|
138
|
+
* name, which are the whole point of a stub — has to reach the transport with
|
|
139
|
+
* `this` still bound to it. Bound methods are memoised so `stub.foo === stub.foo`,
|
|
140
|
+
* which upstream gets for free by there being one object rather than two.
|
|
141
|
+
*/
|
|
142
|
+
function asDurableObjectStub(object) {
|
|
143
|
+
const fetcher = object.getFetcher();
|
|
144
|
+
const bound = /* @__PURE__ */ new Map();
|
|
145
|
+
return new Proxy(fetcher, {
|
|
146
|
+
get(target, property) {
|
|
147
|
+
if (property === "id") return object.getId();
|
|
148
|
+
if (property === "name") return object.getName();
|
|
149
|
+
const cached = bound.get(property);
|
|
150
|
+
if (cached !== void 0) return cached;
|
|
151
|
+
const value = Reflect.get(target, property, target);
|
|
152
|
+
if (typeof value !== "function") return value;
|
|
153
|
+
const method = value.bind(target);
|
|
154
|
+
bound.set(property, method);
|
|
155
|
+
return method;
|
|
156
|
+
},
|
|
157
|
+
has(target, property) {
|
|
158
|
+
if (property === "id" || property === "name") return true;
|
|
159
|
+
return Reflect.has(target, property);
|
|
160
|
+
},
|
|
161
|
+
ownKeys(target) {
|
|
162
|
+
return [
|
|
163
|
+
"id",
|
|
164
|
+
"name",
|
|
165
|
+
...Reflect.ownKeys(target).filter((key) => key !== "id" && key !== "name")
|
|
166
|
+
];
|
|
167
|
+
},
|
|
168
|
+
getOwnPropertyDescriptor(target, property) {
|
|
169
|
+
if (property === "id" || property === "name") return {
|
|
170
|
+
value: property === "id" ? object.getId() : object.getName(),
|
|
171
|
+
writable: false,
|
|
172
|
+
enumerable: true,
|
|
173
|
+
configurable: true
|
|
174
|
+
};
|
|
175
|
+
return Reflect.getOwnPropertyDescriptor(target, property);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* ← `DurableObjectNamespace` (`actor.h:142-291`). "Global durable object class
|
|
181
|
+
* binding type."
|
|
182
|
+
*/
|
|
183
|
+
var DurableObjectNamespace = class DurableObjectNamespace {
|
|
184
|
+
#channel;
|
|
185
|
+
#idFactory;
|
|
186
|
+
constructor(channel, idFactory) {
|
|
187
|
+
this.#channel = channel;
|
|
188
|
+
this.#idFactory = idFactory;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* "Create a new unique ID for a durable object that will be allocated nearby
|
|
192
|
+
* the calling colo."
|
|
193
|
+
*/
|
|
194
|
+
newUniqueId(options) {
|
|
195
|
+
return new DurableObjectId(this.#idFactory.newUniqueId(options?.jurisdiction ?? void 0));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* "Create a name-derived ID. Passing in the same `name` (to the same class)
|
|
199
|
+
* will always produce the same ID."
|
|
200
|
+
*/
|
|
201
|
+
idFromName(name) {
|
|
202
|
+
return new DurableObjectId(this.#idFactory.idFromName(name));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* "Create a DurableObjectId from the stringified form of the ID (as produced by
|
|
206
|
+
* calling `toString()` on a durable object ID). Throws if the ID is not a
|
|
207
|
+
* 64-digit hex number, or if the ID was not originally created for this class."
|
|
208
|
+
*/
|
|
209
|
+
idFromString(id) {
|
|
210
|
+
return new DurableObjectId(this.#idFactory.idFromString(id));
|
|
211
|
+
}
|
|
212
|
+
/** "Gets a durable object by ID or creates it if it doesn't already exist." */
|
|
213
|
+
get(id, options) {
|
|
214
|
+
return this.#getImpl("GET_OR_CREATE", id, options);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* "Gets a durable object by name or creates it if it doesn't already exist.
|
|
218
|
+
* Short for `idFromName()` followed by `get()`."
|
|
219
|
+
*/
|
|
220
|
+
getByName(name, options) {
|
|
221
|
+
return this.#getImpl("GET_OR_CREATE", this.idFromName(name), options);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* "Experimental. Gets a durable object by ID if it already exists. Currently,
|
|
225
|
+
* gated for use by cloudflare only."
|
|
226
|
+
*
|
|
227
|
+
* Upstream exposes it only when the `durableObjectGetExisting` compat flag is
|
|
228
|
+
* on, and `@cloudflare/workers-types` 4.20260702.1 does not declare it. It is
|
|
229
|
+
* exposed unconditionally here, which is the current-behaviour reading every
|
|
230
|
+
* other compat flag in this file gets.
|
|
231
|
+
*/
|
|
232
|
+
getExisting(id, options) {
|
|
233
|
+
return this.#getImpl("GET_EXISTING", id, options);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* "Creates a subnamespace with the jurisdiction hardcoded."
|
|
237
|
+
*
|
|
238
|
+
* The argument is optional because upstream's is a
|
|
239
|
+
* `jsg::Optional<kj::Maybe<kj::String>>`, so both "omitted" and "null" mean the
|
|
240
|
+
* same thing — `cloneWithJurisdiction(kj::none)`, a subnamespace with none.
|
|
241
|
+
*/
|
|
242
|
+
jurisdiction(jurisdiction) {
|
|
243
|
+
return new DurableObjectNamespace(this.#channel, this.#idFactory.cloneWithJurisdiction(jurisdiction ?? void 0));
|
|
244
|
+
}
|
|
245
|
+
/** ← `DurableObjectNamespace::getImpl` (`actor.c++:167-213`). */
|
|
246
|
+
#getImpl(mode, id, options) {
|
|
247
|
+
const durableObjectId = requireDurableObjectId(id);
|
|
248
|
+
const inner = durableObjectId.getInner();
|
|
249
|
+
if (!this.#idFactory.matchesJurisdiction(inner)) throw new TypeError("get called on jurisdictional subnamespace with an ID from a different jurisdiction");
|
|
250
|
+
let routingMode = "DEFAULT";
|
|
251
|
+
const requestedRoutingMode = options?.routingMode;
|
|
252
|
+
if (requestedRoutingMode !== void 0) {
|
|
253
|
+
if (requestedRoutingMode !== "primary-only") throw new RangeError(`unknown routingMode: ${requestedRoutingMode}`);
|
|
254
|
+
routingMode = "PRIMARY_ONLY";
|
|
255
|
+
}
|
|
256
|
+
return asDurableObjectStub(new DurableObject(durableObjectId, this.#channel.getGlobalActor({
|
|
257
|
+
id: inner,
|
|
258
|
+
locationHint: options?.locationHint,
|
|
259
|
+
mode,
|
|
260
|
+
enableReplicaRouting: ENABLE_REPLICA_ROUTING,
|
|
261
|
+
routingMode,
|
|
262
|
+
version: actorVersionOf(options?.version)
|
|
263
|
+
})));
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* ← `version = ActorVersion{.cohort = kj::mv(v.cohort)}` (`actor.c++:186-190`),
|
|
268
|
+
* behind `FeatureFlags::get(js).getEnableVersionApi()` which this file reads as
|
|
269
|
+
* on. A version with no cohort is still a version, which is why the empty object
|
|
270
|
+
* is not collapsed to `undefined`.
|
|
271
|
+
*/
|
|
272
|
+
function actorVersionOf(version) {
|
|
273
|
+
if (version === void 0) return void 0;
|
|
274
|
+
return version.cohort === void 0 ? {} : { cohort: version.cohort };
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* ← `DurableObjectClass` (`actor.h:367-393`). "DurableObjectClass represents a
|
|
278
|
+
* binding to a Durable Object class that can be used as a facet. The only use of
|
|
279
|
+
* this type is to pass to `ctx.facets.get()`."
|
|
280
|
+
*
|
|
281
|
+
* `getChannel()` takes no `IoContext` because the parameter existed to resolve the
|
|
282
|
+
* numbered-channel arm, and there is no numbered-channel arm here.
|
|
283
|
+
*/
|
|
284
|
+
var DurableObjectClass = class {
|
|
285
|
+
#channel;
|
|
286
|
+
constructor(channel) {
|
|
287
|
+
this.#channel = channel;
|
|
288
|
+
}
|
|
289
|
+
/** ← `DurableObjectClass::getChannel` (`actor.c++:232-242`). */
|
|
290
|
+
getChannel() {
|
|
291
|
+
return this.#channel;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* ← `DurableObjectClass::serialize` (`actor.c++:244-306`). Substrate boundary.
|
|
295
|
+
*
|
|
296
|
+
* `requireAllowsTransfer()` runs first, exactly as upstream's does, so a class
|
|
297
|
+
* that refuses transfer reports that rather than the boundary — the refusal is
|
|
298
|
+
* the more specific answer and it is the one upstream would give too.
|
|
299
|
+
*/
|
|
300
|
+
serialize() {
|
|
301
|
+
this.#channel.requireAllowsTransfer();
|
|
302
|
+
throw new Error(ACTOR_CLASS_SERIALIZATION_UNIMPLEMENTED_MESSAGE);
|
|
303
|
+
}
|
|
304
|
+
/** ← `DurableObjectClass::deserialize` (`actor.c++:308-359`). Substrate boundary. */
|
|
305
|
+
static deserialize() {
|
|
306
|
+
throw new Error(ACTOR_CLASS_SERIALIZATION_UNIMPLEMENTED_MESSAGE);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
//#endregion
|
|
310
|
+
//#region src/api/export-loopback.ts
|
|
311
|
+
/**
|
|
312
|
+
* ← what JSG's struct unwrapper does with a value that is not an object
|
|
313
|
+
* (`jsg/struct.h:246`). Undefined and null are **not** in that set: a struct
|
|
314
|
+
* whose every field is optional — which both option structs here are — unwraps
|
|
315
|
+
* from either as an empty struct (`jsg/struct.h:236-243`), so `ctx.exports.Foo()`
|
|
316
|
+
* is upstream's own empty-options call and not an error.
|
|
317
|
+
*/
|
|
318
|
+
var LOOPBACK_OPTIONS_NOT_AN_OBJECT_MESSAGE = "A ctx.exports binding is invoked with an options object: pass { props }, or nothing at all.";
|
|
319
|
+
/** ← what JSG does unwrapping a `jsg::JsRef<jsg::JsObject>` from a non-object. */
|
|
320
|
+
var LOOPBACK_PROPS_NOT_AN_OBJECT_MESSAGE = "`props` must be an object. Upstream unwraps it as a jsg::JsObject, which refuses anything else.";
|
|
321
|
+
/**
|
|
322
|
+
* ← `LoopbackServiceStub` (`export-loopback.h:18-109`).
|
|
323
|
+
*
|
|
324
|
+
* Upstream is a `Fetcher` on the loopback channel and holds the channel number a
|
|
325
|
+
* second time so `callImpl` can re-specialize it. Here the `Fetcher` is the
|
|
326
|
+
* transport's — `api/http.{h,c++}` is not ported — so the unspecialized stub is
|
|
327
|
+
* what the factory returns for a request with no props and no version, and the
|
|
328
|
+
* factory is the thing held twice over.
|
|
329
|
+
*/
|
|
330
|
+
var LoopbackServiceStub = class {
|
|
331
|
+
#channel;
|
|
332
|
+
#fetcher;
|
|
333
|
+
constructor(channel) {
|
|
334
|
+
this.#channel = channel;
|
|
335
|
+
this.#fetcher = channel.getSubrequestChannel({
|
|
336
|
+
props: void 0,
|
|
337
|
+
version: void 0
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/** The `Fetcher` upstream inherits from rather than holds, as `DurableObject`'s is. */
|
|
341
|
+
getFetcher() {
|
|
342
|
+
return this.#fetcher;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* ← `LoopbackServiceStub::callImpl` (`export-loopback.c++:11-29`) reached
|
|
346
|
+
* through `callWithVersion` (`export-loopback.h:53-55`), which is the callable
|
|
347
|
+
* when `enableVersionApi` is on. "Create a specialized Fetcher which can be
|
|
348
|
+
* passed over RPC."
|
|
349
|
+
*/
|
|
350
|
+
callWithVersion(options) {
|
|
351
|
+
return this.#channel.getSubrequestChannel({
|
|
352
|
+
props: requireProps(options.props),
|
|
353
|
+
version: versionRequestOf(options.version)
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
/**
|
|
358
|
+
* ← `LoopbackDurableObjectClass` (`export-loopback.h:116-148`). "Similar to
|
|
359
|
+
* LoopbackServiceStub, but for actor classes … this is used for actor classes
|
|
360
|
+
* that do *not* have any storage configured. If you simply export a class
|
|
361
|
+
* extending `DurableObject` but you don't configure storage for it, it shows up
|
|
362
|
+
* in `ctx.exports` as this type. This can be used to create a Durable Object
|
|
363
|
+
* facet."
|
|
364
|
+
*
|
|
365
|
+
* Upstream's base `DurableObjectClass` holds the channel *number*, and
|
|
366
|
+
* `getChannel(ioctx)` resolves it lazily. There is no numbered arm here, so the
|
|
367
|
+
* unspecialized channel is requested once, in the constructor — which is the same
|
|
368
|
+
* value `getActorClass(channel)` with default props would have produced.
|
|
369
|
+
*/
|
|
370
|
+
var LoopbackDurableObjectClass = class extends DurableObjectClass {
|
|
371
|
+
#channel;
|
|
372
|
+
constructor(channel) {
|
|
373
|
+
super(channel.getActorClass({ props: void 0 }));
|
|
374
|
+
this.#channel = channel;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* ← `LoopbackDurableObjectClass::call` (`export-loopback.c++:31-40`). "Create a
|
|
378
|
+
* specialized DurableObjectClass which can be passed over RPC."
|
|
379
|
+
*
|
|
380
|
+
* The result is a plain `DurableObjectClass`, as `js.alloc<DurableObjectClass>`
|
|
381
|
+
* is: specializing a loopback class does not produce another loopback class.
|
|
382
|
+
*/
|
|
383
|
+
call(options) {
|
|
384
|
+
return new DurableObjectClass(this.#channel.getActorClass({ props: requireProps(options.props) }));
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
function asLoopbackDurableObjectClass(actorClass) {
|
|
388
|
+
return asCallable({
|
|
389
|
+
properties: actorClass,
|
|
390
|
+
prototype: Object.getPrototypeOf(actorClass),
|
|
391
|
+
call: (options) => actorClass.call(requireOptions(options))
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* ← `LoopbackDurableObjectNamespace` (`export-loopback.h:155-189`).
|
|
396
|
+
*
|
|
397
|
+
* Upstream: "used when the class has storage configured. In this case, we want a
|
|
398
|
+
* binding that behaves *both* like a LoopbackDurableObjectClass *and* like a
|
|
399
|
+
* DurableObjectNamespace binding. Easy enough, we'll inherit
|
|
400
|
+
* DurableObjectNamespace, but also make the binding invokable as a function like
|
|
401
|
+
* LoopbackDurableObjectClass."
|
|
402
|
+
*/
|
|
403
|
+
var LoopbackDurableObjectNamespace = class extends DurableObjectNamespace {
|
|
404
|
+
#loopbackClass;
|
|
405
|
+
constructor(channel, idFactory, loopbackClass) {
|
|
406
|
+
super(channel, idFactory);
|
|
407
|
+
this.#loopbackClass = loopbackClass;
|
|
408
|
+
}
|
|
409
|
+
/** ← `getClass()`. "getClass() accessor for use from C++ only." */
|
|
410
|
+
getClass() {
|
|
411
|
+
return this.#loopbackClass;
|
|
412
|
+
}
|
|
413
|
+
/** ← `call`. "Invoking the binding creates a specialization of the class -- not the namespace." */
|
|
414
|
+
call(options) {
|
|
415
|
+
return this.#loopbackClass.call(options);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
/**
|
|
419
|
+
* ← `LoopbackColoLocalActorNamespace` (`export-loopback.h:192-220`). "Like
|
|
420
|
+
* LoopbackDurableObjectNamespace, but for colo-local (ephemeral) actor
|
|
421
|
+
* namespaces."
|
|
422
|
+
*/
|
|
423
|
+
var LoopbackColoLocalActorNamespace = class extends ColoLocalActorNamespace {
|
|
424
|
+
#loopbackClass;
|
|
425
|
+
constructor(channel, loopbackClass) {
|
|
426
|
+
super(channel);
|
|
427
|
+
this.#loopbackClass = loopbackClass;
|
|
428
|
+
}
|
|
429
|
+
/** ← `getClass()`. "getClass() accessor for use from C++ only." */
|
|
430
|
+
getClass() {
|
|
431
|
+
return this.#loopbackClass;
|
|
432
|
+
}
|
|
433
|
+
/** ← `call`. "Invoking the binding creates a specialization of the class -- not the namespace." */
|
|
434
|
+
call(options) {
|
|
435
|
+
return this.#loopbackClass.call(options);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
/**
|
|
439
|
+
* ← `JSG_CALLABLE`, which makes a JSG resource object invocable while leaving
|
|
440
|
+
* every other property answering from the resource type.
|
|
441
|
+
*
|
|
442
|
+
* `properties` is where reads land, with `this` bound to it so a method reaching
|
|
443
|
+
* a private field still finds one — the same binding, memoised the same way,
|
|
444
|
+
* that `asDurableObjectStub` performs for `JSG_INHERIT`. `prototype` is what
|
|
445
|
+
* `instanceof` sees, and it is separate from `properties` because
|
|
446
|
+
* `LoopbackServiceStub` is one object with two halves: upstream's identity is the
|
|
447
|
+
* resource type while its behaviour is the inherited `Fetcher`'s.
|
|
448
|
+
*
|
|
449
|
+
* The target is an arrow function rather than a plain one because a plain
|
|
450
|
+
* function has a non-configurable own `prototype` property, which a Proxy may not
|
|
451
|
+
* hide from `ownKeys`.
|
|
452
|
+
*
|
|
453
|
+
* This is the one assertion the four producers above need, made once here. It is
|
|
454
|
+
* `asDurableObjectStub`'s, for `asDurableObjectStub`'s reason: the declared value
|
|
455
|
+
* is a `Fetcher<T>` or a `DurableObjectClass<T>` intersected with a call
|
|
456
|
+
* signature, and `T` is the caller's claim about a class it named, which no value
|
|
457
|
+
* can confirm. Upstream states the same shapes the same way, in a
|
|
458
|
+
* `JSG_TS_OVERRIDE` that no C++ value is checked against either.
|
|
459
|
+
*/
|
|
460
|
+
var INVOCATION_METHODS = /* @__PURE__ */ new Set([
|
|
461
|
+
"call",
|
|
462
|
+
"apply",
|
|
463
|
+
"bind"
|
|
464
|
+
]);
|
|
465
|
+
function asCallable(facade) {
|
|
466
|
+
const bound = /* @__PURE__ */ new Map();
|
|
467
|
+
const target = () => {
|
|
468
|
+
throw new Error("unreachable: the apply trap answers every invocation");
|
|
469
|
+
};
|
|
470
|
+
return new Proxy(target, {
|
|
471
|
+
apply(_target, _thisArg, args) {
|
|
472
|
+
return facade.call(args[0]);
|
|
473
|
+
},
|
|
474
|
+
get(target, property, receiver) {
|
|
475
|
+
if (INVOCATION_METHODS.has(property)) return Reflect.get(target, property, receiver);
|
|
476
|
+
const cached = bound.get(property);
|
|
477
|
+
if (cached !== void 0) return cached;
|
|
478
|
+
const value = Reflect.get(facade.properties, property, facade.properties);
|
|
479
|
+
if (typeof value !== "function") return value;
|
|
480
|
+
const method = value.bind(facade.properties);
|
|
481
|
+
bound.set(property, method);
|
|
482
|
+
return method;
|
|
483
|
+
},
|
|
484
|
+
has(_target, property) {
|
|
485
|
+
return Reflect.has(facade.properties, property);
|
|
486
|
+
},
|
|
487
|
+
ownKeys() {
|
|
488
|
+
return Reflect.ownKeys(facade.properties);
|
|
489
|
+
},
|
|
490
|
+
getOwnPropertyDescriptor(_target, property) {
|
|
491
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(facade.properties, property);
|
|
492
|
+
if (descriptor === void 0) return void 0;
|
|
493
|
+
return {
|
|
494
|
+
...descriptor,
|
|
495
|
+
configurable: true
|
|
496
|
+
};
|
|
497
|
+
},
|
|
498
|
+
getPrototypeOf() {
|
|
499
|
+
return facade.prototype;
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* ← JSG's struct unwrapper (`jsg/struct.h:236-246`), for the two option structs
|
|
505
|
+
* here: every field of both is optional, so undefined and null yield an empty
|
|
506
|
+
* struct and anything that is not an object is a `TypeError`. V8's `IsObject()`
|
|
507
|
+
* is true for functions, which is why one is not refused here either.
|
|
508
|
+
*
|
|
509
|
+
* `cohort` is not checked, because upstream does not check it: JSG's `kj::String`
|
|
510
|
+
* unwrapper calls `ToString` on whatever it is given (`jsg/value.h:501-506`), so
|
|
511
|
+
* refusing a non-string here would refuse what workerd coerces. That is the same
|
|
512
|
+
* reading `api/actor.ts`'s `actorVersionOf` already takes of the same field.
|
|
513
|
+
*/
|
|
514
|
+
function requireOptions(options) {
|
|
515
|
+
if (options !== void 0 && options !== null && typeof options !== "object" && typeof options !== "function") throw new TypeError(LOOPBACK_OPTIONS_NOT_AN_OBJECT_MESSAGE);
|
|
516
|
+
return options ?? {};
|
|
517
|
+
}
|
|
518
|
+
/** ← `jsg::Optional<jsg::JsRef<jsg::JsObject>> props` — present means an object. */
|
|
519
|
+
function requireProps(props) {
|
|
520
|
+
if (props === void 0) return void 0;
|
|
521
|
+
if (props === null || typeof props !== "object" && typeof props !== "function") throw new TypeError(LOOPBACK_PROPS_NOT_AN_OBJECT_MESSAGE);
|
|
522
|
+
return props;
|
|
523
|
+
}
|
|
524
|
+
/** ← `.cohort = kj::mv(version.cohort).orDefault(kj::none)` (`export-loopback.c++:19-23`). */
|
|
525
|
+
function versionRequestOf(version) {
|
|
526
|
+
if (version === void 0) return void 0;
|
|
527
|
+
return { cohort: version.cohort ?? void 0 };
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/api/sql.ts
|
|
531
|
+
/**
|
|
532
|
+
* ← workerd `src/workerd/api/sql.{h,c++}`
|
|
533
|
+
*
|
|
534
|
+
* `SqlStorage` and its two nested types. ~330 call sites depend on this — it is
|
|
535
|
+
* the real storage layer, not KV.
|
|
536
|
+
*
|
|
537
|
+
* Four things about the translation, in descending order of how much they cost:
|
|
538
|
+
*
|
|
539
|
+
* 1. **The cursor is materialised, not live.** Upstream's `Cursor` owns a
|
|
540
|
+
* running `SqliteDatabase::Query` and pulls one row at a time; the backend
|
|
541
|
+
* seam this package chose (`SqlDatabase.exec` → `SqlResult`) has already
|
|
542
|
+
* collected every row before a cursor exists. Everything downstream of that
|
|
543
|
+
* follows: there is no statement cache, so `CachedStatement`, the 1 MiB LRU
|
|
544
|
+
* and `reusedCachedQueryForTest` are absent with it; there is no live
|
|
545
|
+
* statement to cancel, so `Cursor::canceled` and `selfRef` — both already
|
|
546
|
+
* dead upstream, written but never assigned — have nothing to guard; and
|
|
547
|
+
* `endQuery`'s job of returning a statement to the cache is nothing here, so
|
|
548
|
+
* the counters it saves off are simply the counters. What is kept is every
|
|
549
|
+
* observable: the position is shared across `next`/`toArray`/`one`/`raw`,
|
|
550
|
+
* and a drained cursor keeps yielding done.
|
|
551
|
+
* 2. **`Cursor` and `Statement` must be constructible with no arguments**, or
|
|
552
|
+
* `SqlStorage` cannot satisfy workers-types without a cast: the interface
|
|
553
|
+
* types them `typeof SqlStorageCursor` / `typeof SqlStorageStatement`, and
|
|
554
|
+
* both are `abstract` there, so their construct signatures take none.
|
|
555
|
+
* Upstream's are unconstructible from JS for the same reason they are
|
|
556
|
+
* `abstract` in the types — JSG nested types have no JS constructor — so the
|
|
557
|
+
* faithful shape is a constructor that refuses. `sql.Cursor` exists for
|
|
558
|
+
* `instanceof`, which is all upstream exposes it for.
|
|
559
|
+
* 3. **The regulator is ported whole, and that is not the whole authorizer.**
|
|
560
|
+
* Four of its five members are here as callbacks, and none needed the
|
|
561
|
+
* authorizer to compute anything — `isAllowedName` is a prefix test,
|
|
562
|
+
* `isAllowedTrigger` is `return true`, `allowTransactions` throws,
|
|
563
|
+
* `shouldAddQueryStats` is a constant. The fifth, `onError`, is not a
|
|
564
|
+
* callback at all in this port: it is every `throw new Error(message)`
|
|
565
|
+
* below, which is all its upstream body does with a refusal message.
|
|
566
|
+
* For those, what the authorizer supplied was the *identifiers*:
|
|
567
|
+
* with none, the statement text is the only source, so `exec` tokenizes it
|
|
568
|
+
* and runs `isAllowedName` over every identifier-shaped token. That is
|
|
569
|
+
* deliberately STRICTER than upstream — see `SQL_RESERVED_PREFIX_MESSAGE`.
|
|
570
|
+
*
|
|
571
|
+
* But the authorizer also makes decisions no callback ever sees, and those
|
|
572
|
+
* do NOT arrive with the regulator: `SQLITE_ATTACH` / `SQLITE_DETACH`, the
|
|
573
|
+
* `SQLITE_CREATE_TEMP_*` family and the `temp` schema, `SQLITE_PRAGMA`, and
|
|
574
|
+
* the `SQLITE_CREATE_VTABLE` module list. Each is refused from the text in
|
|
575
|
+
* `refuseUnauthorizedForms` and `requireAllowedPragmas`. `SQLITE_FUNCTION`
|
|
576
|
+
* is the one still unported — see the README divergence row.
|
|
577
|
+
* 4. **`ingest` stays at upstream's SQLite seam.** `SqliteDatabase.ingest()`
|
|
578
|
+
* executes every complete statement and returns the partial tail, using the
|
|
579
|
+
* same compiled boundaries and regulator as `exec`.
|
|
580
|
+
*
|
|
581
|
+
* Spec: §1.4, §2.4 in docs/decisions.md.
|
|
582
|
+
*/
|
|
583
|
+
/**
|
|
584
|
+
* ← `SqlStorageRegulator::allowTransactions()`, copied verbatim. Users match on
|
|
585
|
+
* it and it is the one regulator callback our substrate can still answer.
|
|
586
|
+
*/
|
|
587
|
+
var SQL_TRANSACTION_REFUSED_MESSAGE = "To execute a transaction, please use the state.storage.transaction() or state.storage.transactionSync() APIs instead of the SQL BEGIN TRANSACTION or SAVEPOINT statements. The JavaScript API is safer because it will automatically roll back on exceptions, and because it interacts correctly with Durable Objects' automatic atomic write coalescing.";
|
|
588
|
+
/** See translation 2 in the header: the class is exposed for `instanceof` only. */
|
|
589
|
+
var CURSOR_NOT_CONSTRUCTIBLE_MESSAGE = "Illegal invocation: SqlStorage.Cursor cannot be constructed directly. Use sql.exec().";
|
|
590
|
+
/** Same, for the prepared-statement compatibility shim. */
|
|
591
|
+
var STATEMENT_NOT_CONSTRUCTIBLE_MESSAGE = "Illegal invocation: SqlStorage.Statement cannot be constructed directly. Use sql.prepare().";
|
|
592
|
+
/**
|
|
593
|
+
* ← SQLite's own denial text, with the reason appended.
|
|
594
|
+
*
|
|
595
|
+
* There is no upstream string to copy here: `SqlStorageRegulator::onError` just
|
|
596
|
+
* rethrows whatever SQLite produced, and SQLite produces `not authorized` for an
|
|
597
|
+
* authorizer denial (`access to X.Y is prohibited` for the column-read case,
|
|
598
|
+
* which needs a resolved identifier we do not have). The prefix is kept so that
|
|
599
|
+
* anything matching upstream still matches; the rest is here because a bare
|
|
600
|
+
* `not authorized` is not debuggable.
|
|
601
|
+
*/
|
|
602
|
+
var SQL_RESERVED_PREFIX_MESSAGE = "not authorized: a SQL statement may not name the reserved _cf_ prefix, which is where this Durable Object keeps its own KV and metadata tables.";
|
|
603
|
+
/**
|
|
604
|
+
* ← the five transaction-control forms `sqlite3_stmt_readonly()` reports
|
|
605
|
+
* read-only and the authorizer reports as `SQLITE_TRANSACTION` /
|
|
606
|
+
* `SQLITE_SAVEPOINT`. The same set `util/sqlite.ts` classifies, read here from
|
|
607
|
+
* the leading keyword because the untrusted path has to refuse them before the
|
|
608
|
+
* trusted one applies them.
|
|
609
|
+
*
|
|
610
|
+
* `;` counts as leading trivia here and in every other leading-keyword refusal
|
|
611
|
+
* below. A statement boundary comes from the backend, and `node:sqlite` reports
|
|
612
|
+
* an empty leading statement as part of the span it compiled: the `sourceSQL`
|
|
613
|
+
* for `;ATTACH ...` is the whole string, so an anchor of `^\s*` would read the
|
|
614
|
+
* keyword as `;` and let the compiled ATTACH through. The browser backend cuts
|
|
615
|
+
* the same input at the first `;` and refuses the empty statement instead.
|
|
616
|
+
*/
|
|
617
|
+
var TRANSACTION_CONTROL = /^[\s;]*(?:BEGIN|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i;
|
|
618
|
+
/** Cheap pre-test, so the tokenizer below runs only on a statement that could fail it. */
|
|
619
|
+
var RESERVED_PREFIX_HINT = /_cf_/i;
|
|
620
|
+
/** A SQL identifier. Double-quoted and bracketed forms are still identifiers, so only the
|
|
621
|
+
* delimiters are stripped and the word inside is scanned like any other. */
|
|
622
|
+
var IDENTIFIER = /[A-Za-z_][A-Za-z0-9_$]*/g;
|
|
623
|
+
/**
|
|
624
|
+
* Everything in a statement an identifier cannot come from: single-quoted string
|
|
625
|
+
* literals (SQLite escapes an embedded quote by doubling it), `--` line comments,
|
|
626
|
+
* and `/* *\/` block comments. Replaced with a space before tokenizing, so what is
|
|
627
|
+
* left is code.
|
|
628
|
+
*
|
|
629
|
+
* Backtick-quoted names are NOT here: MySQL-compatible quoting produces an
|
|
630
|
+
* identifier, exactly as the double-quoted form does.
|
|
631
|
+
*/
|
|
632
|
+
var NOT_CODE = /'(?:[^']|'')*'|--[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
633
|
+
/**
|
|
634
|
+
* Comments are never code. String literals STAY, for both of this regex's callers: pragma
|
|
635
|
+
* arguments may be quoted, and SQLite's misquoting feature reads a single-quoted string as an
|
|
636
|
+
* identifier — `CREATE TABLE 'temp'.t(x)` really creates a temp-schema table — so the checks
|
|
637
|
+
* that read identifier positions must still see it. `NOT_CODE` blanks literals, which is right
|
|
638
|
+
* for the token scans and would be a bypass for these callers.
|
|
639
|
+
*/
|
|
640
|
+
var NOT_COMMENT = /--[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
641
|
+
/**
|
|
642
|
+
* ← `SqlStorageRegulator` (`sql.h:15-22`, `sql.c++:141-165`), whole.
|
|
643
|
+
*
|
|
644
|
+
* Upstream reaches these through the SQLite authorizer while a statement is
|
|
645
|
+
* being compiled. `exec` calls them from the statement text instead, which is
|
|
646
|
+
* the same translation Section 3 made for the write classifier and for
|
|
647
|
+
* transaction state.
|
|
648
|
+
*/
|
|
649
|
+
var SqlStorageRegulator = {
|
|
650
|
+
/**
|
|
651
|
+
* Upstream's body is `return !name.startsWith("_cf_")`, with an autogate that
|
|
652
|
+
* makes the comparison case-insensitive and logs a warning until it lands. The
|
|
653
|
+
* case-insensitive form is taken here: it is the direction upstream is moving,
|
|
654
|
+
* and there is no logger for the warning half.
|
|
655
|
+
*/
|
|
656
|
+
isAllowedName(name) {
|
|
657
|
+
return name.length < 4 || name.slice(0, 4).toLowerCase() !== "_cf_";
|
|
658
|
+
},
|
|
659
|
+
/** Upstream's body is `return true`. */
|
|
660
|
+
isAllowedTrigger(_name) {
|
|
661
|
+
return true;
|
|
662
|
+
},
|
|
663
|
+
/** Upstream's body is a `JSG_FAIL_REQUIRE` with this message. */
|
|
664
|
+
allowTransactions() {
|
|
665
|
+
throw new Error(SQL_TRANSACTION_REFUSED_MESSAGE);
|
|
666
|
+
},
|
|
667
|
+
/** "Bill for queries executed from JavaScript." Nothing reads it — `SqliteObserver` has no port. */
|
|
668
|
+
shouldAddQueryStats() {
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
/**
|
|
673
|
+
* The text-level stand-in for the authorizer's `isAllowedName` calls — see
|
|
674
|
+
* `SQL_RESERVED_PREFIX_MESSAGE` and the README row.
|
|
675
|
+
*
|
|
676
|
+
* Upstream refuses a *resolved identifier* that starts with `_cf_`, because it
|
|
677
|
+
* reaches `isAllowedName` through the SQLite authorizer while the statement is
|
|
678
|
+
* being compiled. There is no authorizer here, so this tokenizes the statement
|
|
679
|
+
* text instead, over everything that is not a string literal or a comment —
|
|
680
|
+
* which is the same set of characters an identifier can come from.
|
|
681
|
+
*
|
|
682
|
+
* **The literals were once refused too, and that was wrong.** The first draft
|
|
683
|
+
* scanned the whole statement on the reasoning that no legitimate consumer
|
|
684
|
+
* statement contains the token, so being stricter than upstream was the safe
|
|
685
|
+
* direction. A retained conformance case uses `_cf_keepAliveHeartbeat` as a
|
|
686
|
+
* bound value: real workerd accepts it, so this parser must distinguish data
|
|
687
|
+
* from identifiers. `conformance/suite/sql.spec.ts` pins the rule: refused as a
|
|
688
|
+
* table name and as a quoted identifier, allowed as data.
|
|
689
|
+
*
|
|
690
|
+
* A name that merely CONTAINS the token — `my_cf_thing` — stays allowed, because
|
|
691
|
+
* `isAllowedName` tests a prefix.
|
|
692
|
+
*/
|
|
693
|
+
function requireAllowedNames(query) {
|
|
694
|
+
if (!RESERVED_PREFIX_HINT.test(query)) return;
|
|
695
|
+
const code = query.replace(NOT_CODE, " ");
|
|
696
|
+
for (const [token] of code.matchAll(IDENTIFIER)) if (!SqlStorageRegulator.isAllowedName(token)) throw new Error(SQL_RESERVED_PREFIX_MESSAGE);
|
|
697
|
+
}
|
|
698
|
+
/** Refuse transaction control against one SQLite-decided statement boundary. */
|
|
699
|
+
function refuseTransactionControl(statement) {
|
|
700
|
+
const code = statement.replace(NOT_CODE, " ");
|
|
701
|
+
if (TRANSACTION_CONTROL.test(code)) SqlStorageRegulator.allowTransactions();
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* ← the message a `SQLITE_DENY` from the authorizer surfaces to JavaScript,
|
|
705
|
+
* byte-identical so a caller matching on it ports unchanged.
|
|
706
|
+
*/
|
|
707
|
+
var SQL_NOT_AUTHORIZED_MESSAGE = "not authorized: SQLITE_AUTH";
|
|
708
|
+
/**
|
|
709
|
+
* The forms neither the regulator nor any callback sees: the authorizer's own
|
|
710
|
+
* action codes, plus `VACUUM`, which SQLite itself refuses by precondition.
|
|
711
|
+
*
|
|
712
|
+
* Porting `SqlStorageRegulator` whole (point 3 in the file header) carried over its members, but
|
|
713
|
+
* not the authorizer's own action codes: `SQLITE_ATTACH`, `SQLITE_DETACH`, `SQLITE_CREATE_TEMP_*`
|
|
714
|
+
* and `SQLITE_CREATE_VTABLE` consult no callback, so nothing here refused them. Every form below
|
|
715
|
+
* was measured on real workerd through the conformance oracle, not inferred.
|
|
716
|
+
*
|
|
717
|
+
* `ATTACH` and `DETACH` are also the isolation boundary rather than a fidelity detail: both
|
|
718
|
+
* backends open a real file, so on `node:sqlite` an `ATTACH` reads another actor's database and
|
|
719
|
+
* a `VACUUM INTO` writes anywhere the process can. The reserved-name scan does not cover it,
|
|
720
|
+
* because that scan tokenizes the SUBMITTED statement — `other._cf_KV` is caught, and every
|
|
721
|
+
* application table in the same attached database is not.
|
|
722
|
+
*/
|
|
723
|
+
var DATABASE_ATTACHMENT = /^[\s;]*(?:ATTACH|DETACH)\b/i;
|
|
724
|
+
/**
|
|
725
|
+
* ← the `SQLITE_CREATE_TEMP_*` denials (`sqlite.c++:1323`) and the `dbName == temp` rule
|
|
726
|
+
* (`sqlite.c++:1073`), which permits a temp-schema database name only `READ` and `UPDATE`.
|
|
727
|
+
* Upstream's own reason to deny them applies here unchanged: a temporary table makes SQLite
|
|
728
|
+
* open a separate temporary file that the storage engine knows nothing about.
|
|
729
|
+
*
|
|
730
|
+
* Two spellings, one refusal each by its own upstream path: `CREATE TEMP TABLE t(x)` is the
|
|
731
|
+
* keyword and hits the action codes; `CREATE TABLE temp.t(x)` is the schema qualifier and hits
|
|
732
|
+
* the `dbName` rule — and the qualifier was the live gap, where the table was created, written
|
|
733
|
+
* and read back here while workerd refused it outright. The qualifier accepts every quoting
|
|
734
|
+
* SQLite does, single quotes included, with or without whitespace after the keyword (measured:
|
|
735
|
+
* workerd refuses `CREATE TABLE 'temp'.t(x)` and `CREATE TABLE"temp".t(x)` the same way), and
|
|
736
|
+
* is matched only in the object-name position, so an application table merely NAMED `tempest`
|
|
737
|
+
* or `temporary_log` is untouched. The keyword spelling needs no qualifier arm of its own here:
|
|
738
|
+
* `TEMP_SCHEMA` already refuses every `CREATE TEMP…` before this pattern is consulted.
|
|
739
|
+
*
|
|
740
|
+
* The rest of the `dbName` rule goes unported on purpose: with no way to create a temp-schema
|
|
741
|
+
* object, `INSERT`/`DELETE`/`DROP` against one die in SQLite as `no such table`, and the one
|
|
742
|
+
* silent form measures identically — workerd allows `DROP TABLE IF EXISTS temp.ghost` too.
|
|
743
|
+
*/
|
|
744
|
+
var TEMP_SCHEMA = /^[\s;]*CREATE\s+(?:TEMP|TEMPORARY)\b/i;
|
|
745
|
+
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;
|
|
746
|
+
/**
|
|
747
|
+
* ← `SQLITE_CREATE_VTABLE` (`sqlite.c++:1298-1316`): a virtual table is native-code callbacks, so
|
|
748
|
+
* upstream allows exactly four modules — FTS5 and its `fts5vocab` companion, R*Tree and its
|
|
749
|
+
* `rtree_i32` variant — and denies every other module SQLite was compiled with.
|
|
750
|
+
*
|
|
751
|
+
* `dbstat` is why this is not only fidelity: it reports a row per table with page counts and
|
|
752
|
+
* byte sizes, so `SELECT name FROM d` enumerates `_cf_KV` and the rest of the runtime's own
|
|
753
|
+
* tables without the statement ever naming them — around `requireAllowedNames`, which can only
|
|
754
|
+
* see the text it was given.
|
|
755
|
+
*
|
|
756
|
+
* The table name and the module accept every quoting SQLite does — double quotes, backticks,
|
|
757
|
+
* brackets, and the misquoting feature's single-quoted string, with or without whitespace
|
|
758
|
+
* before them — because the module has to be read from PAST the name, and a guessed name
|
|
759
|
+
* boundary is a bypass in both directions: an unparseable name skipped the check, and
|
|
760
|
+
* `"a USING fts5 b" USING dbstat` read its module out of the quoted name. Measured: workerd
|
|
761
|
+
* refuses both, refuses `CREATE VIRTUAL TABLE"d"USING dbstat`, resolves `USING "dbstat"` to
|
|
762
|
+
* the same denial, and allows `USING 'fts5'`. A `CREATE VIRTUAL TABLE` whose module the
|
|
763
|
+
* pattern cannot read is refused outright — the deliberately stricter direction the
|
|
764
|
+
* unparseable-PRAGMA fallback below already takes.
|
|
765
|
+
*/
|
|
766
|
+
var SQL_IDENTIFIER_SOURCE = /"(?:[^"]|"")*"|'(?:[^']|'')*'|`(?:[^`]|``)*`|\[[^\]]*\]|[A-Za-z_\u0080-\uffff][A-Za-z0-9_$\u0080-\uffff]*/.source;
|
|
767
|
+
var VIRTUAL_TABLE = /^[\s;]*CREATE\s+VIRTUAL\s+TABLE\b/i;
|
|
768
|
+
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");
|
|
769
|
+
var ALLOWED_VIRTUAL_TABLE_MODULES = /* @__PURE__ */ new Set([
|
|
770
|
+
"fts5",
|
|
771
|
+
"fts5vocab",
|
|
772
|
+
"rtree",
|
|
773
|
+
"rtree_i32"
|
|
774
|
+
]);
|
|
775
|
+
/**
|
|
776
|
+
* `VACUUM` has no action code of its own, so the authorizer never sees it. What refuses it
|
|
777
|
+
* upstream is SQLite's own `cannot VACUUM from within a transaction` precondition, against the
|
|
778
|
+
* transaction a Durable Object always has open — and that message is what the oracle returned,
|
|
779
|
+
* for `VACUUM`, `VACUUM main` and `VACUUM INTO` alike. Byte-identical for the same reason
|
|
780
|
+
* `SQL_NOT_AUTHORIZED_MESSAGE` is: a caller matching on it ports unchanged.
|
|
781
|
+
*
|
|
782
|
+
* Refused here unconditionally rather than by transaction state, which is a divergence only in
|
|
783
|
+
* mechanism: this runtime never runs a statement where upstream would have allowed it.
|
|
784
|
+
*/
|
|
785
|
+
var VACUUM_STATEMENT = /^[\s;]*VACUUM\b/i;
|
|
786
|
+
var SQL_VACUUM_REFUSED_MESSAGE = "cannot VACUUM from within a transaction: SQLITE_ERROR";
|
|
787
|
+
/** Refuse the authorizer-only forms against one SQLite-decided statement boundary. */
|
|
788
|
+
function refuseUnauthorizedForms(statement) {
|
|
789
|
+
const code = statement.replace(NOT_COMMENT, " ");
|
|
790
|
+
if (VACUUM_STATEMENT.test(code)) throw new Error(SQL_VACUUM_REFUSED_MESSAGE);
|
|
791
|
+
if (DATABASE_ATTACHMENT.test(code) || TEMP_SCHEMA.test(code) || TEMP_QUALIFIED.test(code)) throw new Error(SQL_NOT_AUTHORIZED_MESSAGE);
|
|
792
|
+
if (VIRTUAL_TABLE.test(code)) {
|
|
793
|
+
const moduleName = VIRTUAL_TABLE_MODULE.exec(code)?.[1];
|
|
794
|
+
if (moduleName === void 0 || !ALLOWED_VIRTUAL_TABLE_MODULES.has(unquoted(moduleName).toLowerCase())) throw new Error(SQL_NOT_AUTHORIZED_MESSAGE);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/** Cheap pre-test; `PRAGMA` and the `pragma_` functions both contain it. */
|
|
798
|
+
var PRAGMA_HINT = /pragma/i;
|
|
799
|
+
/** `PRAGMA [schema.]name`, then `= value`, `(argument)`, or nothing. */
|
|
800
|
+
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;
|
|
801
|
+
/**
|
|
802
|
+
* ← `ALLOWED_PRAGMAS` (`util/sqlite.c++:543-571`) and `PragmaSignature` (`:528-535`),
|
|
803
|
+
* verbatim. `table_list`, `table_info`, and `table_xinfo` are special-cased
|
|
804
|
+
* ahead of the table in the authorizer, exactly as upstream's `SQLITE_PRAGMA`
|
|
805
|
+
* case does (`util/sqlite.c++:1194-1273`).
|
|
806
|
+
*/
|
|
807
|
+
var ALLOWED_PRAGMAS = /* @__PURE__ */ new Map([
|
|
808
|
+
["data_version", "NO_ARG"],
|
|
809
|
+
["page_size", "NO_ARG"],
|
|
810
|
+
["case_sensitive_like", "BOOLEAN"],
|
|
811
|
+
["foreign_keys", "BOOLEAN"],
|
|
812
|
+
["defer_foreign_keys", "BOOLEAN"],
|
|
813
|
+
["ignore_check_constraints", "BOOLEAN"],
|
|
814
|
+
["legacy_alter_table", "BOOLEAN"],
|
|
815
|
+
["recursive_triggers", "BOOLEAN"],
|
|
816
|
+
["reverse_unordered_selects", "BOOLEAN"],
|
|
817
|
+
["foreign_key_check", "OPTIONAL_OBJECT_NAME"],
|
|
818
|
+
["foreign_key_list", "OBJECT_NAME"],
|
|
819
|
+
["index_info", "OBJECT_NAME"],
|
|
820
|
+
["index_list", "OBJECT_NAME"],
|
|
821
|
+
["index_xinfo", "OBJECT_NAME"],
|
|
822
|
+
["quick_check", "NULL_NUMBER_OR_OBJECT_NAME"],
|
|
823
|
+
["optimize", "NULL_OR_NUMBER"]
|
|
824
|
+
]);
|
|
825
|
+
/** Upstream compares the eight literal forms as PREFIXES, case-insensitively. */
|
|
826
|
+
var BOOLEAN_PRAGMA_VALUE = /^(?:true|false|yes|no|on|off|1|0)/i;
|
|
827
|
+
/**
|
|
828
|
+
* The pragmas SQLite ships (https://www.sqlite.org/pragma.html), so a
|
|
829
|
+
* `pragma_X` identifier can be told apart: `X` here means the table-valued
|
|
830
|
+
* pragma function and follows the allowlist; any other `pragma_`-prefixed
|
|
831
|
+
* identifier is an ordinary application name, which upstream's authorizer
|
|
832
|
+
* distinguishes by resolution and the conformance suite pins.
|
|
833
|
+
*/
|
|
834
|
+
var SQLITE_PRAGMA_NAMES = /* @__PURE__ */ new Set([
|
|
835
|
+
"analysis_limit",
|
|
836
|
+
"application_id",
|
|
837
|
+
"auto_vacuum",
|
|
838
|
+
"automatic_index",
|
|
839
|
+
"busy_timeout",
|
|
840
|
+
"cache_size",
|
|
841
|
+
"cache_spill",
|
|
842
|
+
"case_sensitive_like",
|
|
843
|
+
"cell_size_check",
|
|
844
|
+
"checkpoint_fullfsync",
|
|
845
|
+
"collation_list",
|
|
846
|
+
"compile_options",
|
|
847
|
+
"data_version",
|
|
848
|
+
"database_list",
|
|
849
|
+
"defer_foreign_keys",
|
|
850
|
+
"encoding",
|
|
851
|
+
"foreign_key_check",
|
|
852
|
+
"foreign_key_list",
|
|
853
|
+
"foreign_keys",
|
|
854
|
+
"freelist_count",
|
|
855
|
+
"full_column_names",
|
|
856
|
+
"fullfsync",
|
|
857
|
+
"function_list",
|
|
858
|
+
"hard_heap_limit",
|
|
859
|
+
"ignore_check_constraints",
|
|
860
|
+
"incremental_vacuum",
|
|
861
|
+
"index_info",
|
|
862
|
+
"index_list",
|
|
863
|
+
"index_xinfo",
|
|
864
|
+
"integrity_check",
|
|
865
|
+
"journal_mode",
|
|
866
|
+
"journal_size_limit",
|
|
867
|
+
"legacy_alter_table",
|
|
868
|
+
"legacy_file_format",
|
|
869
|
+
"locking_mode",
|
|
870
|
+
"max_page_count",
|
|
871
|
+
"mmap_size",
|
|
872
|
+
"module_list",
|
|
873
|
+
"optimize",
|
|
874
|
+
"page_count",
|
|
875
|
+
"page_size",
|
|
876
|
+
"pragma_list",
|
|
877
|
+
"query_only",
|
|
878
|
+
"quick_check",
|
|
879
|
+
"read_uncommitted",
|
|
880
|
+
"recursive_triggers",
|
|
881
|
+
"reverse_unordered_selects",
|
|
882
|
+
"schema_version",
|
|
883
|
+
"secure_delete",
|
|
884
|
+
"short_column_names",
|
|
885
|
+
"shrink_memory",
|
|
886
|
+
"soft_heap_limit",
|
|
887
|
+
"synchronous",
|
|
888
|
+
"table_info",
|
|
889
|
+
"table_list",
|
|
890
|
+
"table_xinfo",
|
|
891
|
+
"temp_store",
|
|
892
|
+
"threads",
|
|
893
|
+
"trusted_schema",
|
|
894
|
+
"user_version",
|
|
895
|
+
"wal_autocheckpoint",
|
|
896
|
+
"wal_checkpoint",
|
|
897
|
+
"writable_schema"
|
|
898
|
+
]);
|
|
899
|
+
/** kj's `tryParseAs` is decimal; keep the same acceptance. */
|
|
900
|
+
var DECIMAL = /^[+-]?\d+$/;
|
|
901
|
+
/**
|
|
902
|
+
* One layer of SQL quoting, any of the four forms — a pragma argument, or the module token
|
|
903
|
+
* `VIRTUAL_TABLE_MODULE` captured. Doubled inner quotes stay doubled, which cannot change a
|
|
904
|
+
* verdict here: no allowlisted comparison target contains a quote character.
|
|
905
|
+
*/
|
|
906
|
+
function unquoted(argument) {
|
|
907
|
+
const first = argument[0];
|
|
908
|
+
const last = argument[argument.length - 1];
|
|
909
|
+
if (argument.length >= 2) {
|
|
910
|
+
if ((first === "'" || first === "\"" || first === "`") && last === first) return argument.slice(1, -1);
|
|
911
|
+
if (first === "[" && last === "]") return argument.slice(1, -1);
|
|
912
|
+
}
|
|
913
|
+
return argument;
|
|
914
|
+
}
|
|
915
|
+
/** ← the `SQLITE_PRAGMA` authorizer case (`util/sqlite.c++:1194-1273`), whole. */
|
|
916
|
+
function isAllowedPragma(name, argument) {
|
|
917
|
+
const pragma = name.toLowerCase();
|
|
918
|
+
if (pragma === "table_list") return true;
|
|
919
|
+
if (pragma === "table_info" || pragma === "table_xinfo") {
|
|
920
|
+
if (argument === void 0) return false;
|
|
921
|
+
return SqlStorageRegulator.isAllowedName(unquoted(argument));
|
|
922
|
+
}
|
|
923
|
+
const signature = ALLOWED_PRAGMAS.get(pragma);
|
|
924
|
+
if (signature === void 0) return false;
|
|
925
|
+
switch (signature) {
|
|
926
|
+
case "NO_ARG": return argument === void 0;
|
|
927
|
+
case "BOOLEAN": return argument === void 0 || BOOLEAN_PRAGMA_VALUE.test(unquoted(argument));
|
|
928
|
+
case "OBJECT_NAME": return argument !== void 0 && SqlStorageRegulator.isAllowedName(unquoted(argument));
|
|
929
|
+
case "OPTIONAL_OBJECT_NAME": return argument === void 0 || SqlStorageRegulator.isAllowedName(unquoted(argument));
|
|
930
|
+
case "NULL_OR_NUMBER": return argument === void 0 || DECIMAL.test(argument);
|
|
931
|
+
case "NULL_NUMBER_OR_OBJECT_NAME": return argument === void 0 || DECIMAL.test(argument) || SqlStorageRegulator.isAllowedName(unquoted(argument));
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* The text-level stand-in for the authorizer's `SQLITE_PRAGMA` case, against
|
|
936
|
+
* one SQLite-decided statement boundary. Load-bearing beyond fidelity:
|
|
937
|
+
* `user_version` is where runtime storage versioning keeps its per-file stamp
|
|
938
|
+
* (`util/sqlite-migrations.ts`), and `writable_schema` would let application
|
|
939
|
+
* SQL rewrite `sqlite_master` out from under the `_cf_` reservation.
|
|
940
|
+
*
|
|
941
|
+
* The `pragma_` table-valued functions reach the same authorizer path
|
|
942
|
+
* upstream, so they follow the same allowlist here — by pragma NAME only. An
|
|
943
|
+
* argument the text cannot see (a string literal or a binding) goes unchecked
|
|
944
|
+
* where upstream's authorizer sees the resolved value; a `_cf_` name smuggled
|
|
945
|
+
* that way reads schema whose shape is public source anyway, while identifier
|
|
946
|
+
* arguments stay covered by `requireAllowedNames`. The README divergence row
|
|
947
|
+
* records this.
|
|
948
|
+
*/
|
|
949
|
+
function requireAllowedPragmas(statement) {
|
|
950
|
+
if (!PRAGMA_HINT.test(statement)) return;
|
|
951
|
+
const code = statement.replace(NOT_COMMENT, " ");
|
|
952
|
+
const direct = code.match(PRAGMA_STATEMENT);
|
|
953
|
+
if (direct !== null) {
|
|
954
|
+
const [, name = "", assigned, called] = direct;
|
|
955
|
+
const argument = (assigned ?? called)?.trim();
|
|
956
|
+
if (!isAllowedPragma(name, argument === "" ? void 0 : argument)) throw new Error(SQL_NOT_AUTHORIZED_MESSAGE);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (/^[\s;]*PRAGMA\b/i.test(code)) throw new Error(SQL_NOT_AUTHORIZED_MESSAGE);
|
|
960
|
+
const literalFree = code.replace(NOT_CODE, " ");
|
|
961
|
+
for (const [token] of literalFree.matchAll(IDENTIFIER)) {
|
|
962
|
+
if (token.length <= 7 || token.slice(0, 7).toLowerCase() !== "pragma_") continue;
|
|
963
|
+
const name = token.slice(7).toLowerCase();
|
|
964
|
+
if (!SQLITE_PRAGMA_NAMES.has(name)) continue;
|
|
965
|
+
if (name !== "table_list" && name !== "table_info" && name !== "table_xinfo" && !ALLOWED_PRAGMAS.has(name)) throw new Error(SQL_NOT_AUTHORIZED_MESSAGE);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
/** Everything the untrusted path refuses at one statement boundary. */
|
|
969
|
+
function regulateUntrustedStatement(statement) {
|
|
970
|
+
refuseTransactionControl(statement);
|
|
971
|
+
refuseUnauthorizedForms(statement);
|
|
972
|
+
requireAllowedPragmas(statement);
|
|
973
|
+
}
|
|
974
|
+
/** ← `JSG_INHERIT_INTRINSIC(v8::kIteratorPrototype)` (`jsg/iterator.h:1044`). */
|
|
975
|
+
var IteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()));
|
|
976
|
+
/**
|
|
977
|
+
* ← the `JSG_ITERATOR` types (`jsg/iterator.h:1036-1050`): `next` and
|
|
978
|
+
* self-iterability on `%IteratorPrototype%` — which is what carries the ES
|
|
979
|
+
* iterator helpers; `raw().toArray()` is what Drizzle's durable-sqlite driver
|
|
980
|
+
* calls — and NOTHING else. No `return`, no `throw` (only the async variant
|
|
981
|
+
* registers `return_`, `:1069-1085`), so `IteratorClose` after a `break`, a
|
|
982
|
+
* partial destructuring, or a `take()` is a no-op and a retained iterator
|
|
983
|
+
* resumes. Results are `JSG_STRUCT(done, value)` in that key order
|
|
984
|
+
* (`jsg/iterator.h:706-710`).
|
|
985
|
+
*/
|
|
986
|
+
var RawIterator = class {
|
|
987
|
+
#pull;
|
|
988
|
+
constructor(pull) {
|
|
989
|
+
this.#pull = pull;
|
|
990
|
+
}
|
|
991
|
+
next() {
|
|
992
|
+
const raw = this.#pull();
|
|
993
|
+
if (raw === void 0) return {
|
|
994
|
+
done: true,
|
|
995
|
+
value: void 0
|
|
996
|
+
};
|
|
997
|
+
return {
|
|
998
|
+
done: false,
|
|
999
|
+
value: asRawRow([...raw])
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
[Symbol.iterator]() {
|
|
1003
|
+
return this;
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
Object.setPrototypeOf(RawIterator.prototype, IteratorPrototype);
|
|
1007
|
+
Object.defineProperty(RawIterator.prototype, Symbol.toStringTag, {
|
|
1008
|
+
value: "RawIterator",
|
|
1009
|
+
configurable: true
|
|
1010
|
+
});
|
|
1011
|
+
/** ← `RowIterator`, shaped exactly as `RawIterator` above. */
|
|
1012
|
+
var RowIterator = class {
|
|
1013
|
+
#pull;
|
|
1014
|
+
constructor(pull) {
|
|
1015
|
+
this.#pull = pull;
|
|
1016
|
+
}
|
|
1017
|
+
next() {
|
|
1018
|
+
const row = this.#pull();
|
|
1019
|
+
if (row === void 0) return {
|
|
1020
|
+
done: true,
|
|
1021
|
+
value: void 0
|
|
1022
|
+
};
|
|
1023
|
+
return {
|
|
1024
|
+
done: false,
|
|
1025
|
+
value: row
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
[Symbol.iterator]() {
|
|
1029
|
+
return this;
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
Object.setPrototypeOf(RowIterator.prototype, IteratorPrototype);
|
|
1033
|
+
Object.defineProperty(RowIterator.prototype, Symbol.toStringTag, {
|
|
1034
|
+
value: "RowIterator",
|
|
1035
|
+
configurable: true
|
|
1036
|
+
});
|
|
1037
|
+
/**
|
|
1038
|
+
* ← `SqlStorage::Cursor`.
|
|
1039
|
+
*
|
|
1040
|
+
* `rowsRead` is the one counter that is not upstream's. Upstream reads
|
|
1041
|
+
* `Query::getRowsRead()`, a billing counter sourced from libsql's
|
|
1042
|
+
* `STMTSTATUS_ROWS_READ` that counts index rows and that neither backend
|
|
1043
|
+
* exposes — the same absence the README already records for `SqlResult`. The
|
|
1044
|
+
* interface requires a number, so this returns the rows the cursor has yielded,
|
|
1045
|
+
* which is what today's browser host returns and what its tests assert. It
|
|
1046
|
+
* undercounts any query that scans more rows than it returns.
|
|
1047
|
+
*/
|
|
1048
|
+
var Cursor = class {
|
|
1049
|
+
#columnNames;
|
|
1050
|
+
#rawRows;
|
|
1051
|
+
#rowsWritten;
|
|
1052
|
+
#position = 0;
|
|
1053
|
+
constructor(state) {
|
|
1054
|
+
if (state === void 0) throw new Error(CURSOR_NOT_CONSTRUCTIBLE_MESSAGE);
|
|
1055
|
+
this.#columnNames = state.columnNames;
|
|
1056
|
+
this.#rawRows = state.rawRows;
|
|
1057
|
+
this.#rowsWritten = state.rowsWritten;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* ← `JSG_READONLY_PROTOTYPE_PROPERTY(columnNames)` (`sql.h:210`): a
|
|
1061
|
+
* prototype accessor, not an own field, so a cursor JSON-stringifies to `{}`.
|
|
1062
|
+
*/
|
|
1063
|
+
get columnNames() {
|
|
1064
|
+
return this.#columnNames;
|
|
1065
|
+
}
|
|
1066
|
+
/** ← `Cursor::next`, whose `RowIterator::Next` is this exact shape. */
|
|
1067
|
+
next() {
|
|
1068
|
+
const row = this.#nextRow();
|
|
1069
|
+
if (row === void 0) return {
|
|
1070
|
+
done: true,
|
|
1071
|
+
value: void 0
|
|
1072
|
+
};
|
|
1073
|
+
return {
|
|
1074
|
+
done: false,
|
|
1075
|
+
value: row
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
/** ← `Cursor::toArray`, which drains from the current position. */
|
|
1079
|
+
toArray() {
|
|
1080
|
+
const rows = [];
|
|
1081
|
+
for (;;) {
|
|
1082
|
+
const row = this.#nextRow();
|
|
1083
|
+
if (row === void 0) return rows;
|
|
1084
|
+
rows.push(row);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
/** ← `Cursor::one`. Both messages are upstream's, verbatim. */
|
|
1088
|
+
one() {
|
|
1089
|
+
const row = this.#nextRow();
|
|
1090
|
+
if (row === void 0) throw new Error("Expected exactly one result from SQL query, but got no results.");
|
|
1091
|
+
if (this.#position < this.#rawRows.length) {
|
|
1092
|
+
this.#position = this.#rawRows.length;
|
|
1093
|
+
throw new Error("Expected exactly one result from SQL query, but got multiple results.");
|
|
1094
|
+
}
|
|
1095
|
+
return row;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* ← `Cursor::raw`, which shares this cursor's position rather than
|
|
1099
|
+
* restarting. The iterator's shape is `RawIterator`'s whole doc comment.
|
|
1100
|
+
*/
|
|
1101
|
+
raw() {
|
|
1102
|
+
return new RawIterator(() => this.#nextRaw());
|
|
1103
|
+
}
|
|
1104
|
+
/** ← `JSG_ITERABLE(rows)`, yielding through the same shared position. */
|
|
1105
|
+
[Symbol.iterator]() {
|
|
1106
|
+
return new RowIterator(() => this.#nextRow());
|
|
1107
|
+
}
|
|
1108
|
+
get rowsRead() {
|
|
1109
|
+
return this.#position;
|
|
1110
|
+
}
|
|
1111
|
+
/** ← `Cursor::getRowsWritten`, which is `SqlResult.rowsWritten` here. */
|
|
1112
|
+
get rowsWritten() {
|
|
1113
|
+
return this.#rowsWritten;
|
|
1114
|
+
}
|
|
1115
|
+
#nextRaw() {
|
|
1116
|
+
const raw = this.#rawRows[this.#position];
|
|
1117
|
+
if (raw === void 0) return void 0;
|
|
1118
|
+
this.#position += 1;
|
|
1119
|
+
return raw;
|
|
1120
|
+
}
|
|
1121
|
+
/** ← `Cursor::rowIteratorNext`: zip the column names onto the row. */
|
|
1122
|
+
#nextRow() {
|
|
1123
|
+
const raw = this.#nextRaw();
|
|
1124
|
+
if (raw === void 0) return void 0;
|
|
1125
|
+
const row = {};
|
|
1126
|
+
this.#columnNames.forEach((name, index) => {
|
|
1127
|
+
row[name] = raw[index] ?? null;
|
|
1128
|
+
});
|
|
1129
|
+
return asRow(row);
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
/** ← the jsg resource-type tag every workerd API object carries (`resource.h`). */
|
|
1133
|
+
Object.defineProperty(Cursor.prototype, Symbol.toStringTag, {
|
|
1134
|
+
value: "Cursor",
|
|
1135
|
+
configurable: true
|
|
1136
|
+
});
|
|
1137
|
+
/**
|
|
1138
|
+
* ← `SqlStorage::Statement`, which upstream describes as "supported only for
|
|
1139
|
+
* backwards compatibility ... it is actually just a wrapper around `exec()`".
|
|
1140
|
+
* `JSG_CALLABLE(run)` makes the object itself callable, so `prepare()` returns a
|
|
1141
|
+
* function wearing this prototype rather than an object with a `run` method.
|
|
1142
|
+
*/
|
|
1143
|
+
var Statement = class {
|
|
1144
|
+
constructor() {
|
|
1145
|
+
throw new Error(STATEMENT_NOT_CONSTRUCTIBLE_MESSAGE);
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
var SqlStorage = class {
|
|
1149
|
+
#ctx;
|
|
1150
|
+
#owner;
|
|
1151
|
+
/** ← `kj::Maybe<uint> pageSize`, memoized for the same reason. */
|
|
1152
|
+
#pageSize;
|
|
1153
|
+
constructor(ctx, owner) {
|
|
1154
|
+
this.#ctx = ctx;
|
|
1155
|
+
this.#owner = owner;
|
|
1156
|
+
}
|
|
1157
|
+
/** ← `JSG_NESTED_TYPE(Cursor)`. Exposed so `instanceof` works, as upstream's is. */
|
|
1158
|
+
Cursor = Cursor;
|
|
1159
|
+
/** ← `JSG_NESTED_TYPE(Statement)`. */
|
|
1160
|
+
Statement = Statement;
|
|
1161
|
+
exec(query, ...bindings) {
|
|
1162
|
+
requireInputLock(this.#ctx, "sql.exec()");
|
|
1163
|
+
const db = this.#owner.getSqliteDb();
|
|
1164
|
+
const sqlBindings = bindings.map(toSqlBindingValue);
|
|
1165
|
+
requireAllowedNames(query);
|
|
1166
|
+
const result = db.run({ regulate: regulateUntrustedStatement }, query, ...sqlBindings);
|
|
1167
|
+
return new Cursor({
|
|
1168
|
+
columnNames: [...result.columnNames],
|
|
1169
|
+
rawRows: result.rawRows.map((row) => row.map(toSqlStorageValue)),
|
|
1170
|
+
rowsWritten: result.rowsWritten
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* ← `SqlStorage::getDatabaseSize`.
|
|
1175
|
+
*
|
|
1176
|
+
* Upstream's second query is `PRAGMA page_size;`, which `sqlite3_stmt_readonly()`
|
|
1177
|
+
* reports read-only. With no such call the text is the only source and §1.7.1's
|
|
1178
|
+
* rule is write-unless-provably-a-read, so a bare `PRAGMA` would open a
|
|
1179
|
+
* transaction and take an output-gate lock to answer a size question. The
|
|
1180
|
+
* `pragma_page_size` table-valued function is the same value read through the
|
|
1181
|
+
* `SELECT` upstream already uses for the page count.
|
|
1182
|
+
*/
|
|
1183
|
+
get databaseSize() {
|
|
1184
|
+
requireInputLock(this.#ctx, "sql.databaseSize");
|
|
1185
|
+
const db = this.#owner.getSqliteDb();
|
|
1186
|
+
return readNumber(db.run("select (select * from pragma_page_count) - (select * from pragma_freelist_count);"), "page count") * this.#getPageSize(db);
|
|
1187
|
+
}
|
|
1188
|
+
/** ← `SqlStorage::prepare`. Experimental and deprecated upstream; `exec` caches for you. */
|
|
1189
|
+
prepare(query) {
|
|
1190
|
+
requireInputLock(this.#ctx, "sql.prepare()");
|
|
1191
|
+
const run = (...bindings) => this.exec(query, ...bindings);
|
|
1192
|
+
Object.setPrototypeOf(run, Statement.prototype);
|
|
1193
|
+
return run;
|
|
1194
|
+
}
|
|
1195
|
+
/** ← `SqlStorage::ingest`. */
|
|
1196
|
+
ingest(query) {
|
|
1197
|
+
requireInputLock(this.#ctx, "sql.ingest()");
|
|
1198
|
+
requireAllowedNames(query);
|
|
1199
|
+
return this.#owner.getSqliteDb().ingest(query, regulateUntrustedStatement);
|
|
1200
|
+
}
|
|
1201
|
+
/** ← `SqlStorage::setMaxPageCountForTest`, which is what its name says. */
|
|
1202
|
+
setMaxPageCountForTest(count) {
|
|
1203
|
+
requireInputLock(this.#ctx, "sql.setMaxPageCountForTest()");
|
|
1204
|
+
this.#owner.getSqliteDb().run(`PRAGMA max_page_count = ${count}`);
|
|
1205
|
+
}
|
|
1206
|
+
/** ← `SqlStorage::getPageSize`. */
|
|
1207
|
+
#getPageSize(db) {
|
|
1208
|
+
const cached = this.#pageSize;
|
|
1209
|
+
if (cached !== void 0) return cached;
|
|
1210
|
+
const size = readNumber(db.run("select * from pragma_page_size;"), "page size");
|
|
1211
|
+
this.#pageSize = size;
|
|
1212
|
+
return size;
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
function readNumber(result, what) {
|
|
1216
|
+
const value = result.rawRows[0]?.[0];
|
|
1217
|
+
if (typeof value === "number") return value;
|
|
1218
|
+
if (typeof value === "bigint") return Number(value);
|
|
1219
|
+
throw new Error(`Expected a number for the database's ${what}.`);
|
|
1220
|
+
}
|
|
1221
|
+
/** ← JSG's conversion from JavaScript arguments to `SqlStorage::BindingValue`. */
|
|
1222
|
+
function toSqlBindingValue(value) {
|
|
1223
|
+
if (value === null || value === void 0) return null;
|
|
1224
|
+
if (typeof value === "string" || typeof value === "number") return value;
|
|
1225
|
+
if (typeof value === "boolean") return String(value);
|
|
1226
|
+
if (typeof value === "bigint") throw new TypeError("Cannot convert a BigInt value to a number");
|
|
1227
|
+
if (value instanceof ArrayBuffer) return copyBytes(new Uint8Array(value));
|
|
1228
|
+
if (ArrayBuffer.isView(value)) return copyBytes(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
|
|
1229
|
+
throw new TypeError(`Cannot convert ${Object.prototype.toString.call(value)} to a SQL value`);
|
|
1230
|
+
}
|
|
1231
|
+
function copyBytes(bytes) {
|
|
1232
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
1233
|
+
copy.set(bytes);
|
|
1234
|
+
return copy;
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* ← `SqlStorage::wrapSqlValue` plus the `Query::getValue` switch above it.
|
|
1238
|
+
*
|
|
1239
|
+
* Upstream's int64 arm carries its own comment: "int64 will become BigInt, but
|
|
1240
|
+
* most applications won't want all their integers to be BigInt. We will coerce
|
|
1241
|
+
* to a double here." That coercion is kept rather than refused, because it is
|
|
1242
|
+
* the documented behaviour of `sql.exec` and a caller storing an id larger than
|
|
1243
|
+
* 2^53 has already lost on workerd.
|
|
1244
|
+
*/
|
|
1245
|
+
function toSqlStorageValue(value) {
|
|
1246
|
+
if (value === null || value === void 0) return null;
|
|
1247
|
+
if (typeof value === "string" || typeof value === "number") return value;
|
|
1248
|
+
if (typeof value === "bigint") return Number(value);
|
|
1249
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
1250
|
+
if (value instanceof Uint8Array) {
|
|
1251
|
+
const copy = new ArrayBuffer(value.byteLength);
|
|
1252
|
+
new Uint8Array(copy).set(value);
|
|
1253
|
+
return copy;
|
|
1254
|
+
}
|
|
1255
|
+
throw new Error(`SQL returned a ${typeof value}, which is not a SqlStorageValue.`);
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* The two narrowings a generic row type needs. `T` is the caller's claim about
|
|
1259
|
+
* the shape of a row SQLite produced at runtime, so no check can confirm it and
|
|
1260
|
+
* upstream does not try — its `Cursor<T>` is the same claim written in a
|
|
1261
|
+
* `JSG_TS_OVERRIDE`. Confined to these two functions so the claim is one place
|
|
1262
|
+
* rather than sprinkled through the cursor.
|
|
1263
|
+
*/
|
|
1264
|
+
function asRow(row) {
|
|
1265
|
+
return row;
|
|
1266
|
+
}
|
|
1267
|
+
function asRawRow(values) {
|
|
1268
|
+
return values;
|
|
1269
|
+
}
|
|
1270
|
+
//#endregion
|
|
1271
|
+
//#region src/api/actor-state.ts
|
|
1272
|
+
/**
|
|
1273
|
+
* ← workerd `src/workerd/api/actor-state.{h,c++}`
|
|
1274
|
+
*
|
|
1275
|
+
* The JS-facing storage objects: `DurableObjectStorageOperations` and its two
|
|
1276
|
+
* subclasses, `DurableObjectFacets`, and `DurableObjectState`. Everything below
|
|
1277
|
+
* this file is reached through one of them.
|
|
1278
|
+
*
|
|
1279
|
+
* **`DurableObjectStorage` satisfies workers-types with no cast (§2.4).** That
|
|
1280
|
+
* was checked rather than asserted, and two shapes here exist only because it
|
|
1281
|
+
* has to: `sql.Cursor` and `sql.Statement` must be constructible with no
|
|
1282
|
+
* arguments (see `sql.ts`), and `storage.kv` is required. The narrowings that
|
|
1283
|
+
* remain are all one thing —
|
|
1284
|
+
* `get<T>` returns the caller's claim about the shape of a value SQLite handed
|
|
1285
|
+
* back as bytes, which no check can confirm and which upstream states the same
|
|
1286
|
+
* way, as a `jsg::JsRef<jsg::JsValue>` behind a `JSG_TS_OVERRIDE`'d
|
|
1287
|
+
* `Promise<T>`. There is no `as unknown as` anywhere in this layer.
|
|
1288
|
+
*
|
|
1289
|
+
* **Every throw is synchronous, including from the promise-returning methods.**
|
|
1290
|
+
* That is upstream's: a `JSG_REQUIRE` inside a method returning `jsg::Promise`
|
|
1291
|
+
* throws into the isolate before the promise exists, so `put(k, undefined)`
|
|
1292
|
+
* throws rather than rejecting. The same goes for a value that will not decode,
|
|
1293
|
+
* because §1.4 makes the SQLite path run the decoder before `Promise.resolve`.
|
|
1294
|
+
*
|
|
1295
|
+
* **What the input gate does and does not do here.** Every entry point calls
|
|
1296
|
+
* `requireInputLock` — see its comment in `io/io-context.ts`, which is the one
|
|
1297
|
+
* place this package decides what an empty invocation stack means. Nothing else
|
|
1298
|
+
* takes a lock: a read returns a value, a write returns a resolved promise, and
|
|
1299
|
+
* `atCheckpointEnd` is what keeps the whole chain inside one transaction
|
|
1300
|
+
* (§1.7.1). The two exceptions are upstream's own — `sync()` and the bookmark
|
|
1301
|
+
* pair release the gate via `awaitIo`, and `transaction()` takes a critical
|
|
1302
|
+
* section.
|
|
1303
|
+
*
|
|
1304
|
+
* **Decision 2's branch has one reachable site**, and it is not where upstream's
|
|
1305
|
+
* is. §1.4 measures that SQLite cache operations are immediate, so their
|
|
1306
|
+
* `kj::OneOf<T, kj::Promise<T>>` branch has nothing to select between.
|
|
1307
|
+
* `transformMaybeBackpressure` keeps the branch because
|
|
1308
|
+
* `DeleteAllResults.backpressure` is still a promise in `io/actor-cache.ts`.
|
|
1309
|
+
*
|
|
1310
|
+
* Not ported, because the substrate has no equivalent: V8's private wire bytes,
|
|
1311
|
+
* replaced by a browser-safe structured-clone encoding with the same public
|
|
1312
|
+
* value semantics; the billing counters
|
|
1313
|
+
* (`billingUnits`, `ActorObserver`, `updateStorageWriteUnit`) and the trace
|
|
1314
|
+
* spans, both already absent throughout; `enableSql`, a workerd namespace option
|
|
1315
|
+
* that exists to simulate a non-SQLite Durable Object; and `ReplicaActorOutgoingFactory`,
|
|
1316
|
+
* whose replication half is a named boundary in `io/actor-cache.ts`.
|
|
1317
|
+
*
|
|
1318
|
+
* Spec: §1.4, §1.5, §1.10, §2.4, §2.5, decisions 2, 4 and 14 in
|
|
1319
|
+
* docs/decisions.md.
|
|
1320
|
+
*/
|
|
1321
|
+
/**
|
|
1322
|
+
* ← `MAX_FACET_NAME_LENGTH` / `MAX_FACET_TREE_DEPTH`
|
|
1323
|
+
* (`actor-state.c++:943,947`), in the anonymous namespace beside the facet code
|
|
1324
|
+
* that enforces them. The scaffolding had them in `server/`, which is neither
|
|
1325
|
+
* where upstream puts them nor where they are checked.
|
|
1326
|
+
*/
|
|
1327
|
+
var FACET_NAME_MAX_LENGTH = 256;
|
|
1328
|
+
/** Root is at depth 0, so the deepest allowed facet is at depth 3. */
|
|
1329
|
+
var FACET_TREE_MAX_DEPTH = 4;
|
|
1330
|
+
/**
|
|
1331
|
+
* ← what falls off the end of `DurableObjectFacets::get`'s class switch
|
|
1332
|
+
* (`actor-state.c++:1029-1043`).
|
|
1333
|
+
*
|
|
1334
|
+
* Upstream accepts three things as `FacetStartupOptions.class`: a bare
|
|
1335
|
+
* `DurableObjectClass`, a `LoopbackDurableObjectNamespace`, or a
|
|
1336
|
+
* `LoopbackColoLocalActorNamespace`, unwrapping the last two through
|
|
1337
|
+
* `getClass()`. All three are ported — the loopback pair by
|
|
1338
|
+
* `api/export-loopback.ts` — and `KJ_UNREACHABLE` is the fourth case there
|
|
1339
|
+
* because JSG has already refused anything else while unwrapping the
|
|
1340
|
+
* `kj::OneOf`. The check has to be written here because
|
|
1341
|
+
* `@cloudflare/workers-types` declares `interface DurableObjectClass<_T> {}`,
|
|
1342
|
+
* which every object satisfies, so nothing refuses it before the method body.
|
|
1343
|
+
*/
|
|
1344
|
+
var FACET_CLASS_UNSUPPORTED_MESSAGE = "facets.get() was given a class this runtime cannot resolve. `class` must be a DurableObjectClass, a LoopbackDurableObjectNamespace or a LoopbackColoLocalActorNamespace — which is what a ctx.exports entry for a Durable Object class is.";
|
|
1345
|
+
/** ← `DurableObjectStorageOperations::OpName`. Named only where an error quotes them. */
|
|
1346
|
+
var OP_GET = "get()";
|
|
1347
|
+
var OP_GET_ALARM = "getAlarm()";
|
|
1348
|
+
var OP_LIST = "list()";
|
|
1349
|
+
var OP_PUT = "put()";
|
|
1350
|
+
var OP_PUT_ALARM = "setAlarm()";
|
|
1351
|
+
var OP_DELETE = "delete()";
|
|
1352
|
+
var OP_DELETE_ALARM = "deleteAlarm()";
|
|
1353
|
+
var OP_ROLLBACK = "rollback()";
|
|
1354
|
+
/** ← `actor-state.c++:455`, verbatim: the one message both overloads' misuse produces. */
|
|
1355
|
+
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)";
|
|
1356
|
+
/**
|
|
1357
|
+
* ← the `kj::OneOf<kj::String, jsg::Dict<…>>` unwrap on put()'s first parameter: `jsg::Dict`
|
|
1358
|
+
* takes any JS object except an Array — functions and Maps included — and `kj::String` takes
|
|
1359
|
+
* everything else by coercion. A type predicate, so the overload split narrows without a cast.
|
|
1360
|
+
*/
|
|
1361
|
+
function isEntriesArgument(value) {
|
|
1362
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && !Array.isArray(value);
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* ← the struct wrapper (`jsg/struct.h:246-258`), which is NOT the Dict wrapper: `PutOptions` is
|
|
1366
|
+
* all-optional fields, so `null` unwraps to default options, and any object does — arrays and
|
|
1367
|
+
* functions included, because the wrapper checks `IsObject()` with no Array exclusion. Only a
|
|
1368
|
+
* non-null primitive fails to unwrap. Measured on real workerd: `put({k: 1}, null)`, `…, [])`
|
|
1369
|
+
* and `…, function () {})` all write, and `put({k: 1}, "v")` alone is the overload error.
|
|
1370
|
+
*/
|
|
1371
|
+
function isPutOptions(value) {
|
|
1372
|
+
return value === null || typeof value === "object" || typeof value === "function";
|
|
1373
|
+
}
|
|
1374
|
+
/** The key immediately after `k` in byte order is `k` plus this. */
|
|
1375
|
+
var NULL_CHARACTER = "\0";
|
|
1376
|
+
/** ← the `0xff` upstream strips from the tail of a prefix, in UTF-16 code units. */
|
|
1377
|
+
var MAX_CODE_UNIT = 65535;
|
|
1378
|
+
var textEncoder$1 = new TextEncoder();
|
|
1379
|
+
var textDecoder = new TextDecoder();
|
|
1380
|
+
/** A byte JSON could never begin with, `DO`, and the local codec version. */
|
|
1381
|
+
var VALUE_CODEC_HEADER = new Uint8Array([
|
|
1382
|
+
0,
|
|
1383
|
+
68,
|
|
1384
|
+
79,
|
|
1385
|
+
1
|
|
1386
|
+
]);
|
|
1387
|
+
/**
|
|
1388
|
+
* ← `serializeV8Value`. The wire bytes differ because V8's serializer is not
|
|
1389
|
+
* available in browsers; the public structured-clone value semantics do not.
|
|
1390
|
+
* The short header keeps the new representation unambiguous while old JSON rows
|
|
1391
|
+
* remain readable.
|
|
1392
|
+
*/
|
|
1393
|
+
function serializeValue(value) {
|
|
1394
|
+
const body = textEncoder$1.encode(JSON.stringify(serialize(value)));
|
|
1395
|
+
const encoded = new Uint8Array(VALUE_CODEC_HEADER.byteLength + body.byteLength);
|
|
1396
|
+
encoded.set(VALUE_CODEC_HEADER);
|
|
1397
|
+
encoded.set(body, VALUE_CODEC_HEADER.byteLength);
|
|
1398
|
+
return encoded;
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* ← `deserializeV8Value`.
|
|
1402
|
+
*
|
|
1403
|
+
* Upstream logs "the key (to help find the data in the database if it hasn't
|
|
1404
|
+
* been deleted), the length of the value, and the first three bytes of the value
|
|
1405
|
+
* (which is just the v8-internal version header and the tag that indicates the
|
|
1406
|
+
* type of the value, but not its contents)". Our four-byte header carries only
|
|
1407
|
+
* a marker and version for the same reason.
|
|
1408
|
+
*/
|
|
1409
|
+
function deserializeValue(key, buffer) {
|
|
1410
|
+
if (buffer.byteLength === 0) throw new Error(`unexpectedly empty value buffer; key = ${key}`);
|
|
1411
|
+
try {
|
|
1412
|
+
const structured = VALUE_CODEC_HEADER.every((byte, index) => buffer[index] === byte);
|
|
1413
|
+
const bytes = structured ? buffer.subarray(VALUE_CODEC_HEADER.byteLength) : buffer;
|
|
1414
|
+
const parsed = JSON.parse(textDecoder.decode(bytes));
|
|
1415
|
+
return structured ? deserialize(parsed) : parsed;
|
|
1416
|
+
} catch (exception) {
|
|
1417
|
+
throw new Error(`actor storage deserialization failed: failed to deserialize stored value; key = ${key}; size = ${buffer.byteLength}`, { cause: exception });
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* ← `transformMaybeBackpressure` (`actor-state.c++:103-119`). THIS is decision
|
|
1422
|
+
* 2's live site: `DeleteAllResults.backpressure` is still `Promise<void> |
|
|
1423
|
+
* undefined`, so the branch has something to select between.
|
|
1424
|
+
*
|
|
1425
|
+
* Upstream's own note, kept because it is the reason the flag is threaded here
|
|
1426
|
+
* at all: "In practice `allowConcurrency` will have no effect on a backpressure
|
|
1427
|
+
* promise since backpressure blocks everything anyway, but we pass the option
|
|
1428
|
+
* through for consistency in case of future changes."
|
|
1429
|
+
*/
|
|
1430
|
+
function transformMaybeBackpressure(ctx, options, maybeBackpressure) {
|
|
1431
|
+
if (maybeBackpressure === void 0) return Promise.resolve();
|
|
1432
|
+
if (options.allowConcurrency === true) return ctx.awaitIo(maybeBackpressure);
|
|
1433
|
+
return ctx.awaitIoWithInputLock(maybeBackpressure, () => {});
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* ← `DurableObjectStorageOperations::compileListOptions`
|
|
1437
|
+
* (`actor-state.c++:314-417`). Returns undefined if the list operation would
|
|
1438
|
+
* provably return no results. `SyncKvStorage` reuses it, exactly as upstream's
|
|
1439
|
+
* comment says it must.
|
|
1440
|
+
*
|
|
1441
|
+
* Two translations. `startAfter` gains ONE null character where upstream's
|
|
1442
|
+
* `kj::String` gains two, because the second of upstream's is the terminator and
|
|
1443
|
+
* a JS string has none. And every comparison here is on UTF-16 code units where
|
|
1444
|
+
* upstream's is on UTF-8 bytes, while the range the database actually applies is
|
|
1445
|
+
* SQLite's `BINARY` collation over UTF-8 — the two orders agree for every key
|
|
1446
|
+
* outside the astral planes, and a key that mixes astral characters with a
|
|
1447
|
+
* prefix can land on the wrong side of a clamp this function computes.
|
|
1448
|
+
*/
|
|
1449
|
+
function compileListOptions(options) {
|
|
1450
|
+
let start = "";
|
|
1451
|
+
let end;
|
|
1452
|
+
let reverse = false;
|
|
1453
|
+
let limit;
|
|
1454
|
+
if (options !== void 0) {
|
|
1455
|
+
if (options.start !== void 0) {
|
|
1456
|
+
if (options.startAfter !== void 0) throw new TypeError("list() cannot be called with both start and startAfter values.");
|
|
1457
|
+
start = options.start;
|
|
1458
|
+
}
|
|
1459
|
+
if (options.startAfter !== void 0) start = options.startAfter + NULL_CHARACTER;
|
|
1460
|
+
if (options.end !== void 0) end = options.end;
|
|
1461
|
+
if (options.reverse !== void 0) reverse = options.reverse;
|
|
1462
|
+
if (options.limit !== void 0) {
|
|
1463
|
+
if (!(options.limit > 0)) throw new TypeError("List limit must be positive.");
|
|
1464
|
+
limit = options.limit;
|
|
1465
|
+
}
|
|
1466
|
+
const prefix = options.prefix;
|
|
1467
|
+
if (prefix !== void 0 && prefix.length > 0) {
|
|
1468
|
+
if (start < prefix) start = prefix;
|
|
1469
|
+
else if (start.startsWith(prefix)) {} else return;
|
|
1470
|
+
const keyAfterPrefix = firstKeyAfterPrefix(prefix);
|
|
1471
|
+
if (keyAfterPrefix === void 0) {} else if (end === void 0) end = keyAfterPrefix;
|
|
1472
|
+
else if (end <= prefix) return;
|
|
1473
|
+
else if (end.startsWith(prefix)) {} else end = keyAfterPrefix;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (end !== void 0 && end <= start) return;
|
|
1477
|
+
return {
|
|
1478
|
+
start,
|
|
1479
|
+
end,
|
|
1480
|
+
reverse,
|
|
1481
|
+
limit
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* ← the `keyAfterPrefix` vector: strip maximal trailing units, then increment.
|
|
1486
|
+
*
|
|
1487
|
+
* Returns undefined when the prefix is nothing but maximal units, which is
|
|
1488
|
+
* upstream's "the prefix is a string of some number of 0xff bytes, so includes
|
|
1489
|
+
* the entire key space up through the last possible key".
|
|
1490
|
+
*/
|
|
1491
|
+
function firstKeyAfterPrefix(prefix) {
|
|
1492
|
+
let head = prefix;
|
|
1493
|
+
while (head.length > 0 && head.charCodeAt(head.length - 1) === MAX_CODE_UNIT) head = head.slice(0, -1);
|
|
1494
|
+
if (head.length === 0) return void 0;
|
|
1495
|
+
return head.slice(0, -1) + String.fromCharCode(head.charCodeAt(head.length - 1) + 1);
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* ← workerd `src/workerd/api/sync-kv.{h,c++}`. The synchronous surface lives
|
|
1499
|
+
* beside the asynchronous storage owner because both share the same codec and
|
|
1500
|
+
* list-option compiler over one `SqliteKv`.
|
|
1501
|
+
*/
|
|
1502
|
+
var SyncKvStorage = class {
|
|
1503
|
+
#ctx;
|
|
1504
|
+
#kv;
|
|
1505
|
+
constructor(ctx, kv) {
|
|
1506
|
+
this.#ctx = ctx;
|
|
1507
|
+
this.#kv = kv;
|
|
1508
|
+
}
|
|
1509
|
+
get(key) {
|
|
1510
|
+
requireInputLock(this.#ctx, "kv.get()");
|
|
1511
|
+
const value = this.#kv.get(key);
|
|
1512
|
+
if (value === void 0) return void 0;
|
|
1513
|
+
return deserializeValue(key, value);
|
|
1514
|
+
}
|
|
1515
|
+
list(options) {
|
|
1516
|
+
requireInputLock(this.#ctx, "kv.list()");
|
|
1517
|
+
const compiled = compileListOptions(options);
|
|
1518
|
+
if (compiled === void 0) return [];
|
|
1519
|
+
return listIterator(this.#kv.list(compiled.start, compiled.end, compiled.limit, compiled.reverse ? "REVERSE" : "FORWARD"));
|
|
1520
|
+
}
|
|
1521
|
+
put(key, value) {
|
|
1522
|
+
requireInputLock(this.#ctx, "kv.put()");
|
|
1523
|
+
this.#kv.put(key, serializeValue(value));
|
|
1524
|
+
}
|
|
1525
|
+
delete(key) {
|
|
1526
|
+
requireInputLock(this.#ctx, "kv.delete()");
|
|
1527
|
+
return this.#kv.delete(key);
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
/** ← `SyncKvStorage::listNext`, whose cancellation branch is the reason it is not a plain loop. */
|
|
1531
|
+
function* listIterator(cursor) {
|
|
1532
|
+
for (;;) {
|
|
1533
|
+
const pair = cursor.next();
|
|
1534
|
+
if (pair !== void 0) {
|
|
1535
|
+
yield [pair.key, deserializeValue(pair.key, pair.value)];
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
if (cursor.wasCanceled()) throw new Error("kv.list() iterator was invalidated because a new call to kv.list() was started. Only one kv.list() iterator can exist at a time.");
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* ← `DurableObjectStorageOperations`. "Common implementation of
|
|
1544
|
+
* DurableObjectStorage and DurableObjectTransaction. This class is designed to
|
|
1545
|
+
* be used as a mixin."
|
|
1546
|
+
*/
|
|
1547
|
+
var DurableObjectStorageOperations = class {
|
|
1548
|
+
ctx;
|
|
1549
|
+
constructor(ctx) {
|
|
1550
|
+
this.ctx = ctx;
|
|
1551
|
+
}
|
|
1552
|
+
get(keyOrKeys, maybeOptions) {
|
|
1553
|
+
requireInputLock(this.ctx, OP_GET);
|
|
1554
|
+
const options = { ...maybeOptions };
|
|
1555
|
+
if (typeof keyOrKeys === "string") return this.#getOne(keyOrKeys, options);
|
|
1556
|
+
return this.#getMultiple(keyOrKeys, options);
|
|
1557
|
+
}
|
|
1558
|
+
getAlarm(maybeOptions) {
|
|
1559
|
+
requireInputLock(this.ctx, OP_GET_ALARM);
|
|
1560
|
+
const options = {
|
|
1561
|
+
...maybeOptions,
|
|
1562
|
+
noCache: false
|
|
1563
|
+
};
|
|
1564
|
+
return Promise.resolve(this.getCache(OP_GET_ALARM).getAlarm(options));
|
|
1565
|
+
}
|
|
1566
|
+
list(maybeOptions) {
|
|
1567
|
+
requireInputLock(this.ctx, OP_LIST);
|
|
1568
|
+
const compiled = compileListOptions(maybeOptions);
|
|
1569
|
+
if (compiled === void 0) return Promise.resolve(/* @__PURE__ */ new Map());
|
|
1570
|
+
const options = { ...maybeOptions };
|
|
1571
|
+
const cache = this.getCache(OP_LIST);
|
|
1572
|
+
const result = compiled.reverse ? cache.listReverse(compiled.start, compiled.end, compiled.limit, options) : cache.list(compiled.start, compiled.end, compiled.limit, options);
|
|
1573
|
+
return Promise.resolve(listResultsToMap(result));
|
|
1574
|
+
}
|
|
1575
|
+
put(keyOrEntries, valueOrOptions, maybeOptions) {
|
|
1576
|
+
requireInputLock(this.ctx, OP_PUT);
|
|
1577
|
+
if (!isEntriesArgument(keyOrEntries)) {
|
|
1578
|
+
if (valueOrOptions === void 0) throw new TypeError("put() called with undefined value.");
|
|
1579
|
+
return this.#putOne(`${keyOrEntries}`, valueOrOptions, { ...maybeOptions });
|
|
1580
|
+
}
|
|
1581
|
+
if (valueOrOptions !== void 0 && !isPutOptions(valueOrOptions)) throw new TypeError(PUT_OVERLOAD_MESSAGE);
|
|
1582
|
+
return this.#putMultiple(keyOrEntries, { ...valueOrOptions });
|
|
1583
|
+
}
|
|
1584
|
+
delete(keyOrKeys, maybeOptions) {
|
|
1585
|
+
requireInputLock(this.ctx, OP_DELETE);
|
|
1586
|
+
const options = { ...maybeOptions };
|
|
1587
|
+
if (typeof keyOrKeys === "string") return Promise.resolve(this.getCache(OP_DELETE).delete(keyOrKeys, options));
|
|
1588
|
+
return Promise.resolve(this.getCache(OP_DELETE).deleteMultiple(keyOrKeys, options));
|
|
1589
|
+
}
|
|
1590
|
+
setAlarm(scheduledTime, maybeOptions) {
|
|
1591
|
+
requireInputLock(this.ctx, OP_PUT_ALARM);
|
|
1592
|
+
const when = scheduledTime instanceof Date ? scheduledTime.getTime() : scheduledTime;
|
|
1593
|
+
if (!(when > 0)) throw new TypeError("setAlarm() cannot be called with an alarm time <= 0");
|
|
1594
|
+
this.ctx.getActorOrThrow().assertCanSetAlarm();
|
|
1595
|
+
const options = {
|
|
1596
|
+
...maybeOptions,
|
|
1597
|
+
noCache: false
|
|
1598
|
+
};
|
|
1599
|
+
this.getCache(OP_PUT_ALARM).setAlarm(Math.max(when, this.ctx.now()), options);
|
|
1600
|
+
return Promise.resolve();
|
|
1601
|
+
}
|
|
1602
|
+
deleteAlarm(maybeOptions) {
|
|
1603
|
+
requireInputLock(this.ctx, OP_DELETE_ALARM);
|
|
1604
|
+
const options = {
|
|
1605
|
+
...maybeOptions,
|
|
1606
|
+
noCache: false
|
|
1607
|
+
};
|
|
1608
|
+
this.getCache(OP_DELETE_ALARM).setAlarm(null, options);
|
|
1609
|
+
return Promise.resolve();
|
|
1610
|
+
}
|
|
1611
|
+
#getOne(key, options) {
|
|
1612
|
+
const value = this.getCache(OP_GET).get(key, options);
|
|
1613
|
+
return Promise.resolve(value === void 0 ? void 0 : deserializeValue(key, value));
|
|
1614
|
+
}
|
|
1615
|
+
#getMultiple(keys, options) {
|
|
1616
|
+
const result = this.getCache(OP_GET).getMultiple(keys, options);
|
|
1617
|
+
return Promise.resolve(listResultsToMap(result));
|
|
1618
|
+
}
|
|
1619
|
+
#putOne(key, value, options) {
|
|
1620
|
+
this.getCache(OP_PUT).put(key, serializeValue(value), options);
|
|
1621
|
+
return Promise.resolve();
|
|
1622
|
+
}
|
|
1623
|
+
#putMultiple(entries, options) {
|
|
1624
|
+
const pairs = [];
|
|
1625
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
1626
|
+
if (value === void 0) continue;
|
|
1627
|
+
pairs.push({
|
|
1628
|
+
key,
|
|
1629
|
+
value: serializeValue(value)
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
this.getCache(OP_PUT).putMultiple(pairs, options);
|
|
1633
|
+
return Promise.resolve();
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
/** ← `listResultsToMap` and `getMultipleResultsToMap`, minus the billing halves. */
|
|
1637
|
+
function listResultsToMap(rows) {
|
|
1638
|
+
const map = /* @__PURE__ */ new Map();
|
|
1639
|
+
for (const entry of rows) map.set(entry.key, deserializeValue(entry.key, entry.value));
|
|
1640
|
+
return map;
|
|
1641
|
+
}
|
|
1642
|
+
var DurableObjectStorage = class extends DurableObjectStorageOperations {
|
|
1643
|
+
#cache;
|
|
1644
|
+
#sql;
|
|
1645
|
+
#kv;
|
|
1646
|
+
constructor(ctx, cache) {
|
|
1647
|
+
super(ctx);
|
|
1648
|
+
this.#cache = cache;
|
|
1649
|
+
}
|
|
1650
|
+
/** ← `DurableObjectStorage::getActorCacheInterface`, which `DurableObjectState::abort` needs. */
|
|
1651
|
+
getActorCacheInterface() {
|
|
1652
|
+
return this.#cache;
|
|
1653
|
+
}
|
|
1654
|
+
/** ← `DurableObjectStorage::getSqliteDb`. Always SQLite-backed here; see the header. */
|
|
1655
|
+
getSqliteDb() {
|
|
1656
|
+
return this.#cache.getSqliteDatabase();
|
|
1657
|
+
}
|
|
1658
|
+
getCache() {
|
|
1659
|
+
return this.#cache;
|
|
1660
|
+
}
|
|
1661
|
+
/** ← `JSG_LAZY_INSTANCE_PROPERTY(sql, getSql)`. */
|
|
1662
|
+
get sql() {
|
|
1663
|
+
this.#sql ??= new SqlStorage(this.ctx, this);
|
|
1664
|
+
return this.#sql;
|
|
1665
|
+
}
|
|
1666
|
+
/** ← `JSG_LAZY_INSTANCE_PROPERTY(kv, getKv)`. */
|
|
1667
|
+
get kv() {
|
|
1668
|
+
this.#kv ??= new SyncKvStorage(this.ctx, this.#cache.getSqliteKv());
|
|
1669
|
+
return this.#kv;
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* ← `DurableObjectStorage::deleteAll`.
|
|
1673
|
+
*
|
|
1674
|
+
* `deleteAlarm` is upstream's `FeatureFlags::get(js).getDeleteAllDeletesAlarm()`,
|
|
1675
|
+
* a compatibility flag that exists so Workers published before it keep the old
|
|
1676
|
+
* behaviour. A runtime with no deployed history takes the current behaviour.
|
|
1677
|
+
*/
|
|
1678
|
+
deleteAll(maybeOptions) {
|
|
1679
|
+
requireInputLock(this.ctx, "deleteAll()");
|
|
1680
|
+
const options = { ...maybeOptions };
|
|
1681
|
+
const result = this.#cache.deleteAll(options, { deleteAlarm: true });
|
|
1682
|
+
return transformMaybeBackpressure(this.ctx, options, result.backpressure);
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* ← `DurableObjectStorage::transaction`.
|
|
1686
|
+
*
|
|
1687
|
+
* The critical section is load bearing and upstream says why: "the call to
|
|
1688
|
+
* `startTransaction()` is when the SQLite-backed implementation will actually
|
|
1689
|
+
* invoke `BEGIN TRANSACTION`, so it's important that we're inside the
|
|
1690
|
+
* blockConcurrencyWhile block before that point so we don't accidentally catch
|
|
1691
|
+
* some other asynchronous event in our transaction."
|
|
1692
|
+
*
|
|
1693
|
+
* The exception is packed into the result rather than thrown out of the
|
|
1694
|
+
* section, and then rethrown outside it. Upstream's reason: "We don't actually
|
|
1695
|
+
* want to reset the object, we only want to roll back the transaction and
|
|
1696
|
+
* propagate the exception." A throw out of a critical section permanently
|
|
1697
|
+
* breaks the input gate (§1.5), so a failing transaction callback would
|
|
1698
|
+
* destroy the actor.
|
|
1699
|
+
*/
|
|
1700
|
+
transaction(closure) {
|
|
1701
|
+
requireInputLock(this.ctx, "transaction()");
|
|
1702
|
+
return this.ctx.blockConcurrencyWhile(async () => {
|
|
1703
|
+
const txn = new DurableObjectTransaction(this.ctx, this.#cache.startTransaction());
|
|
1704
|
+
try {
|
|
1705
|
+
const value = await closure(txn);
|
|
1706
|
+
txn.maybeCommit();
|
|
1707
|
+
return {
|
|
1708
|
+
isError: false,
|
|
1709
|
+
value
|
|
1710
|
+
};
|
|
1711
|
+
} catch (exception) {
|
|
1712
|
+
txn.maybeRollback();
|
|
1713
|
+
return {
|
|
1714
|
+
isError: true,
|
|
1715
|
+
exception
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
}).then((result) => {
|
|
1719
|
+
if (result.isError) throw result.exception;
|
|
1720
|
+
return result.value;
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
/** ← `DurableObjectStorage::transactionSync`, a forward for the reason above. */
|
|
1724
|
+
transactionSync(callback) {
|
|
1725
|
+
requireInputLock(this.ctx, "transactionSync()");
|
|
1726
|
+
return this.#cache.transactionSync(callback);
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* ← `DurableObjectStorage::sync`.
|
|
1730
|
+
*
|
|
1731
|
+
* Upstream's `awaitIo` rather than `awaitIoWithInputLock`, which is the one
|
|
1732
|
+
* storage method that deliberately opens the gate: "we're merely checking if
|
|
1733
|
+
* we have any pending or in-flight operations, and providing a promise that
|
|
1734
|
+
* resolves when they succeed."
|
|
1735
|
+
*/
|
|
1736
|
+
sync() {
|
|
1737
|
+
requireInputLock(this.ctx, "sync()");
|
|
1738
|
+
return this.ctx.awaitIo(this.#cache.onNoPendingFlush());
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Real, not a boundary: `ActorSqlite`'s is "an ersatz implementation that's
|
|
1742
|
+
* good enough for local dev with D1's Session API", built on the metadata
|
|
1743
|
+
* table's local-development bookmark. Anything above this package that
|
|
1744
|
+
* surfaces it to an application should know it is a counter and not a
|
|
1745
|
+
* recovery point — as it is on workerd.
|
|
1746
|
+
*/
|
|
1747
|
+
getCurrentBookmark() {
|
|
1748
|
+
requireInputLock(this.ctx, "getCurrentBookmark()");
|
|
1749
|
+
return this.ctx.awaitIo(this.#cache.getCurrentBookmark());
|
|
1750
|
+
}
|
|
1751
|
+
waitForBookmark(bookmark) {
|
|
1752
|
+
requireInputLock(this.ctx, "waitForBookmark()");
|
|
1753
|
+
return this.ctx.awaitIo(this.#cache.waitForBookmark(bookmark));
|
|
1754
|
+
}
|
|
1755
|
+
/** Substrate boundary: point-in-time recovery. Upstream reaches the cache directly, as this does. */
|
|
1756
|
+
getBookmarkForTime(timestamp) {
|
|
1757
|
+
return this.#cache.getBookmarkForTime(timestamp instanceof Date ? timestamp.getTime() : timestamp);
|
|
1758
|
+
}
|
|
1759
|
+
/** Substrate boundary: point-in-time recovery. */
|
|
1760
|
+
onNextSessionRestoreBookmark(bookmark) {
|
|
1761
|
+
return this.#cache.onNextSessionRestoreBookmark(bookmark);
|
|
1762
|
+
}
|
|
1763
|
+
/** Substrate boundary: replication. */
|
|
1764
|
+
ensureReplicas() {
|
|
1765
|
+
this.#cache.ensureReplicas();
|
|
1766
|
+
}
|
|
1767
|
+
/** Substrate boundary: replication. */
|
|
1768
|
+
disableReplicas() {
|
|
1769
|
+
this.#cache.disableReplicas();
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* ← `DurableObjectStorage::getPrimary` / `isReplica`. `maybePrimary` is set
|
|
1773
|
+
* only by the replica constructor, and nothing constructs a replica here, so
|
|
1774
|
+
* these answer upstream's own non-replica case rather than a stubbed one.
|
|
1775
|
+
*/
|
|
1776
|
+
getPrimary() {}
|
|
1777
|
+
isReplica() {
|
|
1778
|
+
return false;
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
var DurableObjectTransaction = class extends DurableObjectStorageOperations {
|
|
1782
|
+
/** Becomes undefined when committed or rolled back. */
|
|
1783
|
+
#cacheTxn;
|
|
1784
|
+
#rolledBack = false;
|
|
1785
|
+
constructor(ctx, cacheTxn) {
|
|
1786
|
+
super(ctx);
|
|
1787
|
+
this.#cacheTxn = cacheTxn;
|
|
1788
|
+
}
|
|
1789
|
+
getCache(op) {
|
|
1790
|
+
if (this.#rolledBack) throw new Error(`Cannot ${op} on rolled back transaction`);
|
|
1791
|
+
const txn = this.#cacheTxn;
|
|
1792
|
+
if (txn === void 0) throw new Error(`Cannot call ${op} on transaction that has already committed: did you move \`txn\` outside of the closure?`);
|
|
1793
|
+
return txn;
|
|
1794
|
+
}
|
|
1795
|
+
/** Called from JS. */
|
|
1796
|
+
rollback() {
|
|
1797
|
+
if (this.#rolledBack) return;
|
|
1798
|
+
this.getCache(OP_ROLLBACK);
|
|
1799
|
+
const txn = this.#cacheTxn;
|
|
1800
|
+
if (txn !== void 0) {
|
|
1801
|
+
txn.rollback();
|
|
1802
|
+
txn.drop();
|
|
1803
|
+
this.#cacheTxn = void 0;
|
|
1804
|
+
}
|
|
1805
|
+
this.#rolledBack = true;
|
|
1806
|
+
}
|
|
1807
|
+
/** Just throws an exception saying this isn't supported. */
|
|
1808
|
+
deleteAll() {
|
|
1809
|
+
throw new Error("Cannot call deleteAll() within a transaction");
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Called from the runtime, not JS, after the transaction callback has
|
|
1813
|
+
* completed. Does nothing if the transaction is already committed or rolled
|
|
1814
|
+
* back. Synchronous, because `ActorCacheTransaction::commit` is (§1.4).
|
|
1815
|
+
*/
|
|
1816
|
+
maybeCommit() {
|
|
1817
|
+
const txn = this.#cacheTxn;
|
|
1818
|
+
if (txn === void 0) return;
|
|
1819
|
+
this.#cacheTxn = void 0;
|
|
1820
|
+
txn.commit();
|
|
1821
|
+
txn.drop();
|
|
1822
|
+
}
|
|
1823
|
+
/** Same, for the failure path. Upstream's drops the transaction, whose destructor rolls back. */
|
|
1824
|
+
maybeRollback() {
|
|
1825
|
+
const txn = this.#cacheTxn;
|
|
1826
|
+
this.#cacheTxn = void 0;
|
|
1827
|
+
this.#rolledBack = true;
|
|
1828
|
+
txn?.drop();
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
/**
|
|
1832
|
+
* ← `requireValidFacetName` (`actor-state.c++:949-952`).
|
|
1833
|
+
*
|
|
1834
|
+
* The comparison is `name.size()` on a `kj::StringPtr`, which is **UTF-8 bytes**,
|
|
1835
|
+
* so it is measured in bytes here too — the same `TextEncoder` pass, for the same
|
|
1836
|
+
* reason, that `ColoLocalActorNamespace.get`'s `[1, 2048]` bound already costs.
|
|
1837
|
+
* Comparing `name.length` accepts a 256-character non-ASCII name that upstream
|
|
1838
|
+
* refuses, which is a bound a caller can hit.
|
|
1839
|
+
*/
|
|
1840
|
+
function requireValidFacetName(name) {
|
|
1841
|
+
if (textEncoder$1.encode(name).length > 256) throw new TypeError(`Facet name is too long (max 256 characters).`);
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* ← the `KJ_SWITCH_ONEOF(options.$class)` lambda (`actor-state.c++:1029-1043`).
|
|
1845
|
+
*
|
|
1846
|
+
* Three arms, and the order matters for the same reason it does upstream: a
|
|
1847
|
+
* `LoopbackDurableObjectClass` *is* a `DurableObjectClass`, so it takes the bare
|
|
1848
|
+
* arm here exactly as JSG's `kj::OneOf` unwraps it into the first alternative.
|
|
1849
|
+
* The two loopback namespaces are not classes and carry one, which `getClass()`
|
|
1850
|
+
* hands back.
|
|
1851
|
+
*
|
|
1852
|
+
* A `ctx.exports` entry is the callable façade `api/export-loopback.ts` produces
|
|
1853
|
+
* rather than the instance itself, and every check below is an `instanceof` that
|
|
1854
|
+
* the façade's `getPrototypeOf` answers — which is why that trap exists.
|
|
1855
|
+
*/
|
|
1856
|
+
function requireFacetClass(actorClass) {
|
|
1857
|
+
if (actorClass instanceof DurableObjectClass) return actorClass;
|
|
1858
|
+
if (actorClass instanceof LoopbackDurableObjectNamespace) return actorClass.getClass();
|
|
1859
|
+
if (actorClass instanceof LoopbackColoLocalActorNamespace) return actorClass.getClass();
|
|
1860
|
+
throw new TypeError(FACET_CLASS_UNSUPPORTED_MESSAGE);
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* ← `DurableObjectFacets`.
|
|
1864
|
+
*
|
|
1865
|
+
* **`clone` is the fourth method, and the vendored C++ snapshot does not have
|
|
1866
|
+
* it.** The design record cites `actor-state.h:431-497` and
|
|
1867
|
+
* `server.c++:721-749`; neither line range contains it, `DurableObjectFacets`
|
|
1868
|
+
* there exposes exactly `get`, `abort` and `delete`, and
|
|
1869
|
+
* `Worker::Actor::FacetManager` has exactly `getDepth`, `getFacet`, `abortFacet`
|
|
1870
|
+
* and `deleteFacet`. It is real all the same: `@cloudflare/workers-types`
|
|
1871
|
+
* 4.20260702.1 — a month newer than the snapshot — declares
|
|
1872
|
+
* `clone(src: string, dst: string): void` on `DurableObjectFacets`. So the
|
|
1873
|
+
* signature comes from the types and the semantics from §1.10 (abort dst, delete
|
|
1874
|
+
* dst storage, recursive copy of the src subtree), and the orchestration is
|
|
1875
|
+
* `server/`'s `cloneFacet`. There is nothing upstream to check the body against,
|
|
1876
|
+
* which makes it the one method here with no reference — worth knowing when it
|
|
1877
|
+
* is wrong.
|
|
1878
|
+
*/
|
|
1879
|
+
var DurableObjectFacets = class {
|
|
1880
|
+
#ctx;
|
|
1881
|
+
#facetManager;
|
|
1882
|
+
#parentId;
|
|
1883
|
+
constructor(ctx, facetManager, parentId) {
|
|
1884
|
+
this.#ctx = ctx;
|
|
1885
|
+
this.#facetManager = facetManager;
|
|
1886
|
+
this.#parentId = parentId;
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Get a facet by name, starting it if it isn't already running.
|
|
1890
|
+
* `getStartupOptions` is invoked only if the facet wasn't already running.
|
|
1891
|
+
*
|
|
1892
|
+
* Returns a `Fetcher` instead of a `DurableObject` because the returned stub
|
|
1893
|
+
* does not have the `id` or `name` methods that a DO stub normally has.
|
|
1894
|
+
*/
|
|
1895
|
+
get(name, getStartupOptions) {
|
|
1896
|
+
requireValidFacetName(name);
|
|
1897
|
+
const facetManager = this.#getFacetManager();
|
|
1898
|
+
if (facetManager.getDepth() + 1 >= 4) throw new Error(`Facet nesting depth limit exceeded. The maximum depth including the root Durable Object is 4.`);
|
|
1899
|
+
requireInputLock(this.#ctx, "facets.get()");
|
|
1900
|
+
const getStartInfo = this.#ctx.makeReentryCallback(async () => {
|
|
1901
|
+
const options = await getStartupOptions();
|
|
1902
|
+
const id = options.id;
|
|
1903
|
+
return {
|
|
1904
|
+
actorClass: requireFacetClass(options.class).getChannel(),
|
|
1905
|
+
id: id === void 0 ? this.#parentId : typeof id === "string" ? id : id.name ?? id.toString()
|
|
1906
|
+
};
|
|
1907
|
+
});
|
|
1908
|
+
return facetManager.getFacet(name, getStartInfo);
|
|
1909
|
+
}
|
|
1910
|
+
abort(name, reason) {
|
|
1911
|
+
requireValidFacetName(name);
|
|
1912
|
+
this.#getFacetManager().abortFacet(name, reason);
|
|
1913
|
+
}
|
|
1914
|
+
delete(name) {
|
|
1915
|
+
requireValidFacetName(name);
|
|
1916
|
+
this.#getFacetManager().deleteFacet(name);
|
|
1917
|
+
}
|
|
1918
|
+
clone(src, dst) {
|
|
1919
|
+
requireValidFacetName(src);
|
|
1920
|
+
requireValidFacetName(dst);
|
|
1921
|
+
this.#getFacetManager().cloneFacet(src, dst);
|
|
1922
|
+
}
|
|
1923
|
+
#getFacetManager() {
|
|
1924
|
+
const facetManager = this.#facetManager;
|
|
1925
|
+
if (facetManager === void 0) throw new Error("This Durable Object does not support creating facets.");
|
|
1926
|
+
return facetManager;
|
|
1927
|
+
}
|
|
1928
|
+
};
|
|
1929
|
+
/** The type passed as the first parameter to a Durable Object class's constructor. */
|
|
1930
|
+
var DurableObjectState = class {
|
|
1931
|
+
#ctx;
|
|
1932
|
+
#options;
|
|
1933
|
+
#facets;
|
|
1934
|
+
constructor(ctx, options) {
|
|
1935
|
+
this.#ctx = ctx;
|
|
1936
|
+
this.#options = options;
|
|
1937
|
+
}
|
|
1938
|
+
get id() {
|
|
1939
|
+
return this.#options.id;
|
|
1940
|
+
}
|
|
1941
|
+
get props() {
|
|
1942
|
+
return this.#options.props;
|
|
1943
|
+
}
|
|
1944
|
+
/** ← `JSG_LAZY_INSTANCE_PROPERTY(exports, getExports)`, behind `enableCtxExports` upstream. */
|
|
1945
|
+
get exports() {
|
|
1946
|
+
return this.#options.exports;
|
|
1947
|
+
}
|
|
1948
|
+
get version() {
|
|
1949
|
+
return this.#options.version;
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* NO upstream correspondence, because upstream needs none: a
|
|
1953
|
+
* `ServiceWorkerGlobalScope` IS the isolate's global object there, so an
|
|
1954
|
+
* actor's class reaches its gated `setTimeout` by writing `setTimeout`.
|
|
1955
|
+
*
|
|
1956
|
+
* Here one realm hosts several actors, so the names on `globalThis` can only
|
|
1957
|
+
* be bound to one of them and a continuation cannot be asked which one it
|
|
1958
|
+
* belongs to. `ctx` is the one reference every Durable Object class already
|
|
1959
|
+
* holds and that already means exactly one actor — the constructor was handed
|
|
1960
|
+
* it — so it is where the scope goes. An actor's method writes
|
|
1961
|
+
* `this.ctx.globals.setTimeout(…)`; a free function it calls takes the scope
|
|
1962
|
+
* as a parameter.
|
|
1963
|
+
*
|
|
1964
|
+
* `installActorScope` still exists and is still what a host uses for a
|
|
1965
|
+
* dynamically-loaded Worker source, which has no `ctx` to reach through and
|
|
1966
|
+
* its own module scope to destructure into. The two are the same object.
|
|
1967
|
+
*/
|
|
1968
|
+
get globals() {
|
|
1969
|
+
return this.#options.globals;
|
|
1970
|
+
}
|
|
1971
|
+
get storage() {
|
|
1972
|
+
const storage = this.#options.storage;
|
|
1973
|
+
if (storage === void 0) throw new Error("This Durable Object does not have storage.");
|
|
1974
|
+
return storage;
|
|
1975
|
+
}
|
|
1976
|
+
/** ← `JSG_LAZY_INSTANCE_PROPERTY(facets, getFacets)`. */
|
|
1977
|
+
get facets() {
|
|
1978
|
+
this.#facets ??= new DurableObjectFacets(this.#ctx, this.#options.facets, this.#options.id.toString());
|
|
1979
|
+
return this.#facets;
|
|
1980
|
+
}
|
|
1981
|
+
waitUntil(promise) {
|
|
1982
|
+
this.#ctx.addWaitUntil(promise.then(() => {}));
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* ← `DurableObjectState::blockConcurrencyWhile` (`actor-state.c++:1128-1131`),
|
|
1986
|
+
* which is a one-line forward and nothing else. The 30-second deadline, the
|
|
1987
|
+
* brokenness annotation and the never-settled promise on failure all live in
|
|
1988
|
+
* `IoContext::blockConcurrencyWhile`, which Section 2 already implements.
|
|
1989
|
+
*
|
|
1990
|
+
* Its precondition comes with it: `IoContext::blockConcurrencyWhile` calls
|
|
1991
|
+
* `getInputLock()`, which asserts, so this is reachable only from inside a
|
|
1992
|
+
* gated slice.
|
|
1993
|
+
*/
|
|
1994
|
+
blockConcurrencyWhile(callback) {
|
|
1995
|
+
return this.#ctx.blockConcurrencyWhile(callback);
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* ← `DurableObjectState::abort`. Reset the object, including breaking the
|
|
1999
|
+
* output gate and canceling any writes that haven't been committed yet.
|
|
2000
|
+
*
|
|
2001
|
+
* `js.terminateExecutionNow()` has no port — there is no isolate to terminate —
|
|
2002
|
+
* so the caller's own slice keeps running to its next await, where `IoContext`
|
|
2003
|
+
* refuses to re-enter.
|
|
2004
|
+
*/
|
|
2005
|
+
abort(reason) {
|
|
2006
|
+
const description = reason === void 0 ? "broken.outputGateBroken; jsg.Error: Application called abort() to reset Durable Object." : `broken.outputGateBroken; jsg.Error: ${reason}`;
|
|
2007
|
+
const error = new Error(description);
|
|
2008
|
+
setUserErrorDetail(error);
|
|
2009
|
+
this.#options.storage?.getActorCacheInterface().shutdown(error);
|
|
2010
|
+
this.#ctx.abort(error);
|
|
2011
|
+
}
|
|
2012
|
+
/** ← `DurableObjectState::getPrimaryStub`. Non-null only for a replica; see the storage note. */
|
|
2013
|
+
get primaryStub() {
|
|
2014
|
+
return this.#options.storage?.getPrimary();
|
|
2015
|
+
}
|
|
2016
|
+
/** Substrate boundary: replication. */
|
|
2017
|
+
configureReadReplication(options) {
|
|
2018
|
+
const storage = this.#options.storage;
|
|
2019
|
+
if (storage === void 0) throw new TypeError("This actor does not support read replication.");
|
|
2020
|
+
if (storage.isReplica()) throw new Error("Replica Durable Objects cannot call configureReadReplication().");
|
|
2021
|
+
if (options.mode !== "auto" && options.mode !== "disabled") throw new TypeError(`configureReadReplication() called with unknown mode setting: ${options.mode}.`);
|
|
2022
|
+
return this.#ctx.awaitIo(storage.getActorCacheInterface().configureReadReplication(options.mode === "auto"));
|
|
2023
|
+
}
|
|
2024
|
+
acceptWebSocket(ws, tags) {
|
|
2025
|
+
this.#options.webSockets.acceptWebSocket(ws, tags);
|
|
2026
|
+
}
|
|
2027
|
+
getWebSockets(tag) {
|
|
2028
|
+
return tag === void 0 ? this.#options.webSockets.getWebSockets() : this.#options.webSockets.getWebSockets(tag);
|
|
2029
|
+
}
|
|
2030
|
+
setWebSocketAutoResponse(maybeReqResp) {
|
|
2031
|
+
this.#options.webSockets.setWebSocketAutoResponse(maybeReqResp);
|
|
2032
|
+
}
|
|
2033
|
+
getWebSocketAutoResponse() {
|
|
2034
|
+
return this.#options.webSockets.getWebSocketAutoResponse();
|
|
2035
|
+
}
|
|
2036
|
+
getWebSocketAutoResponseTimestamp(ws) {
|
|
2037
|
+
return this.#options.webSockets.getWebSocketAutoResponseTimestamp(ws);
|
|
2038
|
+
}
|
|
2039
|
+
setHibernatableWebSocketEventTimeout(timeoutMs) {
|
|
2040
|
+
this.#options.webSockets.setHibernatableWebSocketEventTimeout(timeoutMs);
|
|
2041
|
+
}
|
|
2042
|
+
getHibernatableWebSocketEventTimeout() {
|
|
2043
|
+
return this.#options.webSockets.getHibernatableWebSocketEventTimeout();
|
|
2044
|
+
}
|
|
2045
|
+
getTags(ws) {
|
|
2046
|
+
return this.#options.webSockets.getTags(ws);
|
|
2047
|
+
}
|
|
2048
|
+
};
|
|
2049
|
+
//#endregion
|
|
2050
|
+
//#region src/api/web-socket.ts
|
|
2051
|
+
var WebSocketRequestResponsePairImpl = class {
|
|
2052
|
+
#request;
|
|
2053
|
+
#response;
|
|
2054
|
+
constructor(request, response) {
|
|
2055
|
+
this.#request = String(request);
|
|
2056
|
+
this.#response = String(response);
|
|
2057
|
+
}
|
|
2058
|
+
get request() {
|
|
2059
|
+
return this.#request;
|
|
2060
|
+
}
|
|
2061
|
+
get response() {
|
|
2062
|
+
return this.#response;
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
var RuntimeWebSocketRequestResponsePair = new Proxy(WebSocketRequestResponsePairImpl, { apply() {
|
|
2066
|
+
throw new TypeError("Failed to construct 'WebSocketRequestResponsePair': Please use the 'new' operator, this DOM object constructor cannot be called as a function.");
|
|
2067
|
+
} });
|
|
2068
|
+
/** ← the `JSG_REQUIRE(!native.state.is<Accepted>(), ...)` at the head of `accept()`. */
|
|
2069
|
+
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.";
|
|
2070
|
+
var HIBERNATION_ALREADY_ACCEPTED_MESSAGE = "Cannot call `acceptWebSocket()` if the WebSocket was already accepted via `accept()`";
|
|
2071
|
+
var HIBERNATION_AFTER_ACCEPT_MESSAGE = "Can't accept() WebSocket after enabling hibernation.";
|
|
2072
|
+
var HIBERNATION_PAIR_USED_MESSAGE = "Cannot call `acceptWebSocket()` on this WebSocket because its pair has already been accepted or used in a Response.";
|
|
2073
|
+
var MAX_HIBERNATABLE_SOCKETS = 32768;
|
|
2074
|
+
var MAX_TAGS = 10;
|
|
2075
|
+
var MAX_TAG_LENGTH = 256;
|
|
2076
|
+
var MAX_ATTACHMENT_BYTES = 16384;
|
|
2077
|
+
var MAX_AUTO_RESPONSE_BYTES = 2048;
|
|
2078
|
+
var MAX_EVENT_TIMEOUT = 6048e5;
|
|
2079
|
+
var MAX_CLOSE_REASON_BYTES = 123;
|
|
2080
|
+
var WEB_SOCKET_READY_STATES = {
|
|
2081
|
+
READY_STATE_CONNECTING: 0,
|
|
2082
|
+
READY_STATE_OPEN: 1,
|
|
2083
|
+
READY_STATE_CLOSING: 2,
|
|
2084
|
+
READY_STATE_CLOSED: 3,
|
|
2085
|
+
CONNECTING: 0,
|
|
2086
|
+
OPEN: 1,
|
|
2087
|
+
CLOSING: 2,
|
|
2088
|
+
CLOSED: 3
|
|
2089
|
+
};
|
|
2090
|
+
var textEncoder = new TextEncoder();
|
|
2091
|
+
var SOCKET_EVENTS = [
|
|
2092
|
+
"open",
|
|
2093
|
+
"message",
|
|
2094
|
+
"close",
|
|
2095
|
+
"error"
|
|
2096
|
+
];
|
|
2097
|
+
var metadata = /* @__PURE__ */ new WeakMap();
|
|
2098
|
+
function socketMetadata(socket) {
|
|
2099
|
+
let value = metadata.get(socket);
|
|
2100
|
+
if (value === void 0) {
|
|
2101
|
+
value = {};
|
|
2102
|
+
metadata.set(socket, value);
|
|
2103
|
+
}
|
|
2104
|
+
return value;
|
|
2105
|
+
}
|
|
2106
|
+
function isRawWebSocket(value) {
|
|
2107
|
+
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";
|
|
2108
|
+
}
|
|
2109
|
+
function requireWebSocket(value, operation) {
|
|
2110
|
+
if (!isRawWebSocket(value)) throw new TypeError(`Failed to execute '${operation}' on 'WebSocket': parameter 1 is not of type 'WebSocket'.`);
|
|
2111
|
+
return value;
|
|
2112
|
+
}
|
|
2113
|
+
function serializeAttachment(socket, value) {
|
|
2114
|
+
try {
|
|
2115
|
+
structuredClone(value);
|
|
2116
|
+
} catch (error) {
|
|
2117
|
+
if (error instanceof DOMException && error.name === "DataCloneError") throw new DOMException(error.message.replace(/^Failed to execute 'structuredClone' on '[^']+': /, ""), "DataCloneError");
|
|
2118
|
+
throw error;
|
|
2119
|
+
}
|
|
2120
|
+
const bytes = serializeValue(value);
|
|
2121
|
+
const measuredBytes = typeof value === "string" ? textEncoder.encode(value).byteLength + 5 : bytes.byteLength;
|
|
2122
|
+
if (measuredBytes > MAX_ATTACHMENT_BYTES) throw new Error(`A WebSocket 'attachment' cannot be larger than ${MAX_ATTACHMENT_BYTES} bytes.'attachment' was ${measuredBytes} bytes.`);
|
|
2123
|
+
const state = socketMetadata(socket);
|
|
2124
|
+
state.attachment = bytes;
|
|
2125
|
+
if (state.accepted?.mode === "hibernatable") state.accepted.registry.attachmentChanged(socket, bytes);
|
|
2126
|
+
}
|
|
2127
|
+
function deserializeAttachment(socket) {
|
|
2128
|
+
const attachment = socketMetadata(socket).attachment;
|
|
2129
|
+
if (attachment === void 0) return null;
|
|
2130
|
+
return deserializeValue("WebSocket attachment", attachment);
|
|
2131
|
+
}
|
|
2132
|
+
function serializeAttachmentMethod(value) {
|
|
2133
|
+
if (arguments.length === 0) throw new TypeError("Failed to execute 'serializeAttachment' on 'WebSocket': parameter 1 is not of type 'Value'.");
|
|
2134
|
+
serializeAttachment(requireWebSocket(this, "serializeAttachment"), value);
|
|
2135
|
+
}
|
|
2136
|
+
function deserializeAttachmentMethod() {
|
|
2137
|
+
return deserializeAttachment(requireWebSocket(this, "deserializeAttachment"));
|
|
2138
|
+
}
|
|
2139
|
+
function cloneMessageData(data) {
|
|
2140
|
+
if (typeof data === "string" || data instanceof Blob) return data;
|
|
2141
|
+
if (data instanceof ArrayBuffer) return data.slice(0);
|
|
2142
|
+
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice().buffer;
|
|
2143
|
+
return String(data);
|
|
2144
|
+
}
|
|
2145
|
+
var MemoryWebSocketEndpoint = class extends EventTarget {
|
|
2146
|
+
peer;
|
|
2147
|
+
#sentClose = false;
|
|
2148
|
+
send(data) {
|
|
2149
|
+
this.peer.dispatchEvent(new MessageEvent("message", { data: cloneMessageData(data) }));
|
|
2150
|
+
}
|
|
2151
|
+
close(code = 1e3, reason = "") {
|
|
2152
|
+
if (this.#sentClose) return;
|
|
2153
|
+
this.#sentClose = true;
|
|
2154
|
+
this.peer.dispatchEvent(new CloseEvent("close", {
|
|
2155
|
+
code,
|
|
2156
|
+
reason,
|
|
2157
|
+
wasClean: true
|
|
2158
|
+
}));
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
/** One public socket identity, in classic or hibernatable mode after acceptance. */
|
|
2162
|
+
var AcceptedWebSocket = class AcceptedWebSocket extends EventTarget {
|
|
2163
|
+
static READY_STATE_CONNECTING = WEB_SOCKET_READY_STATES.READY_STATE_CONNECTING;
|
|
2164
|
+
static READY_STATE_OPEN = WEB_SOCKET_READY_STATES.READY_STATE_OPEN;
|
|
2165
|
+
static READY_STATE_CLOSING = WEB_SOCKET_READY_STATES.READY_STATE_CLOSING;
|
|
2166
|
+
static READY_STATE_CLOSED = WEB_SOCKET_READY_STATES.READY_STATE_CLOSED;
|
|
2167
|
+
static CONNECTING = WEB_SOCKET_READY_STATES.CONNECTING;
|
|
2168
|
+
static OPEN = WEB_SOCKET_READY_STATES.OPEN;
|
|
2169
|
+
static CLOSING = WEB_SOCKET_READY_STATES.CLOSING;
|
|
2170
|
+
static CLOSED = WEB_SOCKET_READY_STATES.CLOSED;
|
|
2171
|
+
bufferedAmount = 0;
|
|
2172
|
+
extensions = "";
|
|
2173
|
+
protocol = "";
|
|
2174
|
+
url = "";
|
|
2175
|
+
#ctx;
|
|
2176
|
+
#socket;
|
|
2177
|
+
#pairState;
|
|
2178
|
+
#delivery = { mode: "pending" };
|
|
2179
|
+
#pump = Promise.resolve();
|
|
2180
|
+
#pending = [];
|
|
2181
|
+
#readyState = AcceptedWebSocket.OPEN;
|
|
2182
|
+
#ownClose = false;
|
|
2183
|
+
#peerClose = false;
|
|
2184
|
+
#binaryType = "blob";
|
|
2185
|
+
onopen = null;
|
|
2186
|
+
onmessage = null;
|
|
2187
|
+
onclose = null;
|
|
2188
|
+
onerror = null;
|
|
2189
|
+
constructor(ctx, socket, pairState) {
|
|
2190
|
+
super();
|
|
2191
|
+
this.#ctx = ctx;
|
|
2192
|
+
this.#socket = socket;
|
|
2193
|
+
this.#pairState = pairState;
|
|
2194
|
+
if (pairState === void 0) this.#enableClassic();
|
|
2195
|
+
for (const type of SOCKET_EVENTS) socket.addEventListener(type, (event) => {
|
|
2196
|
+
this.#receive(type, event);
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
get readyState() {
|
|
2200
|
+
return this.#readyState;
|
|
2201
|
+
}
|
|
2202
|
+
get binaryType() {
|
|
2203
|
+
return this.#binaryType;
|
|
2204
|
+
}
|
|
2205
|
+
set binaryType(value) {
|
|
2206
|
+
this.#binaryType = value;
|
|
2207
|
+
}
|
|
2208
|
+
accept() {
|
|
2209
|
+
if (this.#delivery.mode === "hibernatable") throw new TypeError(HIBERNATION_AFTER_ACCEPT_MESSAGE);
|
|
2210
|
+
if (this.#delivery.mode === "classic") throw new Error(ALREADY_ACCEPTED_MESSAGE);
|
|
2211
|
+
if (this.#pairState !== void 0) this.#pairState.used = true;
|
|
2212
|
+
this.#enableClassic();
|
|
2213
|
+
}
|
|
2214
|
+
send(data) {
|
|
2215
|
+
if (this.#delivery.mode === "hibernatable" && this.#ownClose) throw new TypeError("Can't call WebSocket send() after close().");
|
|
2216
|
+
if (this.#peerClose || this.#readyState === AcceptedWebSocket.CLOSED) return;
|
|
2217
|
+
this.#markPairUsed();
|
|
2218
|
+
this.#enqueue(() => this.#socket.send(data));
|
|
2219
|
+
}
|
|
2220
|
+
close(code, reason = "") {
|
|
2221
|
+
if (this.#readyState === AcceptedWebSocket.CLOSED || this.#ownClose) return;
|
|
2222
|
+
if (this.#delivery.mode === "hibernatable" || this.#pairState !== void 0) validateClose(code, reason);
|
|
2223
|
+
this.#markPairUsed();
|
|
2224
|
+
this.#ownClose = true;
|
|
2225
|
+
this.#readyState = this.#peerClose ? AcceptedWebSocket.CLOSED : AcceptedWebSocket.CLOSING;
|
|
2226
|
+
this.#enqueue(() => this.#socket.close(code, reason));
|
|
2227
|
+
}
|
|
2228
|
+
serializeAttachment(value) {
|
|
2229
|
+
if (arguments.length === 0) serializeAttachmentMethod.call(this);
|
|
2230
|
+
else serializeAttachment(this, value);
|
|
2231
|
+
}
|
|
2232
|
+
deserializeAttachment() {
|
|
2233
|
+
return deserializeAttachment(this);
|
|
2234
|
+
}
|
|
2235
|
+
acceptHibernation(registry) {
|
|
2236
|
+
if (this.#delivery.mode !== "pending") throw new Error(HIBERNATION_ALREADY_ACCEPTED_MESSAGE);
|
|
2237
|
+
if (this.#pairState?.used === true && !this.#pairState.hibernationAccepted) throw new Error(HIBERNATION_PAIR_USED_MESSAGE);
|
|
2238
|
+
if (this.#pairState !== void 0) {
|
|
2239
|
+
this.#pairState.used = true;
|
|
2240
|
+
this.#pairState.hibernationAccepted = true;
|
|
2241
|
+
}
|
|
2242
|
+
this.#activateHibernation(registry, this.#ctx);
|
|
2243
|
+
}
|
|
2244
|
+
rehydrateHibernation(registry, ctx) {
|
|
2245
|
+
this.#activateHibernation(registry, ctx);
|
|
2246
|
+
}
|
|
2247
|
+
#activateHibernation(registry, ctx) {
|
|
2248
|
+
this.#ctx = ctx;
|
|
2249
|
+
this.#delivery = {
|
|
2250
|
+
mode: "hibernatable",
|
|
2251
|
+
registry
|
|
2252
|
+
};
|
|
2253
|
+
this.#pending = [];
|
|
2254
|
+
}
|
|
2255
|
+
markPairUsed() {
|
|
2256
|
+
this.#markPairUsed();
|
|
2257
|
+
}
|
|
2258
|
+
#markPairUsed() {
|
|
2259
|
+
if (this.#pairState !== void 0) this.#pairState.used = true;
|
|
2260
|
+
}
|
|
2261
|
+
#enableClassic() {
|
|
2262
|
+
const delivery = {
|
|
2263
|
+
mode: "classic",
|
|
2264
|
+
criticalSection: this.#ctx.getCriticalSection()
|
|
2265
|
+
};
|
|
2266
|
+
this.#delivery = delivery;
|
|
2267
|
+
socketMetadata(this).accepted = { mode: "classic" };
|
|
2268
|
+
const pending = this.#pending;
|
|
2269
|
+
this.#pending = [];
|
|
2270
|
+
for (const item of pending) this.#deliverClassic(item.type, item.event, delivery.criticalSection);
|
|
2271
|
+
}
|
|
2272
|
+
#receive(type, event) {
|
|
2273
|
+
if (type === "close") {
|
|
2274
|
+
this.#receiveClose(event);
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
const delivery = this.#delivery;
|
|
2278
|
+
if (delivery.mode === "pending") this.#pending.push({
|
|
2279
|
+
type,
|
|
2280
|
+
event
|
|
2281
|
+
});
|
|
2282
|
+
else if (delivery.mode === "classic") this.#deliverClassic(type, event, delivery.criticalSection);
|
|
2283
|
+
else delivery.registry.receive(this, type, event);
|
|
2284
|
+
}
|
|
2285
|
+
#receiveClose(event) {
|
|
2286
|
+
if (this.#ownClose) this.#readyState = AcceptedWebSocket.CLOSED;
|
|
2287
|
+
else {
|
|
2288
|
+
this.#peerClose = true;
|
|
2289
|
+
this.#readyState = AcceptedWebSocket.CLOSING;
|
|
2290
|
+
if (this.#delivery.mode === "classic" && this.#pairState !== void 0) {
|
|
2291
|
+
if (event.code === 1005 || event.code === 1006 || event.code === 1015) this.#readyState = AcceptedWebSocket.CLOSED;
|
|
2292
|
+
else this.close(event.code, event.reason);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
const delivery = this.#delivery;
|
|
2296
|
+
if (delivery.mode === "pending") this.#pending.push({
|
|
2297
|
+
type: "close",
|
|
2298
|
+
event
|
|
2299
|
+
});
|
|
2300
|
+
else if (delivery.mode === "classic") this.#deliverClassic("close", event, delivery.criticalSection);
|
|
2301
|
+
else delivery.registry.receive(this, "close", event);
|
|
2302
|
+
}
|
|
2303
|
+
#deliverClassic(type, event, criticalSection) {
|
|
2304
|
+
this.#ctx.addWaitUntil(this.#ctx.run(() => {
|
|
2305
|
+
const delivered = cloneEventFor(type, event);
|
|
2306
|
+
this.dispatchEvent(delivered);
|
|
2307
|
+
const handler = this[`on${type}`];
|
|
2308
|
+
handler?.(delivered);
|
|
2309
|
+
}, { input: criticalSection }));
|
|
2310
|
+
}
|
|
2311
|
+
#enqueue(write) {
|
|
2312
|
+
const outputLock = this.#ctx.waitForOutputLocks();
|
|
2313
|
+
this.#pump = this.#pump.then(async () => {
|
|
2314
|
+
await outputLock;
|
|
2315
|
+
write();
|
|
2316
|
+
});
|
|
2317
|
+
this.#ctx.addWaitUntil(this.#pump);
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
for (const [name, value] of Object.entries(WEB_SOCKET_READY_STATES)) Object.defineProperty(AcceptedWebSocket.prototype, name, {
|
|
2321
|
+
value,
|
|
2322
|
+
enumerable: true
|
|
2323
|
+
});
|
|
2324
|
+
var HibernatableWebSocketRegistry = class {
|
|
2325
|
+
#ctx;
|
|
2326
|
+
#dispatch;
|
|
2327
|
+
#host;
|
|
2328
|
+
#entries = [];
|
|
2329
|
+
#autoResponse = null;
|
|
2330
|
+
#eventTimeout = null;
|
|
2331
|
+
#pairConstructor;
|
|
2332
|
+
constructor(ctx, dispatch, host, rehydrated = []) {
|
|
2333
|
+
this.#ctx = ctx;
|
|
2334
|
+
this.#dispatch = dispatch;
|
|
2335
|
+
this.#host = host;
|
|
2336
|
+
for (const value of rehydrated) this.#rehydrate(value);
|
|
2337
|
+
}
|
|
2338
|
+
get WebSocketPair() {
|
|
2339
|
+
const registry = this;
|
|
2340
|
+
this.#pairConstructor ??= new Proxy(class WebSocketPair {}, { construct: () => registry.#createPair() });
|
|
2341
|
+
return this.#pairConstructor;
|
|
2342
|
+
}
|
|
2343
|
+
acceptWebSocket(socket, tags) {
|
|
2344
|
+
if (!isRawWebSocket(socket)) throw new TypeError("Failed to execute 'acceptWebSocket' on 'DurableObjectState': parameter 1 is not of type 'WebSocket'.");
|
|
2345
|
+
const state = socketMetadata(socket);
|
|
2346
|
+
if (state.accepted !== void 0) throw new Error(HIBERNATION_ALREADY_ACCEPTED_MESSAGE);
|
|
2347
|
+
if (this.#entries.length >= MAX_HIBERNATABLE_SOCKETS) throw new Error(`only ${MAX_HIBERNATABLE_SOCKETS} websockets can be accepted on a single Durable Object instance`);
|
|
2348
|
+
const normalizedTags = normalizeTags(tags);
|
|
2349
|
+
if (socket instanceof AcceptedWebSocket) socket.acceptHibernation(this);
|
|
2350
|
+
else this.#listenRaw(socket);
|
|
2351
|
+
state.accepted = {
|
|
2352
|
+
mode: "hibernatable",
|
|
2353
|
+
registry: this
|
|
2354
|
+
};
|
|
2355
|
+
this.#entries.push({
|
|
2356
|
+
socket,
|
|
2357
|
+
tags: normalizedTags
|
|
2358
|
+
});
|
|
2359
|
+
this.#host?.accepted(socket, normalizedTags);
|
|
2360
|
+
}
|
|
2361
|
+
getWebSockets(tag) {
|
|
2362
|
+
if (arguments.length > 0) {
|
|
2363
|
+
if (typeof tag !== "string") return [];
|
|
2364
|
+
return this.#entries.filter((entry) => entry.tags.includes(tag)).map((entry) => entry.socket);
|
|
2365
|
+
}
|
|
2366
|
+
return this.#entries.map((entry) => entry.socket).reverse();
|
|
2367
|
+
}
|
|
2368
|
+
getTags(socket) {
|
|
2369
|
+
const state = isRawWebSocket(socket) ? metadata.get(socket) : void 0;
|
|
2370
|
+
if (state?.accepted === void 0) throw new Error("you must call 'acceptWebSocket()' before attempting to access the tags of a WebSocket.");
|
|
2371
|
+
if (state.accepted.mode !== "hibernatable") throw new Error("only hibernatable websockets can have tags.");
|
|
2372
|
+
const entry = this.#entries.find((candidate) => candidate.socket === socket);
|
|
2373
|
+
if (entry === void 0) throw new Error("you must call 'acceptWebSocket()' before attempting to access the tags of a WebSocket.");
|
|
2374
|
+
return [...entry.tags];
|
|
2375
|
+
}
|
|
2376
|
+
setWebSocketAutoResponse(pair) {
|
|
2377
|
+
if (pair === void 0) {
|
|
2378
|
+
this.#autoResponse = null;
|
|
2379
|
+
this.#host?.autoResponse(null);
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
if (!(pair instanceof WebSocketRequestResponsePairImpl)) throw new TypeError("Failed to execute 'setWebSocketAutoResponse' on 'DurableObjectState': parameter 1 is not of type 'WebSocketRequestResponsePair'.");
|
|
2383
|
+
validateAutoResponseSize("Request", pair.request);
|
|
2384
|
+
validateAutoResponseSize("Response", pair.response);
|
|
2385
|
+
this.#autoResponse = pair;
|
|
2386
|
+
this.#host?.autoResponse({
|
|
2387
|
+
request: pair.request,
|
|
2388
|
+
response: pair.response
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
getWebSocketAutoResponse() {
|
|
2392
|
+
const pair = this.#autoResponse;
|
|
2393
|
+
return pair === null ? null : new RuntimeWebSocketRequestResponsePair(pair.request, pair.response);
|
|
2394
|
+
}
|
|
2395
|
+
getWebSocketAutoResponseTimestamp(socket) {
|
|
2396
|
+
if (!isRawWebSocket(socket)) throw new TypeError("Failed to execute 'getWebSocketAutoResponseTimestamp' on 'DurableObjectState': parameter 1 is not of type 'WebSocket'.");
|
|
2397
|
+
const timestamp = this.#entries.find((entry) => entry.socket === socket)?.autoResponseTimestamp;
|
|
2398
|
+
return timestamp === void 0 ? null : new Date(timestamp);
|
|
2399
|
+
}
|
|
2400
|
+
setHibernatableWebSocketEventTimeout(value) {
|
|
2401
|
+
if (value === void 0 || Number(value) === 0) {
|
|
2402
|
+
this.#eventTimeout = null;
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
const number = Number(value);
|
|
2406
|
+
if (Number.isNaN(number)) throw new TypeError("The value cannot be converted because it is not an integer.");
|
|
2407
|
+
if (number < 0) throw new TypeError("The value cannot be converted because it is negative and this API expects a positive number.");
|
|
2408
|
+
if (number > 4294967295) throw new TypeError("Value out of range. Must be less than or equal to 4294967295.");
|
|
2409
|
+
const timeout = Math.trunc(number);
|
|
2410
|
+
if (timeout > MAX_EVENT_TIMEOUT) throw new Error(`Event timeout should not exceed ${MAX_EVENT_TIMEOUT} ms.`);
|
|
2411
|
+
this.#eventTimeout = timeout;
|
|
2412
|
+
}
|
|
2413
|
+
getHibernatableWebSocketEventTimeout() {
|
|
2414
|
+
return this.#eventTimeout;
|
|
2415
|
+
}
|
|
2416
|
+
attachmentChanged(socket, bytes) {
|
|
2417
|
+
if (this.#entries.some((entry) => entry.socket === socket)) this.#host?.attachment(socket, bytes);
|
|
2418
|
+
}
|
|
2419
|
+
receive(socket, type, event) {
|
|
2420
|
+
const entry = this.#entries.find((candidate) => candidate.socket === socket);
|
|
2421
|
+
if (entry === void 0) return;
|
|
2422
|
+
if (type === "message") {
|
|
2423
|
+
const data = event.data;
|
|
2424
|
+
if (typeof data === "string" && data === this.#autoResponse?.request) {
|
|
2425
|
+
entry.autoResponseTimestamp = this.#ctx.now();
|
|
2426
|
+
socket.send(this.#autoResponse.response);
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
const message = cloneMessageData(data);
|
|
2430
|
+
if (message instanceof Blob) {
|
|
2431
|
+
this.#ctx.addWaitUntil(message.arrayBuffer().then((buffer) => {
|
|
2432
|
+
this.#schedule(() => this.#dispatch.message(socket, buffer));
|
|
2433
|
+
}));
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
this.#schedule(() => this.#dispatch.message(socket, message));
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
if (type === "close") {
|
|
2440
|
+
this.#remove(entry);
|
|
2441
|
+
const close = event;
|
|
2442
|
+
this.#schedule(() => this.#dispatch.close(socket, close.code, close.reason, close.wasClean));
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
if (type === "error") this.#schedule(() => this.#dispatch.error(socket, event));
|
|
2446
|
+
}
|
|
2447
|
+
#schedule(handler) {
|
|
2448
|
+
this.#ctx.addWaitUntil(this.#ctx.run(handler).then(() => {}));
|
|
2449
|
+
}
|
|
2450
|
+
#remove(entry) {
|
|
2451
|
+
const index = this.#entries.indexOf(entry);
|
|
2452
|
+
if (index === -1) return;
|
|
2453
|
+
this.#entries.splice(index, 1);
|
|
2454
|
+
this.#host?.closed(entry.socket);
|
|
2455
|
+
}
|
|
2456
|
+
#listenRaw(socket) {
|
|
2457
|
+
const state = socketMetadata(socket);
|
|
2458
|
+
if (state.rawListenersInstalled === true) return;
|
|
2459
|
+
state.rawListenersInstalled = true;
|
|
2460
|
+
for (const type of [
|
|
2461
|
+
"message",
|
|
2462
|
+
"close",
|
|
2463
|
+
"error"
|
|
2464
|
+
]) socket.addEventListener(type, (event) => {
|
|
2465
|
+
const accepted = socketMetadata(socket).accepted;
|
|
2466
|
+
if (accepted?.mode === "hibernatable") accepted.registry.receive(socket, type, event);
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
#rehydrate(value) {
|
|
2470
|
+
const socket = value.socket;
|
|
2471
|
+
if (!isRawWebSocket(socket)) throw new TypeError("ActorContainerOptions.webSockets contains a non-WebSocket value.");
|
|
2472
|
+
const tags = normalizeTags(value.tags);
|
|
2473
|
+
const state = socketMetadata(socket);
|
|
2474
|
+
state.accepted = {
|
|
2475
|
+
mode: "hibernatable",
|
|
2476
|
+
registry: this
|
|
2477
|
+
};
|
|
2478
|
+
if (value.attachment !== void 0) state.attachment = value.attachment.slice();
|
|
2479
|
+
if (socket instanceof AcceptedWebSocket) socket.rehydrateHibernation(this, this.#ctx);
|
|
2480
|
+
else this.#listenRaw(socket);
|
|
2481
|
+
const entry = {
|
|
2482
|
+
socket,
|
|
2483
|
+
tags
|
|
2484
|
+
};
|
|
2485
|
+
if (value.autoResponseTimestamp !== void 0) entry.autoResponseTimestamp = value.autoResponseTimestamp;
|
|
2486
|
+
this.#entries.push(entry);
|
|
2487
|
+
}
|
|
2488
|
+
#createPair() {
|
|
2489
|
+
const pairState = {
|
|
2490
|
+
used: false,
|
|
2491
|
+
hibernationAccepted: false
|
|
2492
|
+
};
|
|
2493
|
+
const left = new MemoryWebSocketEndpoint();
|
|
2494
|
+
const right = new MemoryWebSocketEndpoint();
|
|
2495
|
+
left.peer = right;
|
|
2496
|
+
right.peer = left;
|
|
2497
|
+
return {
|
|
2498
|
+
0: new AcceptedWebSocket(this.#ctx, left, pairState),
|
|
2499
|
+
1: new AcceptedWebSocket(this.#ctx, right, pairState)
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
function acceptWebSocket(ctx, socket) {
|
|
2504
|
+
const state = socketMetadata(socket);
|
|
2505
|
+
if (state.accepted !== void 0) throw new Error(ALREADY_ACCEPTED_MESSAGE);
|
|
2506
|
+
if (socket instanceof AcceptedWebSocket) {
|
|
2507
|
+
socket.accept();
|
|
2508
|
+
return socket;
|
|
2509
|
+
}
|
|
2510
|
+
state.accepted = { mode: "classic" };
|
|
2511
|
+
return new AcceptedWebSocket(ctx, socket);
|
|
2512
|
+
}
|
|
2513
|
+
function markWebSocketUsed(socket) {
|
|
2514
|
+
if (socket instanceof AcceptedWebSocket) socket.markPairUsed();
|
|
2515
|
+
}
|
|
2516
|
+
function installWebSocketGlobals(target, pairConstructor) {
|
|
2517
|
+
const constructor = globalThis.WebSocket;
|
|
2518
|
+
for (const [name, value] of Object.entries(WEB_SOCKET_READY_STATES)) {
|
|
2519
|
+
defineValue(constructor, name, value);
|
|
2520
|
+
defineValue(constructor.prototype, name, value);
|
|
2521
|
+
}
|
|
2522
|
+
defineValue(constructor.prototype, "serializeAttachment", serializeAttachmentMethod);
|
|
2523
|
+
defineValue(constructor.prototype, "deserializeAttachment", deserializeAttachmentMethod);
|
|
2524
|
+
defineValue(target, "WebSocket", constructor);
|
|
2525
|
+
defineValue(target, "WebSocketPair", pairConstructor);
|
|
2526
|
+
defineValue(target, "WebSocketRequestResponsePair", RuntimeWebSocketRequestResponsePair);
|
|
2527
|
+
}
|
|
2528
|
+
function defineValue(target, name, value) {
|
|
2529
|
+
if (Object.getOwnPropertyDescriptor(target, name)?.configurable === false) return;
|
|
2530
|
+
Object.defineProperty(target, name, {
|
|
2531
|
+
configurable: true,
|
|
2532
|
+
writable: true,
|
|
2533
|
+
value
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
function normalizeTags(tags) {
|
|
2537
|
+
if (tags === void 0) return [];
|
|
2538
|
+
if (!Array.isArray(tags)) throw new TypeError("Failed to execute 'acceptWebSocket' on 'DurableObjectState': parameter 2 is not of type 'Array'.");
|
|
2539
|
+
if (tags.length > MAX_TAGS) throw new Error(`a Hibernatable WebSocket cannot have more than ${MAX_TAGS} tags`);
|
|
2540
|
+
const normalized = [...new Set(tags.map(String))];
|
|
2541
|
+
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).`);
|
|
2542
|
+
return normalized;
|
|
2543
|
+
}
|
|
2544
|
+
function validateAutoResponseSize(side, value) {
|
|
2545
|
+
const bytes = textEncoder.encode(value).byteLength;
|
|
2546
|
+
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.`);
|
|
2547
|
+
}
|
|
2548
|
+
function validateClose(code, reason) {
|
|
2549
|
+
if (code !== void 0 && code !== 1e3 && (code < 3e3 || code > 4999)) throw new DOMException(`Invalid WebSocket close code: ${code}.`, "InvalidAccessError");
|
|
2550
|
+
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");
|
|
2551
|
+
}
|
|
2552
|
+
function cloneEventFor(type, event) {
|
|
2553
|
+
if (type === "message") {
|
|
2554
|
+
const source = event;
|
|
2555
|
+
return new MessageEvent("message", {
|
|
2556
|
+
data: source.data,
|
|
2557
|
+
origin: source.origin,
|
|
2558
|
+
lastEventId: source.lastEventId
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
if (type === "close") {
|
|
2562
|
+
const source = event;
|
|
2563
|
+
return new CloseEvent("close", {
|
|
2564
|
+
code: source.code,
|
|
2565
|
+
reason: source.reason,
|
|
2566
|
+
wasClean: source.wasClean
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
return new Event(type);
|
|
2570
|
+
}
|
|
2571
|
+
//#endregion
|
|
2572
|
+
export { DurableObjectClass as _, installWebSocketGlobals as a, DurableObjectStorage as c, LoopbackColoLocalActorNamespace as d, LoopbackDurableObjectClass as f, ACTOR_CLASS_SERIALIZATION_UNIMPLEMENTED_MESSAGE as g, asLoopbackDurableObjectClass as h, acceptWebSocket as i, FACET_NAME_MAX_LENGTH as l, LoopbackServiceStub as m, HibernatableWebSocketRegistry as n, markWebSocketUsed as o, LoopbackDurableObjectNamespace as p, RuntimeWebSocketRequestResponsePair as r, DurableObjectState as s, ALREADY_ACCEPTED_MESSAGE as t, FACET_TREE_MAX_DEPTH as u, DurableObjectId as v, DurableObjectNamespace as y };
|
|
2573
|
+
|
|
2574
|
+
//# sourceMappingURL=web-socket-W63BdFn5.js.map
|