@pluv/platform-cloudflare 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +15 -15
- package/CHANGELOG.md +78 -0
- package/dist/index.d.mts +32 -10
- package/dist/index.d.ts +32 -10
- package/dist/index.js +238 -19
- package/dist/index.mjs +239 -18
- package/package.json +9 -9
- package/src/CloudflarePlatform.ts +114 -4
- package/src/CloudflareWebSocket.ts +48 -13
- package/src/PersistanceCloudflare.ts +100 -0
- package/src/constants.ts +3 -0
- package/src/createPluvHandler.ts +26 -12
- package/src/index.ts +2 -1
- package/src/platformCloudflare.ts +3 -2
- package/src/utils/index.ts +1 -0
- package/src/utils/partitionByLength.ts +8 -0
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defProps = Object.defineProperties;
|
|
3
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
7
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
+
var __spreadValues = (a, b) => {
|
|
9
|
+
for (var prop in b || (b = {}))
|
|
10
|
+
if (__hasOwnProp.call(b, prop))
|
|
11
|
+
__defNormalProp(a, prop, b[prop]);
|
|
12
|
+
if (__getOwnPropSymbols)
|
|
13
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
14
|
+
if (__propIsEnum.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
}
|
|
17
|
+
return a;
|
|
18
|
+
};
|
|
19
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
1
20
|
var __async = (__this, __arguments, generator) => {
|
|
2
21
|
return new Promise((resolve, reject) => {
|
|
3
22
|
var fulfilled = (value) => {
|
|
@@ -25,14 +44,23 @@ var createPluvHandler = (config) => {
|
|
|
25
44
|
const { authorize, binding, endpoint = "/api/pluv", modify, io } = config;
|
|
26
45
|
const DurableObject = class {
|
|
27
46
|
constructor(state, env) {
|
|
28
|
-
this.
|
|
29
|
-
this._io = io.getRoom(state.id.toString(), { env });
|
|
47
|
+
this._room = io.getRoom(state.id.toString(), { env, state });
|
|
30
48
|
}
|
|
31
49
|
webSocketClose(ws, code, reason, wasClean) {
|
|
50
|
+
if (io._registrationMode !== "detached") return;
|
|
51
|
+
const handler2 = this._room.onClose(ws);
|
|
52
|
+
handler2({ code, reason });
|
|
32
53
|
}
|
|
33
54
|
webSocketError(ws, error) {
|
|
55
|
+
if (io._registrationMode !== "detached") return;
|
|
56
|
+
const handler2 = this._room.onError(ws);
|
|
57
|
+
const eventError = error instanceof Error ? error : new Error("Internal Error");
|
|
58
|
+
handler2({ error: eventError, message: eventError.message });
|
|
34
59
|
}
|
|
35
60
|
webSocketMessage(ws, message) {
|
|
61
|
+
if (io._registrationMode !== "detached") return;
|
|
62
|
+
const handler2 = this._room.onMessage(ws);
|
|
63
|
+
handler2({ data: message });
|
|
36
64
|
}
|
|
37
65
|
fetch(request) {
|
|
38
66
|
return __async(this, null, function* () {
|
|
@@ -42,11 +70,7 @@ var createPluvHandler = (config) => {
|
|
|
42
70
|
}
|
|
43
71
|
const { 0: client, 1: server } = new WebSocketPair();
|
|
44
72
|
const token = new URL(request.url).searchParams.get("token");
|
|
45
|
-
yield this.
|
|
46
|
-
env: this._env,
|
|
47
|
-
request,
|
|
48
|
-
token
|
|
49
|
-
});
|
|
73
|
+
yield this._room.register(server, { token });
|
|
50
74
|
return new Response(null, { status: 101, webSocket: client });
|
|
51
75
|
});
|
|
52
76
|
}
|
|
@@ -133,13 +157,45 @@ import { AbstractPlatform } from "@pluv/io";
|
|
|
133
157
|
// src/CloudflareWebSocket.ts
|
|
134
158
|
import { AbstractWebSocket } from "@pluv/io";
|
|
135
159
|
var CloudflareWebSocket = class extends AbstractWebSocket {
|
|
160
|
+
set presence(presence) {
|
|
161
|
+
const deserialized = this.webSocket.deserializeAttachment();
|
|
162
|
+
const state = deserialized.state;
|
|
163
|
+
this.webSocket.serializeAttachment(__spreadProps(__spreadValues({}, this.webSocket.deserializeAttachment()), {
|
|
164
|
+
state: __spreadProps(__spreadValues({}, state), { presence })
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
136
167
|
get readyState() {
|
|
137
168
|
return this.webSocket.readyState;
|
|
138
169
|
}
|
|
170
|
+
get sessionId() {
|
|
171
|
+
var _a, _b;
|
|
172
|
+
const deserialized = (_a = this.webSocket.deserializeAttachment()) != null ? _a : {};
|
|
173
|
+
const sessionId = (_b = deserialized.sessionId) != null ? _b : crypto.randomUUID();
|
|
174
|
+
if (typeof deserialized.sessionId !== "string") {
|
|
175
|
+
this.webSocket.serializeAttachment(__spreadProps(__spreadValues({}, deserialized), { sessionId }));
|
|
176
|
+
}
|
|
177
|
+
return sessionId;
|
|
178
|
+
}
|
|
179
|
+
get state() {
|
|
180
|
+
var _a;
|
|
181
|
+
const deserialized = this.webSocket.deserializeAttachment();
|
|
182
|
+
const state = (_a = deserialized.state) != null ? _a : null;
|
|
183
|
+
if (!state) throw new Error("Could not get websocket state");
|
|
184
|
+
return state;
|
|
185
|
+
}
|
|
139
186
|
constructor(webSocket, config) {
|
|
140
|
-
const { room
|
|
141
|
-
super(
|
|
142
|
-
|
|
187
|
+
const { room } = config;
|
|
188
|
+
super(webSocket, config);
|
|
189
|
+
const state = {
|
|
190
|
+
presence: null,
|
|
191
|
+
quit: false,
|
|
192
|
+
room,
|
|
193
|
+
timers: { ping: (/* @__PURE__ */ new Date()).getTime() }
|
|
194
|
+
};
|
|
195
|
+
webSocket.serializeAttachment(__spreadValues({
|
|
196
|
+
sessionId: this.sessionId,
|
|
197
|
+
state
|
|
198
|
+
}, webSocket.deserializeAttachment()));
|
|
143
199
|
}
|
|
144
200
|
addEventListener(type, handler) {
|
|
145
201
|
this.webSocket.addEventListener(type, handler);
|
|
@@ -149,10 +205,6 @@ var CloudflareWebSocket = class extends AbstractWebSocket {
|
|
|
149
205
|
if (!canClose) return;
|
|
150
206
|
this.webSocket.close(code, reason);
|
|
151
207
|
}
|
|
152
|
-
initialize() {
|
|
153
|
-
this.webSocket.accept();
|
|
154
|
-
return Promise.resolve(() => void 0);
|
|
155
|
-
}
|
|
156
208
|
send(message) {
|
|
157
209
|
if (this.readyState !== this.OPEN) return;
|
|
158
210
|
this.webSocket.send(message);
|
|
@@ -162,10 +214,169 @@ var CloudflareWebSocket = class extends AbstractWebSocket {
|
|
|
162
214
|
}
|
|
163
215
|
};
|
|
164
216
|
|
|
217
|
+
// src/PersistanceCloudflare.ts
|
|
218
|
+
import { AbstractPersistance } from "@pluv/io";
|
|
219
|
+
|
|
220
|
+
// src/utils/partitionByLength.ts
|
|
221
|
+
var partitionByLength = (arr, length) => {
|
|
222
|
+
if (!arr.length) return [];
|
|
223
|
+
const head = arr.slice(0, length);
|
|
224
|
+
const tail = arr.slice(length);
|
|
225
|
+
return [head, ...partitionByLength(tail, length)];
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// src/PersistanceCloudflare.ts
|
|
229
|
+
var CLOUDFLARE_DELETE_BATCH_LIMIT = 128;
|
|
230
|
+
var STORAGE_PREFIX = "$PLUV_STORAGE;";
|
|
231
|
+
var USER_PREFIX = "$PLUV_USER";
|
|
232
|
+
var PersistanceCloudflare = class extends AbstractPersistance {
|
|
233
|
+
constructor(config) {
|
|
234
|
+
super();
|
|
235
|
+
const { state } = config;
|
|
236
|
+
this._state = state;
|
|
237
|
+
}
|
|
238
|
+
addUser(room, connectionId, user) {
|
|
239
|
+
return __async(this, null, function* () {
|
|
240
|
+
yield this._state.storage.put(this._getUserKey(room, connectionId), user);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
deleteStorageState(room) {
|
|
244
|
+
return __async(this, null, function* () {
|
|
245
|
+
yield this._state.storage.delete(this._getStorageKey(room));
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
deleteUser(room, connectionId) {
|
|
249
|
+
return __async(this, null, function* () {
|
|
250
|
+
yield this._state.storage.delete(this._getUserKey(room, connectionId));
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
deleteUsers(room) {
|
|
254
|
+
return __async(this, null, function* () {
|
|
255
|
+
const map = yield this._state.storage.list({
|
|
256
|
+
allowConcurrency: true,
|
|
257
|
+
noCache: true,
|
|
258
|
+
prefix: USER_PREFIX
|
|
259
|
+
});
|
|
260
|
+
const partitions = partitionByLength(Array.from(map.keys()), CLOUDFLARE_DELETE_BATCH_LIMIT);
|
|
261
|
+
yield this._state.storage.transaction((tx) => __async(this, null, function* () {
|
|
262
|
+
yield partitions.reduce((promise, partition) => __async(this, null, function* () {
|
|
263
|
+
return yield promise.then(() => __async(this, null, function* () {
|
|
264
|
+
yield tx.delete(partition);
|
|
265
|
+
}));
|
|
266
|
+
}), Promise.resolve());
|
|
267
|
+
}));
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
getSize(room) {
|
|
271
|
+
return __async(this, null, function* () {
|
|
272
|
+
const storage = yield this._state.storage.list({ prefix: USER_PREFIX });
|
|
273
|
+
return storage.size;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
getStorageState(room) {
|
|
277
|
+
return __async(this, null, function* () {
|
|
278
|
+
const storage = yield this._state.storage.get(this._getStorageKey(room));
|
|
279
|
+
return storage != null ? storage : null;
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
getUser(room, connectionId) {
|
|
283
|
+
return __async(this, null, function* () {
|
|
284
|
+
const user = yield this._state.storage.get(this._getUserKey(room, connectionId));
|
|
285
|
+
return user != null ? user : null;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
getUsers(room) {
|
|
289
|
+
return __async(this, null, function* () {
|
|
290
|
+
const storage = yield this._state.storage.list({
|
|
291
|
+
prefix: USER_PREFIX
|
|
292
|
+
});
|
|
293
|
+
return Array.from(storage.values());
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
setStorageState(room, state) {
|
|
297
|
+
return __async(this, null, function* () {
|
|
298
|
+
yield this._state.storage.put(this._getStorageKey(room), state, {
|
|
299
|
+
allowConcurrency: true
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
_getStorageKey(room) {
|
|
304
|
+
return `${STORAGE_PREFIX}::${room}`;
|
|
305
|
+
}
|
|
306
|
+
_getUserKey(room, connectionId) {
|
|
307
|
+
return `${USER_PREFIX}::${room}::${connectionId}`;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// src/constants.ts
|
|
312
|
+
var DEFAULT_REGISTRATION_MODE = "detached";
|
|
313
|
+
|
|
165
314
|
// src/CloudflarePlatform.ts
|
|
166
|
-
var CloudflarePlatform = class extends AbstractPlatform {
|
|
315
|
+
var CloudflarePlatform = class _CloudflarePlatform extends AbstractPlatform {
|
|
316
|
+
constructor(config = {}) {
|
|
317
|
+
var _a;
|
|
318
|
+
super(__spreadValues(__spreadValues({}, config), config.context && config.mode === "detached" ? { persistance: new PersistanceCloudflare(config.context) } : {}));
|
|
319
|
+
this._registrationMode = (_a = config.mode) != null ? _a : DEFAULT_REGISTRATION_MODE;
|
|
320
|
+
const detachedState = this._getDetachedState();
|
|
321
|
+
if (!detachedState) return;
|
|
322
|
+
detachedState.setWebSocketAutoResponse(
|
|
323
|
+
new WebSocketRequestResponsePair('{"type":"$PING","data":{}}', JSON.stringify({ type: "$PONG", data: {} }))
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
acceptWebSocket(webSocket) {
|
|
327
|
+
return __async(this, null, function* () {
|
|
328
|
+
const detachedState = this._getDetachedState();
|
|
329
|
+
if (!detachedState) {
|
|
330
|
+
webSocket.webSocket.accept();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
detachedState.acceptWebSocket(webSocket.webSocket);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
167
336
|
convertWebSocket(webSocket, config) {
|
|
168
|
-
|
|
337
|
+
const { room } = config;
|
|
338
|
+
return new CloudflareWebSocket(webSocket, { persistance: this.persistance, room });
|
|
339
|
+
}
|
|
340
|
+
getLastPing(webSocket) {
|
|
341
|
+
var _a;
|
|
342
|
+
const detachedState = this._getDetachedState();
|
|
343
|
+
if (!detachedState) return null;
|
|
344
|
+
const timestamp = detachedState.getWebSocketAutoResponseTimestamp(webSocket.webSocket);
|
|
345
|
+
return (_a = timestamp == null ? void 0 : timestamp.getTime()) != null ? _a : null;
|
|
346
|
+
}
|
|
347
|
+
getSerializedState(webSocket) {
|
|
348
|
+
var _a;
|
|
349
|
+
const deserialized = webSocket.webSocket.deserializeAttachment();
|
|
350
|
+
return (_a = deserialized == null ? void 0 : deserialized.state) != null ? _a : null;
|
|
351
|
+
}
|
|
352
|
+
getSessionId(webSocket) {
|
|
353
|
+
var _a;
|
|
354
|
+
const deserialized = (_a = webSocket.deserializeAttachment()) != null ? _a : {};
|
|
355
|
+
const sessionId = deserialized.sessionId;
|
|
356
|
+
if (typeof sessionId !== "string") {
|
|
357
|
+
throw new Error("This websocket was not registered");
|
|
358
|
+
}
|
|
359
|
+
return sessionId;
|
|
360
|
+
}
|
|
361
|
+
getWebSockets() {
|
|
362
|
+
var _a;
|
|
363
|
+
const detachedState = this._getDetachedState();
|
|
364
|
+
if (!detachedState) return [];
|
|
365
|
+
return (_a = detachedState.getWebSockets()) != null ? _a : [];
|
|
366
|
+
}
|
|
367
|
+
initialize(config) {
|
|
368
|
+
var _a;
|
|
369
|
+
const context = (_a = config.context) != null ? _a : __spreadValues(__spreadValues({}, this._ioContext), this._roomContext);
|
|
370
|
+
if (!context.env || !context.state) {
|
|
371
|
+
throw new Error("Could not derive platform context");
|
|
372
|
+
}
|
|
373
|
+
return new _CloudflarePlatform(__spreadProps(__spreadValues({
|
|
374
|
+
mode: this._registrationMode,
|
|
375
|
+
persistance: this.persistance,
|
|
376
|
+
pubSub: this.pubSub
|
|
377
|
+
}, config), {
|
|
378
|
+
context: { env: context.env, state: context.state }
|
|
379
|
+
}))._initialize();
|
|
169
380
|
}
|
|
170
381
|
parseData(data) {
|
|
171
382
|
if (typeof data === "string") return JSON.parse(data);
|
|
@@ -175,11 +386,21 @@ var CloudflarePlatform = class extends AbstractPlatform {
|
|
|
175
386
|
randomUUID() {
|
|
176
387
|
return crypto.randomUUID();
|
|
177
388
|
}
|
|
389
|
+
setSerializedState(webSocket, state) {
|
|
390
|
+
var _a;
|
|
391
|
+
const deserialized = (_a = webSocket.webSocket.deserializeAttachment()) != null ? _a : {};
|
|
392
|
+
webSocket.webSocket.serializeAttachment(__spreadProps(__spreadValues({}, deserialized), { state }));
|
|
393
|
+
}
|
|
394
|
+
_getDetachedState() {
|
|
395
|
+
var _a, _b;
|
|
396
|
+
if (this._registrationMode !== "detached") return null;
|
|
397
|
+
return (_b = (_a = this._roomContext) == null ? void 0 : _a.state) != null ? _b : null;
|
|
398
|
+
}
|
|
178
399
|
};
|
|
179
400
|
|
|
180
401
|
// src/platformCloudflare.ts
|
|
181
|
-
var platformCloudflare = () => {
|
|
182
|
-
return new CloudflarePlatform();
|
|
402
|
+
var platformCloudflare = (config = {}) => {
|
|
403
|
+
return new CloudflarePlatform(config);
|
|
183
404
|
};
|
|
184
405
|
export {
|
|
185
406
|
createPluvHandler,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pluv/platform-cloudflare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "@pluv/io adapter for cloudflare workers",
|
|
5
5
|
"author": "leedavidcs",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,17 +17,17 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"path-to-regexp": "^7.
|
|
21
|
-
"@pluv/io": "^0.
|
|
22
|
-
"@pluv/types": "^0.
|
|
20
|
+
"path-to-regexp": "^7.1.0",
|
|
21
|
+
"@pluv/io": "^0.21.0",
|
|
22
|
+
"@pluv/types": "^0.21.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@cloudflare/workers-types": "^4.
|
|
25
|
+
"@cloudflare/workers-types": "^4.20240806.0",
|
|
26
26
|
"eslint": "^8.57.0",
|
|
27
|
-
"tsup": "^8.
|
|
28
|
-
"typescript": "^5.4
|
|
29
|
-
"@pluv/tsconfig": "^0.
|
|
30
|
-
"eslint-config-pluv": "^0.
|
|
27
|
+
"tsup": "^8.2.4",
|
|
28
|
+
"typescript": "^5.5.4",
|
|
29
|
+
"@pluv/tsconfig": "^0.21.0",
|
|
30
|
+
"eslint-config-pluv": "^0.21.0"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
@@ -1,14 +1,112 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AbstractPlatformConfig,
|
|
3
|
+
ConvertWebSocketConfig,
|
|
4
|
+
WebSocketRegistrationMode,
|
|
5
|
+
WebSocketSerializedState,
|
|
6
|
+
} from "@pluv/io";
|
|
2
7
|
import { AbstractPlatform } from "@pluv/io";
|
|
3
8
|
import { CloudflareWebSocket } from "./CloudflareWebSocket";
|
|
9
|
+
import { PersistanceCloudflare } from "./PersistanceCloudflare";
|
|
10
|
+
import { DEFAULT_REGISTRATION_MODE } from "./constants";
|
|
11
|
+
|
|
12
|
+
export type CloudflarePlatformConfig<TEnv extends Record<string, any> = {}> = AbstractPlatformConfig<
|
|
13
|
+
{ env: TEnv },
|
|
14
|
+
{ state: DurableObjectState }
|
|
15
|
+
> & { mode?: WebSocketRegistrationMode };
|
|
4
16
|
|
|
5
17
|
export class CloudflarePlatform<TEnv extends Record<string, any> = {}> extends AbstractPlatform<
|
|
6
|
-
|
|
18
|
+
CloudflareWebSocket,
|
|
7
19
|
{ env: TEnv },
|
|
8
|
-
{
|
|
20
|
+
{ state: DurableObjectState }
|
|
9
21
|
> {
|
|
22
|
+
readonly _registrationMode: WebSocketRegistrationMode;
|
|
23
|
+
|
|
24
|
+
constructor(config: CloudflarePlatformConfig<TEnv> = {}) {
|
|
25
|
+
super({
|
|
26
|
+
...config,
|
|
27
|
+
...(config.context && config.mode === "detached"
|
|
28
|
+
? { persistance: new PersistanceCloudflare(config.context) }
|
|
29
|
+
: {}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
this._registrationMode = config.mode ?? DEFAULT_REGISTRATION_MODE;
|
|
33
|
+
|
|
34
|
+
const detachedState = this._getDetachedState();
|
|
35
|
+
|
|
36
|
+
if (!detachedState) return;
|
|
37
|
+
|
|
38
|
+
detachedState.setWebSocketAutoResponse(
|
|
39
|
+
new WebSocketRequestResponsePair('{"type":"$PING","data":{}}', JSON.stringify({ type: "$PONG", data: {} })),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public async acceptWebSocket(webSocket: CloudflareWebSocket): Promise<void> {
|
|
44
|
+
const detachedState = this._getDetachedState();
|
|
45
|
+
|
|
46
|
+
if (!detachedState) {
|
|
47
|
+
webSocket.webSocket.accept();
|
|
48
|
+
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
detachedState.acceptWebSocket(webSocket.webSocket);
|
|
53
|
+
}
|
|
54
|
+
|
|
10
55
|
public convertWebSocket(webSocket: WebSocket, config: ConvertWebSocketConfig): CloudflareWebSocket {
|
|
11
|
-
|
|
56
|
+
const { room } = config;
|
|
57
|
+
|
|
58
|
+
return new CloudflareWebSocket(webSocket, { persistance: this.persistance, room });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public getLastPing(webSocket: CloudflareWebSocket): number | null {
|
|
62
|
+
const detachedState = this._getDetachedState();
|
|
63
|
+
|
|
64
|
+
if (!detachedState) return null;
|
|
65
|
+
|
|
66
|
+
const timestamp = detachedState.getWebSocketAutoResponseTimestamp(webSocket.webSocket);
|
|
67
|
+
|
|
68
|
+
return timestamp?.getTime() ?? null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public getSerializedState(webSocket: CloudflareWebSocket): WebSocketSerializedState | null {
|
|
72
|
+
const deserialized = webSocket.webSocket.deserializeAttachment();
|
|
73
|
+
|
|
74
|
+
return deserialized?.state ?? null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public getSessionId(webSocket: WebSocket): string | null {
|
|
78
|
+
const deserialized = webSocket.deserializeAttachment() ?? {};
|
|
79
|
+
const sessionId = deserialized.sessionId;
|
|
80
|
+
|
|
81
|
+
if (typeof sessionId !== "string") {
|
|
82
|
+
throw new Error("This websocket was not registered");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return sessionId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public getWebSockets(): readonly WebSocket[] {
|
|
89
|
+
const detachedState = this._getDetachedState();
|
|
90
|
+
|
|
91
|
+
if (!detachedState) return [];
|
|
92
|
+
|
|
93
|
+
return detachedState.getWebSockets() ?? [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public initialize(config: AbstractPlatformConfig<{ env: TEnv }, { state: DurableObjectState }>): this {
|
|
97
|
+
const context = config.context ?? { ...this._ioContext, ...this._roomContext };
|
|
98
|
+
|
|
99
|
+
if (!context.env || !context.state) {
|
|
100
|
+
throw new Error("Could not derive platform context");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return new CloudflarePlatform<TEnv>({
|
|
104
|
+
mode: this._registrationMode,
|
|
105
|
+
persistance: this.persistance,
|
|
106
|
+
pubSub: this.pubSub,
|
|
107
|
+
...config,
|
|
108
|
+
context: { env: context.env, state: context.state },
|
|
109
|
+
})._initialize() as this;
|
|
12
110
|
}
|
|
13
111
|
|
|
14
112
|
public parseData(data: string | ArrayBuffer): Record<string, any> {
|
|
@@ -22,4 +120,16 @@ export class CloudflarePlatform<TEnv extends Record<string, any> = {}> extends A
|
|
|
22
120
|
public randomUUID(): string {
|
|
23
121
|
return crypto.randomUUID();
|
|
24
122
|
}
|
|
123
|
+
|
|
124
|
+
public setSerializedState(webSocket: CloudflareWebSocket, state: WebSocketSerializedState): void {
|
|
125
|
+
const deserialized = webSocket.webSocket.deserializeAttachment() ?? {};
|
|
126
|
+
|
|
127
|
+
webSocket.webSocket.serializeAttachment({ ...deserialized, state });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private _getDetachedState(): DurableObjectState | null {
|
|
131
|
+
if (this._registrationMode !== "detached") return null;
|
|
132
|
+
|
|
133
|
+
return this._roomContext?.state ?? null;
|
|
134
|
+
}
|
|
25
135
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { AbstractEventMap, AbstractListener,
|
|
1
|
+
import type { AbstractEventMap, AbstractListener, AbstractWebSocketConfig, WebSocketSerializedState } from "@pluv/io";
|
|
2
|
+
import { AbstractWebSocket } from "@pluv/io";
|
|
3
|
+
import type { JsonObject } from "@pluv/types";
|
|
2
4
|
|
|
3
5
|
export interface CloudflareWebSocketEventMap {
|
|
4
6
|
close: CloseEvent;
|
|
@@ -9,19 +11,58 @@ export interface CloudflareWebSocketEventMap {
|
|
|
9
11
|
|
|
10
12
|
export type CloudflareWebSocketConfig = AbstractWebSocketConfig;
|
|
11
13
|
|
|
12
|
-
export class CloudflareWebSocket extends AbstractWebSocket {
|
|
13
|
-
public
|
|
14
|
+
export class CloudflareWebSocket extends AbstractWebSocket<WebSocket> {
|
|
15
|
+
public set presence(presence: JsonObject | null) {
|
|
16
|
+
const deserialized = this.webSocket.deserializeAttachment();
|
|
17
|
+
const state = deserialized.state;
|
|
18
|
+
|
|
19
|
+
this.webSocket.serializeAttachment({
|
|
20
|
+
...this.webSocket.deserializeAttachment(),
|
|
21
|
+
state: { ...state, presence },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
14
24
|
|
|
15
25
|
public get readyState(): 0 | 1 | 2 | 3 {
|
|
16
26
|
return this.webSocket.readyState as 0 | 1 | 2 | 3;
|
|
17
27
|
}
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
const
|
|
29
|
+
public get sessionId(): string {
|
|
30
|
+
const deserialized = this.webSocket.deserializeAttachment() ?? {};
|
|
31
|
+
const sessionId = deserialized.sessionId ?? crypto.randomUUID();
|
|
32
|
+
|
|
33
|
+
if (typeof deserialized.sessionId !== "string") {
|
|
34
|
+
this.webSocket.serializeAttachment({ ...deserialized, sessionId });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return sessionId;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public get state(): WebSocketSerializedState {
|
|
41
|
+
const deserialized = this.webSocket.deserializeAttachment();
|
|
42
|
+
const state = deserialized.state ?? null;
|
|
21
43
|
|
|
22
|
-
|
|
44
|
+
if (!state) throw new Error("Could not get websocket state");
|
|
23
45
|
|
|
24
|
-
|
|
46
|
+
return state;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
constructor(webSocket: WebSocket, config: CloudflareWebSocketConfig) {
|
|
50
|
+
const { room } = config;
|
|
51
|
+
|
|
52
|
+
super(webSocket, config);
|
|
53
|
+
|
|
54
|
+
const state: WebSocketSerializedState = {
|
|
55
|
+
presence: null,
|
|
56
|
+
quit: false,
|
|
57
|
+
room,
|
|
58
|
+
timers: { ping: new Date().getTime() },
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
webSocket.serializeAttachment({
|
|
62
|
+
sessionId: this.sessionId,
|
|
63
|
+
state,
|
|
64
|
+
...webSocket.deserializeAttachment(),
|
|
65
|
+
});
|
|
25
66
|
}
|
|
26
67
|
|
|
27
68
|
public addEventListener<TType extends keyof AbstractEventMap>(type: TType, handler: AbstractListener<TType>) {
|
|
@@ -36,12 +77,6 @@ export class CloudflareWebSocket extends AbstractWebSocket {
|
|
|
36
77
|
this.webSocket.close(code, reason);
|
|
37
78
|
}
|
|
38
79
|
|
|
39
|
-
public initialize(): Promise<() => undefined> {
|
|
40
|
-
this.webSocket.accept();
|
|
41
|
-
|
|
42
|
-
return Promise.resolve(() => undefined);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
80
|
public send(message: string | ArrayBuffer | ArrayBufferView): void {
|
|
46
81
|
if (this.readyState !== this.OPEN) return;
|
|
47
82
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { AbstractPersistance } from "@pluv/io";
|
|
2
|
+
import type { JsonObject } from "@pluv/types";
|
|
3
|
+
import { partitionByLength } from "./utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* !HACK
|
|
7
|
+
* @description The max amount of keys a Cloudflare transactional storage may delete at once
|
|
8
|
+
* @see https://developers.cloudflare.com/durable-objects/api/transactional-storage-api/#delete
|
|
9
|
+
* @date July 8, 2024
|
|
10
|
+
*/
|
|
11
|
+
const CLOUDFLARE_DELETE_BATCH_LIMIT = 128;
|
|
12
|
+
const STORAGE_PREFIX = "$PLUV_STORAGE;";
|
|
13
|
+
const USER_PREFIX = "$PLUV_USER";
|
|
14
|
+
|
|
15
|
+
export interface PersistanceCloudflareConfig<TEnv extends Record<string, any> = {}> {
|
|
16
|
+
env: TEnv;
|
|
17
|
+
state: DurableObjectState;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class PersistanceCloudflare<TEnv extends Record<string, any> = {}> extends AbstractPersistance {
|
|
21
|
+
private _state: DurableObjectState;
|
|
22
|
+
|
|
23
|
+
constructor(config: PersistanceCloudflareConfig<TEnv>) {
|
|
24
|
+
super();
|
|
25
|
+
|
|
26
|
+
const { state } = config;
|
|
27
|
+
|
|
28
|
+
this._state = state;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async addUser(room: string, connectionId: string, user: JsonObject | null): Promise<void> {
|
|
32
|
+
await this._state.storage.put(this._getUserKey(room, connectionId), user);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public async deleteStorageState(room: string): Promise<void> {
|
|
36
|
+
await this._state.storage.delete(this._getStorageKey(room));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async deleteUser(room: string, connectionId: string): Promise<void> {
|
|
40
|
+
await this._state.storage.delete(this._getUserKey(room, connectionId));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public async deleteUsers(room: string): Promise<void> {
|
|
44
|
+
const map = await this._state.storage.list({
|
|
45
|
+
allowConcurrency: true,
|
|
46
|
+
noCache: true,
|
|
47
|
+
prefix: USER_PREFIX,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const partitions = partitionByLength(Array.from(map.keys()), CLOUDFLARE_DELETE_BATCH_LIMIT);
|
|
51
|
+
|
|
52
|
+
await this._state.storage.transaction(async (tx) => {
|
|
53
|
+
await partitions.reduce(async (promise, partition) => {
|
|
54
|
+
return await promise.then(async () => {
|
|
55
|
+
await tx.delete(partition as string[]);
|
|
56
|
+
});
|
|
57
|
+
}, Promise.resolve());
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public async getSize(room: string): Promise<number> {
|
|
62
|
+
const storage = await this._state.storage.list({ prefix: USER_PREFIX });
|
|
63
|
+
|
|
64
|
+
return storage.size;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public async getStorageState(room: string): Promise<string | null> {
|
|
68
|
+
const storage = await this._state.storage.get<string>(this._getStorageKey(room));
|
|
69
|
+
|
|
70
|
+
return storage ?? null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public async getUser(room: string, connectionId: string): Promise<JsonObject | null> {
|
|
74
|
+
const user = await this._state.storage.get<JsonObject | null>(this._getUserKey(room, connectionId));
|
|
75
|
+
|
|
76
|
+
return user ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public async getUsers(room: string): Promise<readonly (JsonObject | null)[]> {
|
|
80
|
+
const storage = await this._state.storage.list({
|
|
81
|
+
prefix: USER_PREFIX,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return Array.from(storage.values()) as (JsonObject | null)[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public async setStorageState(room: string, state: string): Promise<void> {
|
|
88
|
+
await this._state.storage.put(this._getStorageKey(room), state, {
|
|
89
|
+
allowConcurrency: true,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private _getStorageKey(room: string): string {
|
|
94
|
+
return `${STORAGE_PREFIX}::${room}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private _getUserKey(room: string, connectionId: string): string {
|
|
98
|
+
return `${USER_PREFIX}::${room}::${connectionId}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/constants.ts
ADDED