@pluv/platform-cloudflare 0.20.0 → 0.21.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/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._env = env;
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._io.register(server, {
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, sessionId, userId } = config;
141
- super({ room, sessionId, userId });
142
- this.webSocket = webSocket;
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 = "attached";
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
- return new CloudflareWebSocket(webSocket, config);
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.20.0",
3
+ "version": "0.21.1",
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.0.0",
21
- "@pluv/io": "^0.20.0",
22
- "@pluv/types": "^0.20.0"
20
+ "path-to-regexp": "^7.1.0",
21
+ "@pluv/io": "^0.21.1",
22
+ "@pluv/types": "^0.21.1"
23
23
  },
24
24
  "devDependencies": {
25
- "@cloudflare/workers-types": "^4.20240620.0",
25
+ "@cloudflare/workers-types": "^4.20240806.0",
26
26
  "eslint": "^8.57.0",
27
- "tsup": "^8.1.0",
28
- "typescript": "^5.4.5",
29
- "@pluv/tsconfig": "^0.20.0",
30
- "eslint-config-pluv": "^0.20.0"
27
+ "tsup": "^8.2.4",
28
+ "typescript": "^5.5.4",
29
+ "@pluv/tsconfig": "^0.21.1",
30
+ "eslint-config-pluv": "^0.21.1"
31
31
  },
32
32
  "scripts": {
33
33
  "build": "tsup src/index.ts --format esm,cjs --dts",
@@ -1,14 +1,112 @@
1
- import type { ConvertWebSocketConfig } from "@pluv/io";
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
- WebSocket,
18
+ CloudflareWebSocket,
7
19
  { env: TEnv },
8
- { request: Request }
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
- return new CloudflareWebSocket(webSocket, config);
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, AbstractWebSocket, AbstractWebSocketConfig } from "@pluv/io";
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 webSocket: WebSocket;
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
- constructor(webSocket: WebSocket, config: CloudflareWebSocketConfig) {
20
- const { room, sessionId, userId } = config;
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
- super({ room, sessionId, userId });
44
+ if (!state) throw new Error("Could not get websocket state");
23
45
 
24
- this.webSocket = webSocket;
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
+ }
@@ -0,0 +1,3 @@
1
+ import type { WebSocketRegistrationMode } from "@pluv/io";
2
+
3
+ export const DEFAULT_REGISTRATION_MODE: WebSocketRegistrationMode = "attached";