@pluv/persistence-cloudflare-transactional-storage 0.41.6 → 0.42.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 +8 -8
- package/CHANGELOG.md +31 -0
- package/dist/index.d.mts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +229 -35
- package/dist/index.mjs +229 -35
- package/package.json +7 -7
- package/src/PersistenceCloudflareTransactionalStorage.ts +267 -40
- package/src/utils/castNumber.ts +3 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/sql.ts +10 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @pluv/persistence-cloudflare-transactional-storage@0.
|
|
2
|
+
> @pluv/persistence-cloudflare-transactional-storage@0.42.0 build /home/runner/work/pluv/pluv/packages/persistence-cloudflare-transactional-storage
|
|
3
3
|
> tsup src/index.ts --format esm,cjs --dts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
[34mCLI[39m Target: es6
|
|
9
9
|
[34mESM[39m Build start
|
|
10
10
|
[34mCJS[39m Build start
|
|
11
|
-
[
|
|
12
|
-
[
|
|
13
|
-
[
|
|
14
|
-
[
|
|
11
|
+
[32mCJS[39m [1mdist/index.js [22m[32m10.29 KB[39m
|
|
12
|
+
[32mCJS[39m ⚡️ Build success in 81ms
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m9.23 KB[39m
|
|
14
|
+
[32mESM[39m ⚡️ Build success in 100ms
|
|
15
15
|
[34mDTS[39m Build start
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
17
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[32m1.
|
|
18
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m1.
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 2969ms
|
|
17
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m1.21 KB[39m
|
|
18
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m1.21 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @pluv/persistence-cloudflare-transactional-storage
|
|
2
2
|
|
|
3
|
+
## 0.42.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 5c2c71d: **BREAKING** Updated the `PersistenceCloudflareTransactionalStorage` constructor so require a new property `mode` (either `kv` or `sqlite`). `sqlite` makes use of `DurableObject` SQLite storage, and `kv` makes use of their key-value storage. `platformCloudflare` now sets the underlying `PersistenceCloudflareTransactionalStorage` to use the `sqlite` mode by default (was previously using `kv`). For more information on the differences, please refer to [Cloudflare's documentation](https://developers.cloudflare.com/durable-objects/api/storage-api/#sql-api).
|
|
8
|
+
|
|
9
|
+
To continue using the `kv` mode, you will need to install `@pluv/persistence-cloudflare-transactional-storage` and provide it manually to `platformCloudflare` with your desired mode.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
platformCloudflare({
|
|
13
|
+
// ...
|
|
14
|
+
persistence: new PersistenceCloudflareTransactionalStorage({ mode: "kv" }),
|
|
15
|
+
// ...
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- 4ce32ee: **BREAKING** `Persistence.getUsers` now returns a map of connection id strings to user JSON objects instead of an array of user JSON objects. This change is intended to be internal only and generally non-breaking.
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- Updated dependencies [cf1529a]
|
|
24
|
+
- @pluv/io@0.42.0
|
|
25
|
+
- @pluv/types@0.42.0
|
|
26
|
+
|
|
27
|
+
## 0.41.7
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- @pluv/io@0.41.7
|
|
32
|
+
- @pluv/types@0.41.7
|
|
33
|
+
|
|
3
34
|
## 0.41.6
|
|
4
35
|
|
|
5
36
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -2,10 +2,12 @@ import { AbstractPersistence } from '@pluv/io';
|
|
|
2
2
|
import { JsonObject } from '@pluv/types';
|
|
3
3
|
|
|
4
4
|
interface PersistenceCloudflareTransactionalStorageConfig {
|
|
5
|
-
|
|
5
|
+
mode: "kv" | "sqlite";
|
|
6
6
|
}
|
|
7
7
|
declare class PersistenceCloudflareTransactionalStorage extends AbstractPersistence {
|
|
8
|
+
private _mode;
|
|
8
9
|
private _state;
|
|
10
|
+
private _initialized;
|
|
9
11
|
constructor(config: PersistenceCloudflareTransactionalStorageConfig);
|
|
10
12
|
addUser(room: string, connectionId: string, user: JsonObject | null): Promise<void>;
|
|
11
13
|
deleteStorageState(room: string): Promise<void>;
|
|
@@ -13,8 +15,11 @@ declare class PersistenceCloudflareTransactionalStorage extends AbstractPersiste
|
|
|
13
15
|
deleteUsers(room: string): Promise<void>;
|
|
14
16
|
getStorageState(room: string): Promise<string | null>;
|
|
15
17
|
getUser(room: string, connectionId: string): Promise<JsonObject | null>;
|
|
16
|
-
getUsers(room: string): Promise<
|
|
18
|
+
getUsers(room: string): Promise<Map<[connectionId: string][0], JsonObject | null>>;
|
|
17
19
|
getUsersSize(room: string): Promise<number>;
|
|
20
|
+
initialize(roomContext: {
|
|
21
|
+
state: DurableObjectState;
|
|
22
|
+
}): this;
|
|
18
23
|
setStorageState(room: string, state: string): Promise<void>;
|
|
19
24
|
private _getStorageKey;
|
|
20
25
|
private _getUserKey;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { AbstractPersistence } from '@pluv/io';
|
|
|
2
2
|
import { JsonObject } from '@pluv/types';
|
|
3
3
|
|
|
4
4
|
interface PersistenceCloudflareTransactionalStorageConfig {
|
|
5
|
-
|
|
5
|
+
mode: "kv" | "sqlite";
|
|
6
6
|
}
|
|
7
7
|
declare class PersistenceCloudflareTransactionalStorage extends AbstractPersistence {
|
|
8
|
+
private _mode;
|
|
8
9
|
private _state;
|
|
10
|
+
private _initialized;
|
|
9
11
|
constructor(config: PersistenceCloudflareTransactionalStorageConfig);
|
|
10
12
|
addUser(room: string, connectionId: string, user: JsonObject | null): Promise<void>;
|
|
11
13
|
deleteStorageState(room: string): Promise<void>;
|
|
@@ -13,8 +15,11 @@ declare class PersistenceCloudflareTransactionalStorage extends AbstractPersiste
|
|
|
13
15
|
deleteUsers(room: string): Promise<void>;
|
|
14
16
|
getStorageState(room: string): Promise<string | null>;
|
|
15
17
|
getUser(room: string, connectionId: string): Promise<JsonObject | null>;
|
|
16
|
-
getUsers(room: string): Promise<
|
|
18
|
+
getUsers(room: string): Promise<Map<[connectionId: string][0], JsonObject | null>>;
|
|
17
19
|
getUsersSize(room: string): Promise<number>;
|
|
20
|
+
initialize(roomContext: {
|
|
21
|
+
state: DurableObjectState;
|
|
22
|
+
}): this;
|
|
18
23
|
setStorageState(room: string, state: string): Promise<void>;
|
|
19
24
|
private _getStorageKey;
|
|
20
25
|
private _getUserKey;
|
package/dist/index.js
CHANGED
|
@@ -47,6 +47,11 @@ module.exports = __toCommonJS(index_exports);
|
|
|
47
47
|
// src/PersistenceCloudflareTransactionalStorage.ts
|
|
48
48
|
var import_io = require("@pluv/io");
|
|
49
49
|
|
|
50
|
+
// src/utils/castNumber.ts
|
|
51
|
+
var castNumber = (value) => {
|
|
52
|
+
return typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value, 10) : Number(value);
|
|
53
|
+
};
|
|
54
|
+
|
|
50
55
|
// src/utils/partitionByLength.ts
|
|
51
56
|
var partitionByLength = (arr, length) => {
|
|
52
57
|
if (!arr.length) return [];
|
|
@@ -55,86 +60,275 @@ var partitionByLength = (arr, length) => {
|
|
|
55
60
|
return [head, ...partitionByLength(tail, length)];
|
|
56
61
|
};
|
|
57
62
|
|
|
63
|
+
// src/utils/sql.ts
|
|
64
|
+
var sql = (strings, ...values) => {
|
|
65
|
+
return strings.reduce((result, str, i) => {
|
|
66
|
+
const cleaned = str.replace(/(\n[ \t]*)+/g, " ");
|
|
67
|
+
return result + cleaned + (i < values.length ? values[i] : "");
|
|
68
|
+
}, "").trim();
|
|
69
|
+
};
|
|
70
|
+
|
|
58
71
|
// src/PersistenceCloudflareTransactionalStorage.ts
|
|
59
|
-
var
|
|
60
|
-
var
|
|
61
|
-
var
|
|
72
|
+
var KV_CLOUDFLARE_DELETE_BATCH_LIMIT = 128;
|
|
73
|
+
var KV_STORAGE_PREFIX = "$PLUV_STORAGE;";
|
|
74
|
+
var KV_USER_PREFIX = "$PLUV_USER";
|
|
75
|
+
var SQLITE_STORAGE_TABLE = "__pluv_storage";
|
|
76
|
+
var SQLITE_USER_TABLE = "__pluv_user";
|
|
62
77
|
var PersistenceCloudflareTransactionalStorage = class extends import_io.AbstractPersistence {
|
|
63
78
|
constructor(config) {
|
|
64
79
|
super();
|
|
65
|
-
|
|
66
|
-
this.
|
|
80
|
+
this._state = null;
|
|
81
|
+
this._initialized = null;
|
|
82
|
+
const { mode } = config;
|
|
83
|
+
this._mode = mode;
|
|
67
84
|
}
|
|
68
85
|
addUser(room, connectionId, user) {
|
|
69
86
|
return __async(this, null, function* () {
|
|
70
|
-
|
|
87
|
+
if (!this._initialized) return;
|
|
88
|
+
if (!this._state) return;
|
|
89
|
+
yield this._initialized;
|
|
90
|
+
if (this._mode === "kv") {
|
|
91
|
+
yield this._state.storage.put(this._getUserKey(room, connectionId), user);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this._state.storage.sql.exec(
|
|
95
|
+
sql`
|
|
96
|
+
INSERT INTO ${SQLITE_USER_TABLE} VALUES (?, ?, ?);
|
|
97
|
+
`,
|
|
98
|
+
connectionId,
|
|
99
|
+
JSON.stringify(user),
|
|
100
|
+
room
|
|
101
|
+
);
|
|
71
102
|
});
|
|
72
103
|
}
|
|
73
104
|
deleteStorageState(room) {
|
|
74
105
|
return __async(this, null, function* () {
|
|
75
|
-
|
|
106
|
+
if (!this._initialized) return;
|
|
107
|
+
if (!this._state) return;
|
|
108
|
+
yield this._initialized;
|
|
109
|
+
if (this._mode === "kv") {
|
|
110
|
+
yield this._state.storage.delete(this._getStorageKey(room));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this._state.storage.sql.exec(
|
|
114
|
+
sql`
|
|
115
|
+
DELETE FROM ${SQLITE_STORAGE_TABLE}
|
|
116
|
+
WHERE room = ?;
|
|
117
|
+
`,
|
|
118
|
+
room
|
|
119
|
+
);
|
|
76
120
|
});
|
|
77
121
|
}
|
|
78
122
|
deleteUser(room, connectionId) {
|
|
79
123
|
return __async(this, null, function* () {
|
|
80
|
-
|
|
124
|
+
if (!this._initialized) return;
|
|
125
|
+
if (!this._state) return;
|
|
126
|
+
yield this._initialized;
|
|
127
|
+
if (this._mode === "kv") {
|
|
128
|
+
yield this._state.storage.delete(this._getUserKey(room, connectionId));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this._state.storage.sql.exec(
|
|
132
|
+
sql`
|
|
133
|
+
DELETE FROM ${SQLITE_USER_TABLE}
|
|
134
|
+
WHERE room = ?
|
|
135
|
+
AND id = ?;
|
|
136
|
+
`,
|
|
137
|
+
room,
|
|
138
|
+
connectionId
|
|
139
|
+
);
|
|
81
140
|
});
|
|
82
141
|
}
|
|
83
142
|
deleteUsers(room) {
|
|
84
143
|
return __async(this, null, function* () {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
144
|
+
if (!this._initialized) return;
|
|
145
|
+
if (!this._state) return;
|
|
146
|
+
yield this._initialized;
|
|
147
|
+
if (this._mode === "kv") {
|
|
148
|
+
const map = yield this._state.storage.list({
|
|
149
|
+
allowConcurrency: true,
|
|
150
|
+
noCache: true,
|
|
151
|
+
prefix: `${KV_USER_PREFIX}::${room}`
|
|
152
|
+
});
|
|
153
|
+
const partitions = partitionByLength(Array.from(map.keys()), KV_CLOUDFLARE_DELETE_BATCH_LIMIT);
|
|
154
|
+
yield this._state.storage.transaction((tx) => __async(this, null, function* () {
|
|
155
|
+
yield partitions.reduce((promise, partition) => __async(this, null, function* () {
|
|
156
|
+
yield promise;
|
|
94
157
|
yield tx.delete(partition);
|
|
95
|
-
}));
|
|
96
|
-
})
|
|
97
|
-
|
|
158
|
+
}), Promise.resolve());
|
|
159
|
+
}));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this._state.storage.sql.exec(
|
|
163
|
+
sql`
|
|
164
|
+
DELETE FROM ${SQLITE_USER_TABLE}
|
|
165
|
+
WHERE room = ?;
|
|
166
|
+
`,
|
|
167
|
+
room
|
|
168
|
+
);
|
|
98
169
|
});
|
|
99
170
|
}
|
|
100
171
|
getStorageState(room) {
|
|
101
172
|
return __async(this, null, function* () {
|
|
102
|
-
|
|
103
|
-
|
|
173
|
+
if (!this._initialized) return null;
|
|
174
|
+
if (!this._state) return null;
|
|
175
|
+
yield this._initialized;
|
|
176
|
+
if (this._mode === "kv") {
|
|
177
|
+
const storage = yield this._state.storage.get(this._getStorageKey(room));
|
|
178
|
+
return storage != null ? storage : null;
|
|
179
|
+
}
|
|
180
|
+
const cursor = this._state.storage.sql.exec(
|
|
181
|
+
sql`
|
|
182
|
+
SELECT room, data
|
|
183
|
+
FROM ${SQLITE_STORAGE_TABLE}
|
|
184
|
+
WHERE room = ?;
|
|
185
|
+
`,
|
|
186
|
+
room
|
|
187
|
+
);
|
|
188
|
+
try {
|
|
189
|
+
const result = cursor.one();
|
|
190
|
+
return result.data;
|
|
191
|
+
} catch (e) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
104
194
|
});
|
|
105
195
|
}
|
|
106
196
|
getUser(room, connectionId) {
|
|
107
197
|
return __async(this, null, function* () {
|
|
108
|
-
|
|
109
|
-
|
|
198
|
+
if (!this._initialized) return null;
|
|
199
|
+
if (!this._state) return null;
|
|
200
|
+
yield this._initialized;
|
|
201
|
+
if (this._mode === "kv") {
|
|
202
|
+
const user = yield this._state.storage.get(this._getUserKey(room, connectionId));
|
|
203
|
+
return user != null ? user : null;
|
|
204
|
+
}
|
|
205
|
+
const cursor = this._state.storage.sql.exec(
|
|
206
|
+
sql`
|
|
207
|
+
SELECT id, data, room
|
|
208
|
+
FROM ${SQLITE_USER_TABLE}
|
|
209
|
+
WHERE room = ?
|
|
210
|
+
AND id = ?;
|
|
211
|
+
`,
|
|
212
|
+
room,
|
|
213
|
+
connectionId
|
|
214
|
+
);
|
|
215
|
+
try {
|
|
216
|
+
const result = cursor.one();
|
|
217
|
+
const user = JSON.parse(result.data);
|
|
218
|
+
return user;
|
|
219
|
+
} catch (e) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
110
222
|
});
|
|
111
223
|
}
|
|
112
224
|
getUsers(room) {
|
|
113
225
|
return __async(this, null, function* () {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
226
|
+
if (!this._initialized) return /* @__PURE__ */ new Map();
|
|
227
|
+
if (!this._state) return /* @__PURE__ */ new Map();
|
|
228
|
+
yield this._initialized;
|
|
229
|
+
if (this._mode === "kv") {
|
|
230
|
+
const entries = yield this._state.storage.list({
|
|
231
|
+
prefix: `${KV_USER_PREFIX}::${room}`
|
|
232
|
+
}).then((map) => Array.from(map.entries()));
|
|
233
|
+
return entries.reduce((map, [key, user]) => {
|
|
234
|
+
const connectionId = key.split("::").slice(-1)[0];
|
|
235
|
+
return !!connectionId ? map.set(connectionId, user) : map;
|
|
236
|
+
}, /* @__PURE__ */ new Map());
|
|
237
|
+
}
|
|
238
|
+
const cursor = this._state.storage.sql.exec(
|
|
239
|
+
sql`
|
|
240
|
+
SELECT id, room, data
|
|
241
|
+
FROM ${SQLITE_USER_TABLE}
|
|
242
|
+
WHERE room = ?;
|
|
243
|
+
`,
|
|
244
|
+
room
|
|
245
|
+
);
|
|
246
|
+
return cursor.toArray().reduce((map, { id, data }) => {
|
|
247
|
+
try {
|
|
248
|
+
const user = JSON.parse(data);
|
|
249
|
+
return map.set(id, user);
|
|
250
|
+
} catch (e) {
|
|
251
|
+
return map;
|
|
252
|
+
}
|
|
253
|
+
}, /* @__PURE__ */ new Map());
|
|
118
254
|
});
|
|
119
255
|
}
|
|
120
256
|
getUsersSize(room) {
|
|
121
257
|
return __async(this, null, function* () {
|
|
122
|
-
|
|
123
|
-
return
|
|
258
|
+
if (!this._initialized) return 0;
|
|
259
|
+
if (!this._state) return 0;
|
|
260
|
+
yield this._initialized;
|
|
261
|
+
if (this._mode) {
|
|
262
|
+
const storage = yield this._state.storage.list({ prefix: `${KV_USER_PREFIX}::${room}` });
|
|
263
|
+
return storage.size;
|
|
264
|
+
}
|
|
265
|
+
const cursor = this._state.storage.sql.exec(
|
|
266
|
+
sql`
|
|
267
|
+
SELECT COUNT(*) count
|
|
268
|
+
FROM ${SQLITE_USER_TABLE}
|
|
269
|
+
WHERE room = ?;
|
|
270
|
+
`,
|
|
271
|
+
room
|
|
272
|
+
);
|
|
273
|
+
try {
|
|
274
|
+
const result = cursor.one();
|
|
275
|
+
return castNumber(result.count);
|
|
276
|
+
} catch (e) {
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
124
279
|
});
|
|
125
280
|
}
|
|
281
|
+
initialize(roomContext) {
|
|
282
|
+
this._initialized = (() => __async(this, null, function* () {
|
|
283
|
+
const { state } = roomContext;
|
|
284
|
+
this._state = state;
|
|
285
|
+
if (this._mode === "sqlite") {
|
|
286
|
+
this._state.storage.sql.exec(sql`
|
|
287
|
+
CREATE TABLE IF NOT EXISTS ${SQLITE_STORAGE_TABLE}(
|
|
288
|
+
room TEXT PRIMARY KEY,
|
|
289
|
+
data TEXT NOT NULL
|
|
290
|
+
);
|
|
291
|
+
CREATE TABLE IF NOT EXISTS ${SQLITE_USER_TABLE}(
|
|
292
|
+
id TEXT PRIMARY KEY,
|
|
293
|
+
data TEXT NOT NULL,
|
|
294
|
+
room TEXT NOT NULL
|
|
295
|
+
);
|
|
296
|
+
CREATE INDEX IF NOT EXISTS ${SQLITE_USER_TABLE}_room ON ${SQLITE_USER_TABLE}(room);
|
|
297
|
+
`);
|
|
298
|
+
}
|
|
299
|
+
return true;
|
|
300
|
+
}))();
|
|
301
|
+
return this;
|
|
302
|
+
}
|
|
126
303
|
setStorageState(room, state) {
|
|
127
304
|
return __async(this, null, function* () {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
305
|
+
if (!this._initialized) return;
|
|
306
|
+
if (!this._state) return;
|
|
307
|
+
yield this._initialized;
|
|
308
|
+
if (this._mode === "kv") {
|
|
309
|
+
yield this._state.storage.put(this._getStorageKey(room), state, { allowConcurrency: true });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
this._state.storage.sql.exec(
|
|
313
|
+
sql`
|
|
314
|
+
INSERT INTO ${SQLITE_STORAGE_TABLE} VALUES (?, ?);
|
|
315
|
+
`,
|
|
316
|
+
room,
|
|
317
|
+
state
|
|
318
|
+
);
|
|
131
319
|
});
|
|
132
320
|
}
|
|
133
321
|
_getStorageKey(room) {
|
|
134
|
-
|
|
322
|
+
if (this._mode === "sqlite") {
|
|
323
|
+
throw new Error("Attempted to get kv key for sqlite storage");
|
|
324
|
+
}
|
|
325
|
+
return `${KV_STORAGE_PREFIX}::${room}`;
|
|
135
326
|
}
|
|
136
327
|
_getUserKey(room, connectionId) {
|
|
137
|
-
|
|
328
|
+
if (this._mode === "sqlite") {
|
|
329
|
+
throw new Error("Attempted to get kv key for sqlite storage");
|
|
330
|
+
}
|
|
331
|
+
return `${KV_USER_PREFIX}::${room}::${connectionId}`;
|
|
138
332
|
}
|
|
139
333
|
};
|
|
140
334
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/index.mjs
CHANGED
|
@@ -22,6 +22,11 @@ var __async = (__this, __arguments, generator) => {
|
|
|
22
22
|
// src/PersistenceCloudflareTransactionalStorage.ts
|
|
23
23
|
import { AbstractPersistence } from "@pluv/io";
|
|
24
24
|
|
|
25
|
+
// src/utils/castNumber.ts
|
|
26
|
+
var castNumber = (value) => {
|
|
27
|
+
return typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value, 10) : Number(value);
|
|
28
|
+
};
|
|
29
|
+
|
|
25
30
|
// src/utils/partitionByLength.ts
|
|
26
31
|
var partitionByLength = (arr, length) => {
|
|
27
32
|
if (!arr.length) return [];
|
|
@@ -30,86 +35,275 @@ var partitionByLength = (arr, length) => {
|
|
|
30
35
|
return [head, ...partitionByLength(tail, length)];
|
|
31
36
|
};
|
|
32
37
|
|
|
38
|
+
// src/utils/sql.ts
|
|
39
|
+
var sql = (strings, ...values) => {
|
|
40
|
+
return strings.reduce((result, str, i) => {
|
|
41
|
+
const cleaned = str.replace(/(\n[ \t]*)+/g, " ");
|
|
42
|
+
return result + cleaned + (i < values.length ? values[i] : "");
|
|
43
|
+
}, "").trim();
|
|
44
|
+
};
|
|
45
|
+
|
|
33
46
|
// src/PersistenceCloudflareTransactionalStorage.ts
|
|
34
|
-
var
|
|
35
|
-
var
|
|
36
|
-
var
|
|
47
|
+
var KV_CLOUDFLARE_DELETE_BATCH_LIMIT = 128;
|
|
48
|
+
var KV_STORAGE_PREFIX = "$PLUV_STORAGE;";
|
|
49
|
+
var KV_USER_PREFIX = "$PLUV_USER";
|
|
50
|
+
var SQLITE_STORAGE_TABLE = "__pluv_storage";
|
|
51
|
+
var SQLITE_USER_TABLE = "__pluv_user";
|
|
37
52
|
var PersistenceCloudflareTransactionalStorage = class extends AbstractPersistence {
|
|
38
53
|
constructor(config) {
|
|
39
54
|
super();
|
|
40
|
-
|
|
41
|
-
this.
|
|
55
|
+
this._state = null;
|
|
56
|
+
this._initialized = null;
|
|
57
|
+
const { mode } = config;
|
|
58
|
+
this._mode = mode;
|
|
42
59
|
}
|
|
43
60
|
addUser(room, connectionId, user) {
|
|
44
61
|
return __async(this, null, function* () {
|
|
45
|
-
|
|
62
|
+
if (!this._initialized) return;
|
|
63
|
+
if (!this._state) return;
|
|
64
|
+
yield this._initialized;
|
|
65
|
+
if (this._mode === "kv") {
|
|
66
|
+
yield this._state.storage.put(this._getUserKey(room, connectionId), user);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
this._state.storage.sql.exec(
|
|
70
|
+
sql`
|
|
71
|
+
INSERT INTO ${SQLITE_USER_TABLE} VALUES (?, ?, ?);
|
|
72
|
+
`,
|
|
73
|
+
connectionId,
|
|
74
|
+
JSON.stringify(user),
|
|
75
|
+
room
|
|
76
|
+
);
|
|
46
77
|
});
|
|
47
78
|
}
|
|
48
79
|
deleteStorageState(room) {
|
|
49
80
|
return __async(this, null, function* () {
|
|
50
|
-
|
|
81
|
+
if (!this._initialized) return;
|
|
82
|
+
if (!this._state) return;
|
|
83
|
+
yield this._initialized;
|
|
84
|
+
if (this._mode === "kv") {
|
|
85
|
+
yield this._state.storage.delete(this._getStorageKey(room));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this._state.storage.sql.exec(
|
|
89
|
+
sql`
|
|
90
|
+
DELETE FROM ${SQLITE_STORAGE_TABLE}
|
|
91
|
+
WHERE room = ?;
|
|
92
|
+
`,
|
|
93
|
+
room
|
|
94
|
+
);
|
|
51
95
|
});
|
|
52
96
|
}
|
|
53
97
|
deleteUser(room, connectionId) {
|
|
54
98
|
return __async(this, null, function* () {
|
|
55
|
-
|
|
99
|
+
if (!this._initialized) return;
|
|
100
|
+
if (!this._state) return;
|
|
101
|
+
yield this._initialized;
|
|
102
|
+
if (this._mode === "kv") {
|
|
103
|
+
yield this._state.storage.delete(this._getUserKey(room, connectionId));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this._state.storage.sql.exec(
|
|
107
|
+
sql`
|
|
108
|
+
DELETE FROM ${SQLITE_USER_TABLE}
|
|
109
|
+
WHERE room = ?
|
|
110
|
+
AND id = ?;
|
|
111
|
+
`,
|
|
112
|
+
room,
|
|
113
|
+
connectionId
|
|
114
|
+
);
|
|
56
115
|
});
|
|
57
116
|
}
|
|
58
117
|
deleteUsers(room) {
|
|
59
118
|
return __async(this, null, function* () {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
119
|
+
if (!this._initialized) return;
|
|
120
|
+
if (!this._state) return;
|
|
121
|
+
yield this._initialized;
|
|
122
|
+
if (this._mode === "kv") {
|
|
123
|
+
const map = yield this._state.storage.list({
|
|
124
|
+
allowConcurrency: true,
|
|
125
|
+
noCache: true,
|
|
126
|
+
prefix: `${KV_USER_PREFIX}::${room}`
|
|
127
|
+
});
|
|
128
|
+
const partitions = partitionByLength(Array.from(map.keys()), KV_CLOUDFLARE_DELETE_BATCH_LIMIT);
|
|
129
|
+
yield this._state.storage.transaction((tx) => __async(this, null, function* () {
|
|
130
|
+
yield partitions.reduce((promise, partition) => __async(this, null, function* () {
|
|
131
|
+
yield promise;
|
|
69
132
|
yield tx.delete(partition);
|
|
70
|
-
}));
|
|
71
|
-
})
|
|
72
|
-
|
|
133
|
+
}), Promise.resolve());
|
|
134
|
+
}));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this._state.storage.sql.exec(
|
|
138
|
+
sql`
|
|
139
|
+
DELETE FROM ${SQLITE_USER_TABLE}
|
|
140
|
+
WHERE room = ?;
|
|
141
|
+
`,
|
|
142
|
+
room
|
|
143
|
+
);
|
|
73
144
|
});
|
|
74
145
|
}
|
|
75
146
|
getStorageState(room) {
|
|
76
147
|
return __async(this, null, function* () {
|
|
77
|
-
|
|
78
|
-
|
|
148
|
+
if (!this._initialized) return null;
|
|
149
|
+
if (!this._state) return null;
|
|
150
|
+
yield this._initialized;
|
|
151
|
+
if (this._mode === "kv") {
|
|
152
|
+
const storage = yield this._state.storage.get(this._getStorageKey(room));
|
|
153
|
+
return storage != null ? storage : null;
|
|
154
|
+
}
|
|
155
|
+
const cursor = this._state.storage.sql.exec(
|
|
156
|
+
sql`
|
|
157
|
+
SELECT room, data
|
|
158
|
+
FROM ${SQLITE_STORAGE_TABLE}
|
|
159
|
+
WHERE room = ?;
|
|
160
|
+
`,
|
|
161
|
+
room
|
|
162
|
+
);
|
|
163
|
+
try {
|
|
164
|
+
const result = cursor.one();
|
|
165
|
+
return result.data;
|
|
166
|
+
} catch (e) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
79
169
|
});
|
|
80
170
|
}
|
|
81
171
|
getUser(room, connectionId) {
|
|
82
172
|
return __async(this, null, function* () {
|
|
83
|
-
|
|
84
|
-
|
|
173
|
+
if (!this._initialized) return null;
|
|
174
|
+
if (!this._state) return null;
|
|
175
|
+
yield this._initialized;
|
|
176
|
+
if (this._mode === "kv") {
|
|
177
|
+
const user = yield this._state.storage.get(this._getUserKey(room, connectionId));
|
|
178
|
+
return user != null ? user : null;
|
|
179
|
+
}
|
|
180
|
+
const cursor = this._state.storage.sql.exec(
|
|
181
|
+
sql`
|
|
182
|
+
SELECT id, data, room
|
|
183
|
+
FROM ${SQLITE_USER_TABLE}
|
|
184
|
+
WHERE room = ?
|
|
185
|
+
AND id = ?;
|
|
186
|
+
`,
|
|
187
|
+
room,
|
|
188
|
+
connectionId
|
|
189
|
+
);
|
|
190
|
+
try {
|
|
191
|
+
const result = cursor.one();
|
|
192
|
+
const user = JSON.parse(result.data);
|
|
193
|
+
return user;
|
|
194
|
+
} catch (e) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
85
197
|
});
|
|
86
198
|
}
|
|
87
199
|
getUsers(room) {
|
|
88
200
|
return __async(this, null, function* () {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
201
|
+
if (!this._initialized) return /* @__PURE__ */ new Map();
|
|
202
|
+
if (!this._state) return /* @__PURE__ */ new Map();
|
|
203
|
+
yield this._initialized;
|
|
204
|
+
if (this._mode === "kv") {
|
|
205
|
+
const entries = yield this._state.storage.list({
|
|
206
|
+
prefix: `${KV_USER_PREFIX}::${room}`
|
|
207
|
+
}).then((map) => Array.from(map.entries()));
|
|
208
|
+
return entries.reduce((map, [key, user]) => {
|
|
209
|
+
const connectionId = key.split("::").slice(-1)[0];
|
|
210
|
+
return !!connectionId ? map.set(connectionId, user) : map;
|
|
211
|
+
}, /* @__PURE__ */ new Map());
|
|
212
|
+
}
|
|
213
|
+
const cursor = this._state.storage.sql.exec(
|
|
214
|
+
sql`
|
|
215
|
+
SELECT id, room, data
|
|
216
|
+
FROM ${SQLITE_USER_TABLE}
|
|
217
|
+
WHERE room = ?;
|
|
218
|
+
`,
|
|
219
|
+
room
|
|
220
|
+
);
|
|
221
|
+
return cursor.toArray().reduce((map, { id, data }) => {
|
|
222
|
+
try {
|
|
223
|
+
const user = JSON.parse(data);
|
|
224
|
+
return map.set(id, user);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
return map;
|
|
227
|
+
}
|
|
228
|
+
}, /* @__PURE__ */ new Map());
|
|
93
229
|
});
|
|
94
230
|
}
|
|
95
231
|
getUsersSize(room) {
|
|
96
232
|
return __async(this, null, function* () {
|
|
97
|
-
|
|
98
|
-
return
|
|
233
|
+
if (!this._initialized) return 0;
|
|
234
|
+
if (!this._state) return 0;
|
|
235
|
+
yield this._initialized;
|
|
236
|
+
if (this._mode) {
|
|
237
|
+
const storage = yield this._state.storage.list({ prefix: `${KV_USER_PREFIX}::${room}` });
|
|
238
|
+
return storage.size;
|
|
239
|
+
}
|
|
240
|
+
const cursor = this._state.storage.sql.exec(
|
|
241
|
+
sql`
|
|
242
|
+
SELECT COUNT(*) count
|
|
243
|
+
FROM ${SQLITE_USER_TABLE}
|
|
244
|
+
WHERE room = ?;
|
|
245
|
+
`,
|
|
246
|
+
room
|
|
247
|
+
);
|
|
248
|
+
try {
|
|
249
|
+
const result = cursor.one();
|
|
250
|
+
return castNumber(result.count);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
99
254
|
});
|
|
100
255
|
}
|
|
256
|
+
initialize(roomContext) {
|
|
257
|
+
this._initialized = (() => __async(this, null, function* () {
|
|
258
|
+
const { state } = roomContext;
|
|
259
|
+
this._state = state;
|
|
260
|
+
if (this._mode === "sqlite") {
|
|
261
|
+
this._state.storage.sql.exec(sql`
|
|
262
|
+
CREATE TABLE IF NOT EXISTS ${SQLITE_STORAGE_TABLE}(
|
|
263
|
+
room TEXT PRIMARY KEY,
|
|
264
|
+
data TEXT NOT NULL
|
|
265
|
+
);
|
|
266
|
+
CREATE TABLE IF NOT EXISTS ${SQLITE_USER_TABLE}(
|
|
267
|
+
id TEXT PRIMARY KEY,
|
|
268
|
+
data TEXT NOT NULL,
|
|
269
|
+
room TEXT NOT NULL
|
|
270
|
+
);
|
|
271
|
+
CREATE INDEX IF NOT EXISTS ${SQLITE_USER_TABLE}_room ON ${SQLITE_USER_TABLE}(room);
|
|
272
|
+
`);
|
|
273
|
+
}
|
|
274
|
+
return true;
|
|
275
|
+
}))();
|
|
276
|
+
return this;
|
|
277
|
+
}
|
|
101
278
|
setStorageState(room, state) {
|
|
102
279
|
return __async(this, null, function* () {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
280
|
+
if (!this._initialized) return;
|
|
281
|
+
if (!this._state) return;
|
|
282
|
+
yield this._initialized;
|
|
283
|
+
if (this._mode === "kv") {
|
|
284
|
+
yield this._state.storage.put(this._getStorageKey(room), state, { allowConcurrency: true });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
this._state.storage.sql.exec(
|
|
288
|
+
sql`
|
|
289
|
+
INSERT INTO ${SQLITE_STORAGE_TABLE} VALUES (?, ?);
|
|
290
|
+
`,
|
|
291
|
+
room,
|
|
292
|
+
state
|
|
293
|
+
);
|
|
106
294
|
});
|
|
107
295
|
}
|
|
108
296
|
_getStorageKey(room) {
|
|
109
|
-
|
|
297
|
+
if (this._mode === "sqlite") {
|
|
298
|
+
throw new Error("Attempted to get kv key for sqlite storage");
|
|
299
|
+
}
|
|
300
|
+
return `${KV_STORAGE_PREFIX}::${room}`;
|
|
110
301
|
}
|
|
111
302
|
_getUserKey(room, connectionId) {
|
|
112
|
-
|
|
303
|
+
if (this._mode === "sqlite") {
|
|
304
|
+
throw new Error("Attempted to get kv key for sqlite storage");
|
|
305
|
+
}
|
|
306
|
+
return `${KV_USER_PREFIX}::${room}::${connectionId}`;
|
|
113
307
|
}
|
|
114
308
|
};
|
|
115
309
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pluv/persistence-cloudflare-transactional-storage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "@pluv/io crdt storage persistence for cloudflare via transactional storage",
|
|
5
5
|
"author": "leedavidcs",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,16 +17,16 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@pluv/io": "^0.
|
|
21
|
-
"@pluv/types": "^0.
|
|
20
|
+
"@pluv/io": "^0.42.0",
|
|
21
|
+
"@pluv/types": "^0.42.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@cloudflare/workers-types": "^4.
|
|
25
|
-
"eslint": "^9.25.
|
|
24
|
+
"@cloudflare/workers-types": "^4.20250427.0",
|
|
25
|
+
"eslint": "^9.25.1",
|
|
26
26
|
"tsup": "^8.4.0",
|
|
27
27
|
"typescript": "^5.8.3",
|
|
28
|
-
"@pluv/tsconfig": "^0.
|
|
29
|
-
"eslint-config-pluv": "^0.
|
|
28
|
+
"@pluv/tsconfig": "^0.42.0",
|
|
29
|
+
"eslint-config-pluv": "^0.42.0"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AbstractPersistence } from "@pluv/io";
|
|
2
2
|
import type { JsonObject } from "@pluv/types";
|
|
3
|
-
import { partitionByLength } from "./utils";
|
|
3
|
+
import { castNumber, partitionByLength, sql } from "./utils";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* !HACK
|
|
@@ -8,92 +8,319 @@ import { partitionByLength } from "./utils";
|
|
|
8
8
|
* @see https://developers.cloudflare.com/durable-objects/api/transactional-storage-api/#delete
|
|
9
9
|
* @date July 8, 2024
|
|
10
10
|
*/
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
11
|
+
const KV_CLOUDFLARE_DELETE_BATCH_LIMIT = 128;
|
|
12
|
+
const KV_STORAGE_PREFIX = "$PLUV_STORAGE;";
|
|
13
|
+
const KV_USER_PREFIX = "$PLUV_USER";
|
|
14
|
+
|
|
15
|
+
const SQLITE_STORAGE_TABLE = "__pluv_storage";
|
|
16
|
+
const SQLITE_USER_TABLE = "__pluv_user";
|
|
14
17
|
|
|
15
18
|
export interface PersistenceCloudflareTransactionalStorageConfig {
|
|
16
|
-
|
|
19
|
+
mode: "kv" | "sqlite";
|
|
17
20
|
}
|
|
18
|
-
|
|
19
21
|
export class PersistenceCloudflareTransactionalStorage extends AbstractPersistence {
|
|
20
|
-
private
|
|
22
|
+
private _mode: "kv" | "sqlite";
|
|
23
|
+
private _state: DurableObjectState | null = null;
|
|
24
|
+
private _initialized: Promise<true> | null = null;
|
|
21
25
|
|
|
22
26
|
constructor(config: PersistenceCloudflareTransactionalStorageConfig) {
|
|
23
27
|
super();
|
|
24
28
|
|
|
25
|
-
const {
|
|
29
|
+
const { mode } = config;
|
|
26
30
|
|
|
27
|
-
this.
|
|
31
|
+
this._mode = mode;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
public async addUser(room: string, connectionId: string, user: JsonObject | null): Promise<void> {
|
|
31
|
-
|
|
35
|
+
if (!this._initialized) return;
|
|
36
|
+
if (!this._state) return;
|
|
37
|
+
|
|
38
|
+
await this._initialized;
|
|
39
|
+
|
|
40
|
+
if (this._mode === "kv") {
|
|
41
|
+
await this._state.storage.put(this._getUserKey(room, connectionId), user);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this._state.storage.sql.exec(
|
|
46
|
+
sql`
|
|
47
|
+
INSERT INTO ${SQLITE_USER_TABLE} VALUES (?, ?, ?);
|
|
48
|
+
`,
|
|
49
|
+
connectionId,
|
|
50
|
+
JSON.stringify(user),
|
|
51
|
+
room,
|
|
52
|
+
);
|
|
32
53
|
}
|
|
33
54
|
|
|
34
55
|
public async deleteStorageState(room: string): Promise<void> {
|
|
35
|
-
|
|
56
|
+
if (!this._initialized) return;
|
|
57
|
+
if (!this._state) return;
|
|
58
|
+
|
|
59
|
+
await this._initialized;
|
|
60
|
+
|
|
61
|
+
if (this._mode === "kv") {
|
|
62
|
+
await this._state.storage.delete(this._getStorageKey(room));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this._state.storage.sql.exec(
|
|
67
|
+
sql`
|
|
68
|
+
DELETE FROM ${SQLITE_STORAGE_TABLE}
|
|
69
|
+
WHERE room = ?;
|
|
70
|
+
`,
|
|
71
|
+
room,
|
|
72
|
+
);
|
|
36
73
|
}
|
|
37
74
|
|
|
38
75
|
public async deleteUser(room: string, connectionId: string): Promise<void> {
|
|
39
|
-
|
|
76
|
+
if (!this._initialized) return;
|
|
77
|
+
if (!this._state) return;
|
|
78
|
+
|
|
79
|
+
await this._initialized;
|
|
80
|
+
|
|
81
|
+
if (this._mode === "kv") {
|
|
82
|
+
await this._state.storage.delete(this._getUserKey(room, connectionId));
|
|
83
|
+
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this._state.storage.sql.exec(
|
|
88
|
+
sql`
|
|
89
|
+
DELETE FROM ${SQLITE_USER_TABLE}
|
|
90
|
+
WHERE room = ?
|
|
91
|
+
AND id = ?;
|
|
92
|
+
`,
|
|
93
|
+
room,
|
|
94
|
+
connectionId,
|
|
95
|
+
);
|
|
40
96
|
}
|
|
41
97
|
|
|
42
98
|
public async deleteUsers(room: string): Promise<void> {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
noCache: true,
|
|
46
|
-
prefix: USER_PREFIX,
|
|
47
|
-
});
|
|
99
|
+
if (!this._initialized) return;
|
|
100
|
+
if (!this._state) return;
|
|
48
101
|
|
|
49
|
-
|
|
102
|
+
await this._initialized;
|
|
50
103
|
|
|
51
|
-
|
|
52
|
-
await
|
|
53
|
-
|
|
104
|
+
if (this._mode === "kv") {
|
|
105
|
+
const map = await this._state.storage.list({
|
|
106
|
+
allowConcurrency: true,
|
|
107
|
+
noCache: true,
|
|
108
|
+
prefix: `${KV_USER_PREFIX}::${room}`,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const partitions = partitionByLength(Array.from(map.keys()), KV_CLOUDFLARE_DELETE_BATCH_LIMIT);
|
|
112
|
+
|
|
113
|
+
await this._state.storage.transaction(async (tx) => {
|
|
114
|
+
await partitions.reduce(async (promise, partition) => {
|
|
115
|
+
await promise;
|
|
54
116
|
await tx.delete(partition as string[]);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
117
|
+
}, Promise.resolve());
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this._state.storage.sql.exec(
|
|
124
|
+
sql`
|
|
125
|
+
DELETE FROM ${SQLITE_USER_TABLE}
|
|
126
|
+
WHERE room = ?;
|
|
127
|
+
`,
|
|
128
|
+
room,
|
|
129
|
+
);
|
|
58
130
|
}
|
|
59
131
|
|
|
60
132
|
public async getStorageState(room: string): Promise<string | null> {
|
|
61
|
-
|
|
133
|
+
if (!this._initialized) return null;
|
|
134
|
+
if (!this._state) return null;
|
|
135
|
+
|
|
136
|
+
await this._initialized;
|
|
137
|
+
|
|
138
|
+
if (this._mode === "kv") {
|
|
139
|
+
const storage = await this._state.storage.get<string>(this._getStorageKey(room));
|
|
140
|
+
return storage ?? null;
|
|
141
|
+
}
|
|
62
142
|
|
|
63
|
-
|
|
143
|
+
const cursor = this._state.storage.sql.exec<{ room: string; data: string }>(
|
|
144
|
+
sql`
|
|
145
|
+
SELECT room, data
|
|
146
|
+
FROM ${SQLITE_STORAGE_TABLE}
|
|
147
|
+
WHERE room = ?;
|
|
148
|
+
`,
|
|
149
|
+
room,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const result = cursor.one();
|
|
154
|
+
|
|
155
|
+
return result.data;
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
64
159
|
}
|
|
65
160
|
|
|
66
161
|
public async getUser(room: string, connectionId: string): Promise<JsonObject | null> {
|
|
67
|
-
|
|
162
|
+
if (!this._initialized) return null;
|
|
163
|
+
if (!this._state) return null;
|
|
164
|
+
|
|
165
|
+
await this._initialized;
|
|
166
|
+
|
|
167
|
+
if (this._mode === "kv") {
|
|
168
|
+
const user = await this._state.storage.get<JsonObject | null>(this._getUserKey(room, connectionId));
|
|
169
|
+
|
|
170
|
+
return user ?? null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const cursor = this._state.storage.sql.exec<{ id: string; data: string; room: string }>(
|
|
174
|
+
sql`
|
|
175
|
+
SELECT id, data, room
|
|
176
|
+
FROM ${SQLITE_USER_TABLE}
|
|
177
|
+
WHERE room = ?
|
|
178
|
+
AND id = ?;
|
|
179
|
+
`,
|
|
180
|
+
room,
|
|
181
|
+
connectionId,
|
|
182
|
+
);
|
|
68
183
|
|
|
69
|
-
|
|
184
|
+
try {
|
|
185
|
+
const result = cursor.one();
|
|
186
|
+
const user = JSON.parse(result.data);
|
|
187
|
+
|
|
188
|
+
return user;
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
70
192
|
}
|
|
71
193
|
|
|
72
|
-
public async getUsers(room: string): Promise<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
194
|
+
public async getUsers(room: string): Promise<Map<[connectionId: string][0], JsonObject | null>> {
|
|
195
|
+
if (!this._initialized) return new Map();
|
|
196
|
+
if (!this._state) return new Map();
|
|
197
|
+
|
|
198
|
+
await this._initialized;
|
|
199
|
+
|
|
200
|
+
if (this._mode === "kv") {
|
|
201
|
+
const entries = await this._state.storage
|
|
202
|
+
.list<JsonObject | null>({
|
|
203
|
+
prefix: `${KV_USER_PREFIX}::${room}`,
|
|
204
|
+
})
|
|
205
|
+
.then((map) => Array.from(map.entries()));
|
|
206
|
+
|
|
207
|
+
return entries.reduce((map, [key, user]) => {
|
|
208
|
+
const connectionId = key.split("::").slice(-1)[0];
|
|
209
|
+
|
|
210
|
+
return !!connectionId ? map.set(connectionId, user) : map;
|
|
211
|
+
}, new Map<string, JsonObject | null>());
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const cursor = this._state.storage.sql.exec<{ id: string; data: string; room: string }>(
|
|
215
|
+
sql`
|
|
216
|
+
SELECT id, room, data
|
|
217
|
+
FROM ${SQLITE_USER_TABLE}
|
|
218
|
+
WHERE room = ?;
|
|
219
|
+
`,
|
|
220
|
+
room,
|
|
221
|
+
);
|
|
76
222
|
|
|
77
|
-
return
|
|
223
|
+
return cursor.toArray().reduce((map, { id, data }) => {
|
|
224
|
+
try {
|
|
225
|
+
const user = JSON.parse(data) as JsonObject;
|
|
226
|
+
|
|
227
|
+
return map.set(id, user);
|
|
228
|
+
} catch {
|
|
229
|
+
return map;
|
|
230
|
+
}
|
|
231
|
+
}, new Map<string, JsonObject | null>());
|
|
78
232
|
}
|
|
79
233
|
|
|
80
234
|
public async getUsersSize(room: string): Promise<number> {
|
|
81
|
-
|
|
235
|
+
if (!this._initialized) return 0;
|
|
236
|
+
if (!this._state) return 0;
|
|
237
|
+
|
|
238
|
+
await this._initialized;
|
|
239
|
+
|
|
240
|
+
if (this._mode) {
|
|
241
|
+
const storage = await this._state.storage.list({ prefix: `${KV_USER_PREFIX}::${room}` });
|
|
242
|
+
|
|
243
|
+
return storage.size;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const cursor = this._state.storage.sql.exec<{ count: number }>(
|
|
247
|
+
sql`
|
|
248
|
+
SELECT COUNT(*) count
|
|
249
|
+
FROM ${SQLITE_USER_TABLE}
|
|
250
|
+
WHERE room = ?;
|
|
251
|
+
`,
|
|
252
|
+
room,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const result = cursor.one();
|
|
82
257
|
|
|
83
|
-
|
|
258
|
+
return castNumber(result.count);
|
|
259
|
+
} catch {
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public initialize(roomContext: { state: DurableObjectState }): this {
|
|
265
|
+
this._initialized = (async () => {
|
|
266
|
+
const { state } = roomContext;
|
|
267
|
+
|
|
268
|
+
this._state = state;
|
|
269
|
+
|
|
270
|
+
if (this._mode === "sqlite") {
|
|
271
|
+
this._state.storage.sql.exec(sql`
|
|
272
|
+
CREATE TABLE IF NOT EXISTS ${SQLITE_STORAGE_TABLE}(
|
|
273
|
+
room TEXT PRIMARY KEY,
|
|
274
|
+
data TEXT NOT NULL
|
|
275
|
+
);
|
|
276
|
+
CREATE TABLE IF NOT EXISTS ${SQLITE_USER_TABLE}(
|
|
277
|
+
id TEXT PRIMARY KEY,
|
|
278
|
+
data TEXT NOT NULL,
|
|
279
|
+
room TEXT NOT NULL
|
|
280
|
+
);
|
|
281
|
+
CREATE INDEX IF NOT EXISTS ${SQLITE_USER_TABLE}_room ON ${SQLITE_USER_TABLE}(room);
|
|
282
|
+
`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return true as const;
|
|
286
|
+
})();
|
|
287
|
+
|
|
288
|
+
return this;
|
|
84
289
|
}
|
|
85
290
|
|
|
86
291
|
public async setStorageState(room: string, state: string): Promise<void> {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
292
|
+
if (!this._initialized) return;
|
|
293
|
+
if (!this._state) return;
|
|
294
|
+
|
|
295
|
+
await this._initialized;
|
|
296
|
+
|
|
297
|
+
if (this._mode === "kv") {
|
|
298
|
+
await this._state.storage.put(this._getStorageKey(room), state, { allowConcurrency: true });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
this._state.storage.sql.exec(
|
|
303
|
+
sql`
|
|
304
|
+
INSERT INTO ${SQLITE_STORAGE_TABLE} VALUES (?, ?);
|
|
305
|
+
`,
|
|
306
|
+
room,
|
|
307
|
+
state,
|
|
308
|
+
);
|
|
90
309
|
}
|
|
91
310
|
|
|
92
311
|
private _getStorageKey(room: string): string {
|
|
93
|
-
|
|
312
|
+
if (this._mode === "sqlite") {
|
|
313
|
+
throw new Error("Attempted to get kv key for sqlite storage");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return `${KV_STORAGE_PREFIX}::${room}`;
|
|
94
317
|
}
|
|
95
318
|
|
|
96
319
|
private _getUserKey(room: string, connectionId: string): string {
|
|
97
|
-
|
|
320
|
+
if (this._mode === "sqlite") {
|
|
321
|
+
throw new Error("Attempted to get kv key for sqlite storage");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return `${KV_USER_PREFIX}::${room}::${connectionId}`;
|
|
98
325
|
}
|
|
99
326
|
}
|
package/src/utils/index.ts
CHANGED
package/src/utils/sql.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const sql = (strings: TemplateStringsArray, ...values: any[]): string => {
|
|
2
|
+
return strings
|
|
3
|
+
.reduce((result, str, i) => {
|
|
4
|
+
// Compresses the string to a single line (not including the interpolated values)
|
|
5
|
+
const cleaned = str.replace(/(\n[ \t]*)+/g, " ");
|
|
6
|
+
|
|
7
|
+
return result + cleaned + (i < values.length ? values[i] : "");
|
|
8
|
+
}, "")
|
|
9
|
+
.trim();
|
|
10
|
+
};
|