@pluv/io 3.2.2 → 4.0.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/.turbo/turbo-build.log +15 -17
- package/CHANGELOG.md +99 -0
- package/dist/index.d.mts +427 -423
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1390 -1476
- package/dist/index.mjs.map +1 -0
- package/package.json +18 -14
- package/src/IORoom.ts +65 -17
- package/src/PluvIO.ts +4 -16
- package/src/PluvRouter.ts +3 -10
- package/src/PluvServer.ts +31 -22
- package/src/index.ts +1 -0
- package/src/types.ts +13 -2
- package/src/version.generated.ts +1 -1
- package/tsdown.config.ts +14 -0
- package/dist/index.d.ts +0 -539
- package/dist/index.js +0 -1575
package/dist/index.mjs
CHANGED
|
@@ -1,1532 +1,1446 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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));
|
|
20
|
-
var __objRest = (source, exclude) => {
|
|
21
|
-
var target = {};
|
|
22
|
-
for (var prop in source)
|
|
23
|
-
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
24
|
-
target[prop] = source[prop];
|
|
25
|
-
if (source != null && __getOwnPropSymbols)
|
|
26
|
-
for (var prop of __getOwnPropSymbols(source)) {
|
|
27
|
-
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
28
|
-
target[prop] = source[prop];
|
|
29
|
-
}
|
|
30
|
-
return target;
|
|
31
|
-
};
|
|
32
|
-
var __async = (__this, __arguments, generator) => {
|
|
33
|
-
return new Promise((resolve, reject) => {
|
|
34
|
-
var fulfilled = (value) => {
|
|
35
|
-
try {
|
|
36
|
-
step(generator.next(value));
|
|
37
|
-
} catch (e) {
|
|
38
|
-
reject(e);
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
var rejected = (value) => {
|
|
42
|
-
try {
|
|
43
|
-
step(generator.throw(value));
|
|
44
|
-
} catch (e) {
|
|
45
|
-
reject(e);
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
49
|
-
step((generator = generator.apply(__this, __arguments)).next());
|
|
50
|
-
});
|
|
51
|
-
};
|
|
1
|
+
import { makeSubject, subscribe } from "wonka";
|
|
2
|
+
import hkdf from "@panva/hkdf";
|
|
3
|
+
import { EncryptJWT, jwtDecrypt } from "jose";
|
|
4
|
+
import { noop } from "@pluv/crdt";
|
|
5
|
+
import colors from "kleur";
|
|
52
6
|
|
|
53
|
-
|
|
54
|
-
var AbstractPersistence = class {
|
|
55
|
-
};
|
|
7
|
+
//#region src/AbstractPersistence.ts
|
|
8
|
+
var AbstractPersistence = class {};
|
|
56
9
|
|
|
57
|
-
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/Persistence.ts
|
|
58
12
|
var Persistence = class extends AbstractPersistence {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
getUsersSize(room) {
|
|
113
|
-
return __async(this, null, function* () {
|
|
114
|
-
return this._users.size;
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
initialize(roomContext) {
|
|
118
|
-
return this;
|
|
119
|
-
}
|
|
120
|
-
setStorageState(room, state) {
|
|
121
|
-
this._storages.set(room, state);
|
|
122
|
-
return Promise.resolve();
|
|
123
|
-
}
|
|
13
|
+
_storages = /* @__PURE__ */ new Map();
|
|
14
|
+
_users = /* @__PURE__ */ new Map();
|
|
15
|
+
addUser(room, connectionId, user) {
|
|
16
|
+
const users = this._users.get(room) ?? /* @__PURE__ */ new Map();
|
|
17
|
+
this._users.set(room, users);
|
|
18
|
+
users.set(connectionId, user);
|
|
19
|
+
return Promise.resolve();
|
|
20
|
+
}
|
|
21
|
+
deleteStorageState(room) {
|
|
22
|
+
this._storages.delete(room);
|
|
23
|
+
return Promise.resolve();
|
|
24
|
+
}
|
|
25
|
+
async deleteUser(room, connectionId) {
|
|
26
|
+
const users = this._users.get(room);
|
|
27
|
+
if (!users) return;
|
|
28
|
+
users.delete(connectionId);
|
|
29
|
+
if (!users.size) {
|
|
30
|
+
await this.deleteUsers(room);
|
|
31
|
+
await this.deleteStorageState(room);
|
|
32
|
+
}
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
}
|
|
35
|
+
deleteUsers(room) {
|
|
36
|
+
this._users.delete(room);
|
|
37
|
+
return Promise.resolve();
|
|
38
|
+
}
|
|
39
|
+
getSize(room) {
|
|
40
|
+
return Promise.resolve(this._users.get(room)?.size ?? 0);
|
|
41
|
+
}
|
|
42
|
+
getStorageState(room) {
|
|
43
|
+
const storage = this._storages.get(room) ?? null;
|
|
44
|
+
return Promise.resolve(storage);
|
|
45
|
+
}
|
|
46
|
+
getUser(room, connectionId) {
|
|
47
|
+
const user = this._users.get(room)?.get(connectionId);
|
|
48
|
+
return Promise.resolve(user ?? null);
|
|
49
|
+
}
|
|
50
|
+
async getUsers(room) {
|
|
51
|
+
const users = this._users.get(room);
|
|
52
|
+
if (!users) return /* @__PURE__ */ new Map();
|
|
53
|
+
return users;
|
|
54
|
+
}
|
|
55
|
+
async getUsersSize(room) {
|
|
56
|
+
return this._users.size;
|
|
57
|
+
}
|
|
58
|
+
initialize(roomContext) {
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
setStorageState(room, state) {
|
|
62
|
+
this._storages.set(room, state);
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
}
|
|
124
65
|
};
|
|
125
66
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// src/AbstractPubSub.ts
|
|
130
|
-
var AbstractPubSub = class {
|
|
131
|
-
};
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/AbstractPubSub.ts
|
|
69
|
+
var AbstractPubSub = class {};
|
|
132
70
|
|
|
133
|
-
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/PubSub.ts
|
|
134
73
|
var PubSub = class extends AbstractPubSub {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
this._subjects.delete(roomName);
|
|
171
|
-
this._subscriptionsRefs.delete(roomName);
|
|
172
|
-
this._unsubscriptions.delete(subscriptionId);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const index = refs.indexOf(subscriptionId);
|
|
176
|
-
this._subscriptionsRefs.set(
|
|
177
|
-
roomName,
|
|
178
|
-
index === -1 ? refs : [...refs.slice(0, index), ...refs.slice(index + 1)]
|
|
179
|
-
);
|
|
180
|
-
unsubscribe();
|
|
181
|
-
this._unsubscriptions.delete(subscriptionId);
|
|
182
|
-
}
|
|
74
|
+
_currentSubscriptionId = 0;
|
|
75
|
+
_subjects = /* @__PURE__ */ new Map();
|
|
76
|
+
_subscriptionsRefs = /* @__PURE__ */ new Map();
|
|
77
|
+
_unsubscriptions = /* @__PURE__ */ new Map();
|
|
78
|
+
async publish(roomName, payload) {
|
|
79
|
+
const subject = this._subjects.get(roomName);
|
|
80
|
+
if (!subject) return;
|
|
81
|
+
Promise.resolve(subject.next(payload));
|
|
82
|
+
}
|
|
83
|
+
subscribe(roomName, onMessage) {
|
|
84
|
+
const id = this._currentSubscriptionId++;
|
|
85
|
+
const refs = this._subscriptionsRefs.get(roomName) ?? [];
|
|
86
|
+
const subject = makeSubject();
|
|
87
|
+
this._subjects.set(roomName, subject);
|
|
88
|
+
this._subscriptionsRefs.set(roomName, [...refs, id]);
|
|
89
|
+
const unsubscribe = subscribe(onMessage)(subject.source).unsubscribe;
|
|
90
|
+
this._unsubscriptions.set(id, [roomName, unsubscribe]);
|
|
91
|
+
return Promise.resolve(id);
|
|
92
|
+
}
|
|
93
|
+
unsubscribe(subscriptionId) {
|
|
94
|
+
const [roomName, unsubscribe] = this._unsubscriptions.get(subscriptionId) ?? [];
|
|
95
|
+
if (!roomName || !unsubscribe) return;
|
|
96
|
+
const refs = this._subscriptionsRefs.get(roomName);
|
|
97
|
+
if (!refs) return;
|
|
98
|
+
if (refs.length === 1) {
|
|
99
|
+
this._subjects.delete(roomName);
|
|
100
|
+
this._subscriptionsRefs.delete(roomName);
|
|
101
|
+
this._unsubscriptions.delete(subscriptionId);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const index = refs.indexOf(subscriptionId);
|
|
105
|
+
this._subscriptionsRefs.set(roomName, index === -1 ? refs : [...refs.slice(0, index), ...refs.slice(index + 1)]);
|
|
106
|
+
unsubscribe();
|
|
107
|
+
this._unsubscriptions.delete(subscriptionId);
|
|
108
|
+
}
|
|
183
109
|
};
|
|
184
110
|
|
|
185
|
-
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/AbstractPlatform.ts
|
|
186
113
|
var AbstractPlatform = class {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
114
|
+
_initContext;
|
|
115
|
+
_roomContext;
|
|
116
|
+
_initialized = false;
|
|
117
|
+
persistence;
|
|
118
|
+
pubSub;
|
|
119
|
+
_createToken;
|
|
120
|
+
_fetch;
|
|
121
|
+
constructor(config = {}) {
|
|
122
|
+
const { roomContext, persistence, pubSub } = config;
|
|
123
|
+
this._meta = config._meta;
|
|
124
|
+
if (!!roomContext) this._roomContext = roomContext;
|
|
125
|
+
this.persistence = persistence ?? new Persistence();
|
|
126
|
+
this.pubSub = pubSub ?? new PubSub();
|
|
127
|
+
}
|
|
128
|
+
validateConfig(config) {}
|
|
129
|
+
_initialize() {
|
|
130
|
+
if (this._initialized) throw new Error("Platform is already initialized");
|
|
131
|
+
this._initialized = true;
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
202
134
|
};
|
|
203
135
|
|
|
204
|
-
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/AbstractWebSocket.ts
|
|
205
138
|
var AbstractWebSocket = class {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
139
|
+
/** The connection is not yet open. */
|
|
140
|
+
CONNECTING = 0;
|
|
141
|
+
/** The connection is open and ready to communicate. */
|
|
142
|
+
OPEN = 1;
|
|
143
|
+
/** The connection is in the process of closing. */
|
|
144
|
+
CLOSING = 2;
|
|
145
|
+
/** The connection is closed. */
|
|
146
|
+
CLOSED = 3;
|
|
147
|
+
persistence;
|
|
148
|
+
room;
|
|
149
|
+
webSocket;
|
|
150
|
+
_platform;
|
|
151
|
+
constructor(webSocket, config) {
|
|
152
|
+
const { persistence, platform, room } = config;
|
|
153
|
+
this.persistence = persistence;
|
|
154
|
+
this._platform = platform;
|
|
155
|
+
this.room = room;
|
|
156
|
+
this.webSocket = webSocket;
|
|
157
|
+
}
|
|
158
|
+
handleError(params) {
|
|
159
|
+
const { error, message: _message, room, session } = params;
|
|
160
|
+
const message = error instanceof Error ? error.message : "Unexpected error";
|
|
161
|
+
const stack = error instanceof Error ? error.stack ?? null : null;
|
|
162
|
+
const connectionId = session?.id ?? null;
|
|
163
|
+
return this.sendMessage({
|
|
164
|
+
connectionId,
|
|
165
|
+
data: {
|
|
166
|
+
message,
|
|
167
|
+
stack
|
|
168
|
+
},
|
|
169
|
+
room,
|
|
170
|
+
type: "$error",
|
|
171
|
+
user: session?.user ?? null
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
sendMessage(data) {
|
|
175
|
+
return this.send(JSON.stringify(data));
|
|
176
|
+
}
|
|
238
177
|
};
|
|
239
178
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
return yield hkdf("sha256", secret, "", "Pluv.io Generated Encryption Key", 32);
|
|
246
|
-
});
|
|
247
|
-
var now = () => Date.now() / 1e3 | 0;
|
|
248
|
-
var authorize = (params) => {
|
|
249
|
-
const { platform, secret } = params;
|
|
250
|
-
const encode = (encodeParams) => __async(null, null, function* () {
|
|
251
|
-
const { maxAge = DEFAULT_MAX_AGE_MS, room, user } = encodeParams;
|
|
252
|
-
const encryptionSecret = yield getEncryptionKey(secret);
|
|
253
|
-
const token = yield new EncryptJWT({
|
|
254
|
-
room,
|
|
255
|
-
sub: room ? `${room}|${user.id}` : user.id,
|
|
256
|
-
user
|
|
257
|
-
}).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(now() + maxAge).setJti(platform.randomUUID()).encrypt(encryptionSecret);
|
|
258
|
-
return token.toString();
|
|
259
|
-
});
|
|
260
|
-
const decode = (jwt) => __async(null, null, function* () {
|
|
261
|
-
const encryptionSecret = yield getEncryptionKey(secret);
|
|
262
|
-
const { payload } = yield jwtDecrypt(jwt, encryptionSecret, {
|
|
263
|
-
clockTolerance: 15
|
|
264
|
-
});
|
|
265
|
-
return payload != null ? payload : null;
|
|
266
|
-
});
|
|
267
|
-
return { decode, encode };
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/authorize.ts
|
|
181
|
+
const DEFAULT_MAX_AGE_MS = 6e4;
|
|
182
|
+
const getEncryptionKey = async (secret) => {
|
|
183
|
+
return await hkdf("sha256", secret, "", "Pluv.io Generated Encryption Key", 32);
|
|
268
184
|
};
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
self: (_e = (_d = this._self) == null ? void 0 : _d.bind(this)) != null ? _e : null,
|
|
293
|
-
sync: (_g = (_f = this._sync) == null ? void 0 : _f.bind(this)) != null ? _g : null
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
broadcast(resolver) {
|
|
297
|
-
const { broadcast } = this.config;
|
|
298
|
-
if (!!broadcast) throw new Error("Broadcast was already defined for this procedure");
|
|
299
|
-
return new _PluvProcedure(__spreadProps(__spreadValues({}, this.config), {
|
|
300
|
-
broadcast: resolver
|
|
301
|
-
}));
|
|
302
|
-
}
|
|
303
|
-
input(input) {
|
|
304
|
-
return new _PluvProcedure({
|
|
305
|
-
input
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
self(resolver) {
|
|
309
|
-
const { self } = this.config;
|
|
310
|
-
if (!!self) throw new Error("Self was already defined for this procedure");
|
|
311
|
-
return new _PluvProcedure(__spreadProps(__spreadValues({}, this.config), {
|
|
312
|
-
self: resolver
|
|
313
|
-
}));
|
|
314
|
-
}
|
|
315
|
-
sync(resolver) {
|
|
316
|
-
const { sync } = this.config;
|
|
317
|
-
if (!!sync) throw new Error("Sync was already defined for this procedure");
|
|
318
|
-
return new _PluvProcedure(__spreadProps(__spreadValues({}, this.config), {
|
|
319
|
-
sync: resolver
|
|
320
|
-
}));
|
|
321
|
-
}
|
|
322
|
-
_resolver() {
|
|
323
|
-
return (data, context) => {
|
|
324
|
-
var _a, _b, _c;
|
|
325
|
-
return __spreadValues(__spreadValues(__spreadValues({}, (_a = this._broadcast) == null ? void 0 : _a.call(this, data, context)), (_b = this._self) == null ? void 0 : _b.call(this, data, context)), (_c = this._sync) == null ? void 0 : _c.call(this, data, context));
|
|
326
|
-
};
|
|
327
|
-
}
|
|
185
|
+
const now = () => Date.now() / 1e3 | 0;
|
|
186
|
+
const authorize = (params) => {
|
|
187
|
+
const { platform, secret } = params;
|
|
188
|
+
const encode = async (encodeParams) => {
|
|
189
|
+
const { maxAge = DEFAULT_MAX_AGE_MS, room, user } = encodeParams;
|
|
190
|
+
const encryptionSecret = await getEncryptionKey(secret);
|
|
191
|
+
return (await new EncryptJWT({
|
|
192
|
+
room,
|
|
193
|
+
sub: room ? `${room}|${user.id}` : user.id,
|
|
194
|
+
user
|
|
195
|
+
}).setProtectedHeader({
|
|
196
|
+
alg: "dir",
|
|
197
|
+
enc: "A256GCM"
|
|
198
|
+
}).setIssuedAt().setExpirationTime(now() + maxAge).setJti(platform.randomUUID()).encrypt(encryptionSecret)).toString();
|
|
199
|
+
};
|
|
200
|
+
const decode = async (jwt) => {
|
|
201
|
+
const { payload } = await jwtDecrypt(jwt, await getEncryptionKey(secret), { clockTolerance: 15 });
|
|
202
|
+
return payload ?? null;
|
|
203
|
+
};
|
|
204
|
+
return {
|
|
205
|
+
decode,
|
|
206
|
+
encode
|
|
207
|
+
};
|
|
328
208
|
};
|
|
329
209
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/PluvProcedure.ts
|
|
212
|
+
var PluvProcedure = class PluvProcedure {
|
|
213
|
+
_broadcast = null;
|
|
214
|
+
_input = null;
|
|
215
|
+
_self = null;
|
|
216
|
+
_sync = null;
|
|
217
|
+
get config() {
|
|
218
|
+
return {
|
|
219
|
+
broadcast: this._broadcast?.bind(this) ?? null,
|
|
220
|
+
input: this._input ?? null,
|
|
221
|
+
resolver: this._resolver(),
|
|
222
|
+
self: this._self?.bind(this) ?? null,
|
|
223
|
+
sync: this._sync?.bind(this) ?? null
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
constructor(config = {}) {
|
|
227
|
+
const { broadcast, input, self, sync } = config;
|
|
228
|
+
this._broadcast = broadcast ?? null;
|
|
229
|
+
this._input = input ?? null;
|
|
230
|
+
this._self = self ?? null;
|
|
231
|
+
this._sync = sync ?? null;
|
|
232
|
+
}
|
|
233
|
+
broadcast(resolver) {
|
|
234
|
+
const { broadcast } = this.config;
|
|
235
|
+
if (!!broadcast) throw new Error("Broadcast was already defined for this procedure");
|
|
236
|
+
return new PluvProcedure({
|
|
237
|
+
...this.config,
|
|
238
|
+
broadcast: resolver
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
input(input) {
|
|
242
|
+
return new PluvProcedure({ input });
|
|
243
|
+
}
|
|
244
|
+
self(resolver) {
|
|
245
|
+
const { self } = this.config;
|
|
246
|
+
if (!!self) throw new Error("Self was already defined for this procedure");
|
|
247
|
+
return new PluvProcedure({
|
|
248
|
+
...this.config,
|
|
249
|
+
self: resolver
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
sync(resolver) {
|
|
253
|
+
const { sync } = this.config;
|
|
254
|
+
if (!!sync) throw new Error("Sync was already defined for this procedure");
|
|
255
|
+
return new PluvProcedure({
|
|
256
|
+
...this.config,
|
|
257
|
+
sync: resolver
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
_resolver() {
|
|
261
|
+
return (data, context) => {
|
|
262
|
+
return {
|
|
263
|
+
...this._broadcast?.(data, context),
|
|
264
|
+
...this._self?.(data, context),
|
|
265
|
+
...this._sync?.(data, context)
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
}
|
|
352
269
|
};
|
|
353
270
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/PluvRouter.ts
|
|
273
|
+
var PluvRouter = class PluvRouter {
|
|
274
|
+
_defs = { events: {} };
|
|
275
|
+
constructor(events) {
|
|
276
|
+
const invalidName = Object.keys(events).find((name) => !this._isValidEventName(name));
|
|
277
|
+
if (typeof invalidName === "string") throw new Error(`Invalid event name. Event names must be formatted as valid JavaScript variable names: "${invalidName}"`);
|
|
278
|
+
this._defs = { events };
|
|
279
|
+
}
|
|
280
|
+
static merge(...routers) {
|
|
281
|
+
return new PluvRouter(Object.assign(Object.create(null), ...routers.map((router) => router._defs.events)));
|
|
282
|
+
}
|
|
283
|
+
_isValidEventName(name) {
|
|
284
|
+
return /^[a-z_$][a-z0-9_$]*$/gi.test(name);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
361
287
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region src/constants.ts
|
|
290
|
+
const GARBAGE_COLLECT_INTERVAL_MS = 6e4;
|
|
291
|
+
const MAX_PRESENCE_SIZE_BYTES = 512;
|
|
292
|
+
const MAX_STORAGE_SIZE_BYTES = 31457280;
|
|
293
|
+
const MAX_USER_ID_LENGTH = 128;
|
|
294
|
+
const MAX_USER_SIZE_BYTES = 512;
|
|
295
|
+
const PING_TIMEOUT_MS = 3e4;
|
|
369
296
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region src/utils/oneLine.ts
|
|
299
|
+
const oneLine = (strings, ...values) => {
|
|
300
|
+
return strings.map((str) => str.replace(/\s+/g, " ")).reduce((acc, str, i) => acc + str + (i < values.length ? values[i] : ""), "").replace(/\s+/g, " ").trim();
|
|
373
301
|
};
|
|
374
302
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/utils/pickBy.ts
|
|
305
|
+
const pickBy = (obj, by) => {
|
|
306
|
+
const result = {};
|
|
307
|
+
const keys = Object.keys(obj);
|
|
308
|
+
for (let i = 0; i < keys.length; ++i) {
|
|
309
|
+
const key = keys[i];
|
|
310
|
+
const value = obj[key];
|
|
311
|
+
if (by(value)) result[key] = value;
|
|
312
|
+
}
|
|
313
|
+
return result;
|
|
385
314
|
};
|
|
386
315
|
|
|
387
|
-
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/IORoom.ts
|
|
388
318
|
var IORoom = class {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const size = this.getSize();
|
|
586
|
-
this._logDebug(oneLine`
|
|
319
|
+
id;
|
|
320
|
+
_doc;
|
|
321
|
+
_lastGarbageCollectMs = -1 * (GARBAGE_COLLECT_INTERVAL_MS + 1);
|
|
322
|
+
_uninitialize = null;
|
|
323
|
+
_authorize = null;
|
|
324
|
+
_context;
|
|
325
|
+
_crdt;
|
|
326
|
+
_debug;
|
|
327
|
+
_docFactory;
|
|
328
|
+
_getInitialStorage;
|
|
329
|
+
_listeners;
|
|
330
|
+
_platform;
|
|
331
|
+
_roomContext;
|
|
332
|
+
_router;
|
|
333
|
+
_sessions = /* @__PURE__ */ new Map();
|
|
334
|
+
_userSessionss = /* @__PURE__ */ new Map();
|
|
335
|
+
_wasDocEmptyOnInit = true;
|
|
336
|
+
_storageInitializedViaSession = false;
|
|
337
|
+
/**
|
|
338
|
+
* @ignore
|
|
339
|
+
* @readonly
|
|
340
|
+
* @deprecated Internal use only. Changes to this will never be marked as breaking.
|
|
341
|
+
*/
|
|
342
|
+
get _defs() {
|
|
343
|
+
return {
|
|
344
|
+
authorize: this._authorize,
|
|
345
|
+
context: this._context,
|
|
346
|
+
crdt: this._crdt,
|
|
347
|
+
events: this._router._defs.events,
|
|
348
|
+
platform: this._platform
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
get broadcast() {
|
|
352
|
+
const _broadcast = (event, data) => {
|
|
353
|
+
const message = {
|
|
354
|
+
type: event,
|
|
355
|
+
data
|
|
356
|
+
};
|
|
357
|
+
return Promise.resolve(this._broadcast({ message }));
|
|
358
|
+
};
|
|
359
|
+
return new Proxy(_broadcast, { get(fn, prop) {
|
|
360
|
+
return (data) => fn(prop, data);
|
|
361
|
+
} });
|
|
362
|
+
}
|
|
363
|
+
get _initialized() {
|
|
364
|
+
return Promise.resolve(!!this._uninitialize?.then((result) => !!result));
|
|
365
|
+
}
|
|
366
|
+
constructor(id, config) {
|
|
367
|
+
const { _meta, authorize: authorize$1, context, crdt = noop, debug, getInitialStorage, onRoomDestroyed, onStorageDestroyed, onMessage, onUserConnected, onUserDisconnected, platform, roomContext, router } = config;
|
|
368
|
+
this.id = id;
|
|
369
|
+
this._context = context;
|
|
370
|
+
this._crdt = crdt;
|
|
371
|
+
this._debug = debug;
|
|
372
|
+
this._docFactory = crdt.doc(() => ({}));
|
|
373
|
+
this._getInitialStorage = getInitialStorage;
|
|
374
|
+
this._roomContext = roomContext;
|
|
375
|
+
this._router = router;
|
|
376
|
+
this._platform = platform.initialize({
|
|
377
|
+
...!!_meta ? { _meta } : {},
|
|
378
|
+
roomContext
|
|
379
|
+
});
|
|
380
|
+
this._listeners = {
|
|
381
|
+
onRoomDestroyed: (event) => onRoomDestroyed?.(event),
|
|
382
|
+
onStorageDestroyed: (event) => onStorageDestroyed?.(event),
|
|
383
|
+
onMessage: (event) => onMessage?.(event),
|
|
384
|
+
onUserConnected: (event) => onUserConnected?.(event),
|
|
385
|
+
onUserDisconnected: (event) => onUserDisconnected?.(event)
|
|
386
|
+
};
|
|
387
|
+
if (authorize$1) this._authorize = authorize$1;
|
|
388
|
+
this._platform.getWebSockets().forEach((webSocket) => {
|
|
389
|
+
const deserialized = this._platform.getSerializedState(webSocket);
|
|
390
|
+
const sessionId = this._platform.getSessionId(webSocket);
|
|
391
|
+
if (!deserialized) return;
|
|
392
|
+
if (typeof sessionId !== "string") return;
|
|
393
|
+
const pluvWs = this._platform.convertWebSocket(webSocket, { room: this.id });
|
|
394
|
+
this._sessions.set(sessionId, pluvWs);
|
|
395
|
+
const userId = pluvWs.session.user?.id;
|
|
396
|
+
if (!!userId) this._addUserSession(userId, sessionId);
|
|
397
|
+
});
|
|
398
|
+
const { doc, uninitialize } = this._initialize();
|
|
399
|
+
this._doc = doc;
|
|
400
|
+
this._uninitialize = uninitialize;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* @description Closes a connection with the specified sessionId
|
|
404
|
+
* @param sessionId The session id of the connection to close
|
|
405
|
+
*/
|
|
406
|
+
async evict(sessionId) {
|
|
407
|
+
const session = this._sessions.get(sessionId);
|
|
408
|
+
if (session) session.state.quit = true;
|
|
409
|
+
await this._emitQuitters();
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* @description Closes all connections to this room, effectively destroying
|
|
413
|
+
* the room
|
|
414
|
+
*/
|
|
415
|
+
async evictAll() {
|
|
416
|
+
Array.from(this._sessions.values()).forEach((session) => {
|
|
417
|
+
session.state.quit = true;
|
|
418
|
+
});
|
|
419
|
+
await this._emitQuitters();
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* @description The IORoom will garbage collect occasionally as connections ping/pong the
|
|
423
|
+
* server
|
|
424
|
+
*/
|
|
425
|
+
async garbageCollect() {
|
|
426
|
+
this._lastGarbageCollectMs = Date.now();
|
|
427
|
+
this._emitQuitters();
|
|
428
|
+
this._emitSyncState();
|
|
429
|
+
}
|
|
430
|
+
getSize() {
|
|
431
|
+
const currentTime = (/* @__PURE__ */ new Date()).getTime();
|
|
432
|
+
/**
|
|
433
|
+
* @description Doing this instead of .size because some sessions
|
|
434
|
+
* in the map can be considered as "omitted".
|
|
435
|
+
* @date December 21, 2022
|
|
436
|
+
*/
|
|
437
|
+
return Array.from(this._sessions.values()).reduce((count, pluvWs) => {
|
|
438
|
+
if (pluvWs.state.quit) return count;
|
|
439
|
+
return currentTime - (this._platform.getLastPing(pluvWs) ?? pluvWs.state.timers.ping) > PING_TIMEOUT_MS ? count : count + 1;
|
|
440
|
+
}, 0);
|
|
441
|
+
}
|
|
442
|
+
onClose(webSocket) {
|
|
443
|
+
this._ensureDetached();
|
|
444
|
+
const wsSession = this._getAbstractWs(webSocket);
|
|
445
|
+
if (!wsSession) return async () => void 0;
|
|
446
|
+
return this._onClose(wsSession);
|
|
447
|
+
}
|
|
448
|
+
onError(webSocket) {
|
|
449
|
+
this._ensureDetached();
|
|
450
|
+
const wsSession = this._getAbstractWs(webSocket);
|
|
451
|
+
if (!wsSession) return async () => void 0;
|
|
452
|
+
return this._onClose(wsSession);
|
|
453
|
+
}
|
|
454
|
+
onMessage(webSocket) {
|
|
455
|
+
this._ensureDetached();
|
|
456
|
+
const wsSession = this._getAbstractWs(webSocket);
|
|
457
|
+
if (!wsSession) return async () => void 0;
|
|
458
|
+
return this._onMessage(wsSession);
|
|
459
|
+
}
|
|
460
|
+
async register(webSocket, ...options) {
|
|
461
|
+
const _options = options[0] ?? {};
|
|
462
|
+
const token = _options.token ?? null;
|
|
463
|
+
/**
|
|
464
|
+
* TODO
|
|
465
|
+
* @description Update the sessionId strategy so that the userId can be used to retrieve
|
|
466
|
+
* the session in O(1) time if authorized.
|
|
467
|
+
* @date April 15, 2025
|
|
468
|
+
*/
|
|
469
|
+
const sessionId = this._platform.getSessionId(webSocket);
|
|
470
|
+
if (typeof sessionId === "string" && this._sessions.has(sessionId)) return;
|
|
471
|
+
if (!await this._initialized) {
|
|
472
|
+
this._initialize();
|
|
473
|
+
await this._initialized;
|
|
474
|
+
}
|
|
475
|
+
const user = await this._getAuthorizedUser(token, _options);
|
|
476
|
+
const isUnauthorized = !!this._getIOAuthorize(_options) && !user;
|
|
477
|
+
const pluvWs = this._platform.convertWebSocket(webSocket, { room: this.id });
|
|
478
|
+
if (isUnauthorized) {
|
|
479
|
+
this._logDebug(`${colors.blue("Authorization failed for connection")}`);
|
|
480
|
+
pluvWs.handleError({
|
|
481
|
+
error: /* @__PURE__ */ new Error("Not authorized"),
|
|
482
|
+
room: this.id
|
|
483
|
+
});
|
|
484
|
+
pluvWs.close(3e3, "WebSocket unauthorized.");
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (!!user) {
|
|
488
|
+
const latest = this._getLatestPresence(user.id);
|
|
489
|
+
const prevState = pluvWs.state;
|
|
490
|
+
pluvWs.user = user;
|
|
491
|
+
this._platform.setSerializedState(pluvWs, {
|
|
492
|
+
...prevState,
|
|
493
|
+
presence: latest.presence,
|
|
494
|
+
timers: {
|
|
495
|
+
...prevState.timers,
|
|
496
|
+
presence: latest.timer
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
this._addUserSession(user.id, pluvWs.sessionId);
|
|
500
|
+
}
|
|
501
|
+
this._logDebug(`${colors.blue(`Registering connection for room ${this.id}:`)} ${pluvWs.sessionId}`);
|
|
502
|
+
await this._platform.acceptWebSocket(pluvWs);
|
|
503
|
+
this._sessions.set(pluvWs.sessionId, pluvWs);
|
|
504
|
+
await this._platform.persistence.addUser(this.id, pluvWs.sessionId, user ?? {});
|
|
505
|
+
if (this._platform._config.registrationMode === "attached") {
|
|
506
|
+
const onClose = this._onClose(pluvWs).bind(this);
|
|
507
|
+
const onMessage = this._onMessage(pluvWs).bind(this);
|
|
508
|
+
pluvWs.addEventListener("close", onClose);
|
|
509
|
+
pluvWs.addEventListener("error", onClose);
|
|
510
|
+
pluvWs.addEventListener("message", onMessage);
|
|
511
|
+
}
|
|
512
|
+
await this._emitRegistered(pluvWs);
|
|
513
|
+
const size = this.getSize();
|
|
514
|
+
this._logDebug(oneLine`
|
|
587
515
|
${colors.blue(`Registered connection for room ${this.id}:`)}
|
|
588
516
|
${pluvWs.sessionId}
|
|
589
517
|
`);
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
518
|
+
this._logDebug(`${colors.blue(`Room ${this.id} size:`)} ${size}`);
|
|
519
|
+
}
|
|
520
|
+
_addUserSession(userId, sessionId) {
|
|
521
|
+
const updated = (this._userSessionss.get(userId) ?? /* @__PURE__ */ new Set()).add(sessionId);
|
|
522
|
+
this._userSessionss.set(userId, updated);
|
|
523
|
+
return updated;
|
|
524
|
+
}
|
|
525
|
+
async _broadcast(params) {
|
|
526
|
+
const { message, senderId } = params;
|
|
527
|
+
const user = ((senderId ? this._sessions.get(senderId) ?? null : null)?.session ?? null)?.user ?? null;
|
|
528
|
+
if (typeof senderId !== "string") return;
|
|
529
|
+
this._platform.pubSub.publish(this.id, {
|
|
530
|
+
connectionId: senderId ?? null,
|
|
531
|
+
room: this.id,
|
|
532
|
+
user,
|
|
533
|
+
...message
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
async _closeWebSockets(webSockets) {
|
|
537
|
+
const closeWebSocket = async (webSocket) => {
|
|
538
|
+
webSocket.state = {
|
|
539
|
+
...webSocket.state,
|
|
540
|
+
quit: true
|
|
541
|
+
};
|
|
542
|
+
const sessionId = webSocket.sessionId;
|
|
543
|
+
this._logDebug(`${colors.blue(`Unregistering connection for room ${this.id}:`)} ${sessionId}`);
|
|
544
|
+
this._sessions.delete(sessionId);
|
|
545
|
+
await this._platform.persistence.deleteUser(this.id, sessionId).catch(() => null);
|
|
546
|
+
await this._broadcast({
|
|
547
|
+
message: {
|
|
548
|
+
type: "$exit",
|
|
549
|
+
data: { sessionId }
|
|
550
|
+
},
|
|
551
|
+
senderId: sessionId
|
|
552
|
+
});
|
|
553
|
+
const user = webSocket.session.user;
|
|
554
|
+
if (!!user) this._removeUserSession(user.id, sessionId);
|
|
555
|
+
try {
|
|
556
|
+
const [doc, context] = await Promise.all([this._doc, this._getContext()]);
|
|
557
|
+
const encodedState = doc.getEncodedState();
|
|
558
|
+
await Promise.resolve(this._listeners.onUserDisconnected({
|
|
559
|
+
context,
|
|
560
|
+
encodedState,
|
|
561
|
+
platform: this._platform,
|
|
562
|
+
room: this.id,
|
|
563
|
+
user
|
|
564
|
+
}));
|
|
565
|
+
} catch (error) {
|
|
566
|
+
console.error(error);
|
|
567
|
+
}
|
|
568
|
+
this._logDebug(`${colors.blue(`Unregistered connection for room ${this.id}:`)} ${webSocket.sessionId}`);
|
|
569
|
+
};
|
|
570
|
+
const promises = webSockets.map(async (webSocket) => {
|
|
571
|
+
await closeWebSocket(webSocket).catch(() => null);
|
|
572
|
+
});
|
|
573
|
+
await Promise.all(promises).catch(() => null);
|
|
574
|
+
const size = this.getSize();
|
|
575
|
+
this._logDebug(`${colors.blue(`Room ${this.id} size:`)} ${size}`);
|
|
576
|
+
if (!!size) return;
|
|
577
|
+
if (!this._uninitialize) return;
|
|
578
|
+
await (await this._uninitialize).bind(this)();
|
|
579
|
+
}
|
|
580
|
+
async _emitQuitters() {
|
|
581
|
+
const currentTime = (/* @__PURE__ */ new Date()).getTime();
|
|
582
|
+
const quitters = Array.from(this._sessions.values()).filter((pluvWs) => {
|
|
583
|
+
return pluvWs.state.quit || currentTime - pluvWs.state.timers.ping > PING_TIMEOUT_MS;
|
|
584
|
+
});
|
|
585
|
+
this._closeWebSockets(quitters);
|
|
586
|
+
}
|
|
587
|
+
async _emitRegistered(pluvWs) {
|
|
588
|
+
const session = pluvWs.session;
|
|
589
|
+
const sessionId = session.id;
|
|
590
|
+
const presence = session.presence;
|
|
591
|
+
const user = session.user;
|
|
592
|
+
const [doc, context] = await Promise.all([this._doc, this._getContext()]);
|
|
593
|
+
const encodedState = doc.isEmpty() ? null : doc.getEncodedState();
|
|
594
|
+
await this._sendSelfMessage({
|
|
595
|
+
type: "$registered",
|
|
596
|
+
data: {
|
|
597
|
+
presence,
|
|
598
|
+
sessionId,
|
|
599
|
+
state: encodedState,
|
|
600
|
+
timers: { presence: session.timers.presence }
|
|
601
|
+
}
|
|
602
|
+
}, {
|
|
603
|
+
sessionId,
|
|
604
|
+
user
|
|
605
|
+
});
|
|
606
|
+
try {
|
|
607
|
+
await Promise.resolve(this._listeners.onUserConnected({
|
|
608
|
+
context,
|
|
609
|
+
encodedState,
|
|
610
|
+
platform: this._platform,
|
|
611
|
+
room: this.id,
|
|
612
|
+
user,
|
|
613
|
+
webSocket: pluvWs.webSocket
|
|
614
|
+
}));
|
|
615
|
+
} catch (err) {
|
|
616
|
+
console.error(err);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
async _emitSyncState() {
|
|
620
|
+
const connectionIds = await this._platform.persistence.getUsers(this.id).then((map) => Object.keys(map));
|
|
621
|
+
await this._broadcast({ message: {
|
|
622
|
+
type: "$syncStateReceived",
|
|
623
|
+
data: { connectionIds }
|
|
624
|
+
} });
|
|
625
|
+
}
|
|
626
|
+
_ensureDetached() {
|
|
627
|
+
if (this._platform._config.registrationMode === "detached") return;
|
|
628
|
+
throw new Error("Platform must use detached mode");
|
|
629
|
+
}
|
|
630
|
+
_getAbstractWs(webSocket) {
|
|
631
|
+
if (webSocket instanceof AbstractWebSocket) return webSocket;
|
|
632
|
+
const sessionId = this._platform.getSessionId(webSocket);
|
|
633
|
+
if (typeof sessionId === "string") {
|
|
634
|
+
const session = this._sessions.get(sessionId) ?? null;
|
|
635
|
+
if (session) return session;
|
|
636
|
+
}
|
|
637
|
+
return Array.from(this._sessions.values()).find((pluvWs) => pluvWs.webSocket === webSocket) ?? null;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* TODO
|
|
641
|
+
* @description Revisit any cast types
|
|
642
|
+
* @date July 17, 2025
|
|
643
|
+
*/
|
|
644
|
+
async _getAuthorizedUser(token, options) {
|
|
645
|
+
const ioAuthorize = this._getIOAuthorize(options);
|
|
646
|
+
if (!ioAuthorize) return null;
|
|
647
|
+
if (!token) return null;
|
|
648
|
+
if (!ioAuthorize.secret) throw new Error("`authorize` was specified without a valid secret");
|
|
649
|
+
const payload = await authorize({
|
|
650
|
+
platform: this._platform,
|
|
651
|
+
secret: ioAuthorize.secret
|
|
652
|
+
}).decode(token);
|
|
653
|
+
if (!payload) {
|
|
654
|
+
this._logDebug(colors.blue("Could not decode token:"));
|
|
655
|
+
this._logDebug(token);
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
if (payload.room !== this.id) {
|
|
659
|
+
this._logDebug(colors.blue(`Token is not authorized for room ${this.id}:`));
|
|
660
|
+
this._logDebug(colors.blue("Received:"), payload.room);
|
|
661
|
+
this._logDebug(token);
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
return ioAuthorize.user.parse(payload.user) ?? null;
|
|
666
|
+
} catch {
|
|
667
|
+
this._logDebug(`${colors.blue("Token fails validation:")} ${token}`);
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async _getContext() {
|
|
672
|
+
return typeof this._context === "function" ? await Promise.resolve(this._context(this._roomContext)) : await Promise.resolve(this._context);
|
|
673
|
+
}
|
|
674
|
+
async _getInitialDoc() {
|
|
675
|
+
const doc = this._docFactory.getEmpty();
|
|
676
|
+
const [encodedState, context] = await Promise.all([this._platform.persistence.getStorageState(this.id), this._getContext()]);
|
|
677
|
+
if (!encodedState) {
|
|
678
|
+
const loadedState = await this._getInitialStorage({
|
|
679
|
+
context,
|
|
680
|
+
room: this.id
|
|
681
|
+
});
|
|
682
|
+
if (!!loadedState && !this._docFactory.isEmpty(loadedState)) doc.applyEncodedState({ update: loadedState });
|
|
683
|
+
}
|
|
684
|
+
if (typeof encodedState === "string") {
|
|
685
|
+
doc.applyEncodedState({ update: encodedState });
|
|
686
|
+
if (!doc.isEmpty()) this._storageInitializedViaSession = true;
|
|
687
|
+
}
|
|
688
|
+
return doc;
|
|
689
|
+
}
|
|
690
|
+
_getIOAuthorize(options) {
|
|
691
|
+
if (typeof this._authorize === "function") return this._authorize(options);
|
|
692
|
+
return this._authorize;
|
|
693
|
+
}
|
|
694
|
+
_getLatestPresence(userId) {
|
|
695
|
+
if (!this._authorize) return {
|
|
696
|
+
timer: null,
|
|
697
|
+
presence: null
|
|
698
|
+
};
|
|
699
|
+
const sessionIds = Array.from(this._userSessionss.get(userId)?.values() ?? []);
|
|
700
|
+
if (!sessionIds.length) return {
|
|
701
|
+
timer: null,
|
|
702
|
+
presence: null
|
|
703
|
+
};
|
|
704
|
+
return sessionIds.reduce((state, sessionId) => {
|
|
705
|
+
const pluvWs = this._sessions.get(sessionId) ?? null;
|
|
706
|
+
if (!pluvWs) return state;
|
|
707
|
+
const session = pluvWs.session;
|
|
708
|
+
const presence = session.presence;
|
|
709
|
+
const timer = session.timers.presence;
|
|
710
|
+
if (session.user?.id !== userId) return state;
|
|
711
|
+
if (typeof state.timer !== "number") return {
|
|
712
|
+
presence,
|
|
713
|
+
timer
|
|
714
|
+
};
|
|
715
|
+
if (typeof timer !== "number") return state;
|
|
716
|
+
return timer > state.timer ? {
|
|
717
|
+
presence,
|
|
718
|
+
timer
|
|
719
|
+
} : state;
|
|
720
|
+
}, {
|
|
721
|
+
presence: null,
|
|
722
|
+
timer: null
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
_getProcedure(message) {
|
|
726
|
+
return this._router._defs.events[message.type] ?? null;
|
|
727
|
+
}
|
|
728
|
+
_getProcedureInputs(message) {
|
|
729
|
+
const procedure = this._getProcedure(message);
|
|
730
|
+
if (!procedure) return message.data;
|
|
731
|
+
return procedure.config.input ? procedure.config.input.parse(message.data) : message.data;
|
|
732
|
+
}
|
|
733
|
+
_getSession(webSocket) {
|
|
734
|
+
const pluvWs = this._getAbstractWs(webSocket);
|
|
735
|
+
if (!pluvWs) throw new Error("Session could not be found");
|
|
736
|
+
return pluvWs.session;
|
|
737
|
+
}
|
|
738
|
+
_getSessions() {
|
|
739
|
+
return Array.from(this._sessions.values()).map((pluvWs) => pluvWs.session);
|
|
740
|
+
}
|
|
741
|
+
_initialize() {
|
|
742
|
+
const promise = (async () => {
|
|
743
|
+
this._logDebug(`${colors.blue(`Initializing room: ${this.id}`)}`);
|
|
744
|
+
if (!!this._uninitialize) {
|
|
745
|
+
await (await this._uninitialize).bind(this)();
|
|
746
|
+
this._uninitialize = null;
|
|
747
|
+
}
|
|
748
|
+
const pubSubId = await this._platform.pubSub.subscribe(this.id, async ({ options = {}, ...message }) => {
|
|
749
|
+
await promise;
|
|
750
|
+
const sender = {
|
|
751
|
+
sessionId: message.connectionId,
|
|
752
|
+
user: message.user
|
|
753
|
+
};
|
|
754
|
+
switch (options.type) {
|
|
755
|
+
case "self":
|
|
756
|
+
await this._sendSelfMessage(message, sender);
|
|
757
|
+
return;
|
|
758
|
+
case "sync":
|
|
759
|
+
await this._sendSyncMessage(message, sender);
|
|
760
|
+
return;
|
|
761
|
+
case "broadcast":
|
|
762
|
+
default: {
|
|
763
|
+
const sessionIds = options.sessionIds;
|
|
764
|
+
await this._sendBroadcastMessage(message, sender, sessionIds);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
const doc = await this._getInitialDoc();
|
|
769
|
+
this._wasDocEmptyOnInit = doc.isEmpty();
|
|
770
|
+
const uninitialize = async () => {
|
|
771
|
+
this._platform.pubSub.unsubscribe(pubSubId);
|
|
772
|
+
const [doc$1, context] = await Promise.all([this._doc, this._getContext()]);
|
|
773
|
+
const encodedState = doc$1.getEncodedState();
|
|
774
|
+
doc$1.destroy();
|
|
775
|
+
this._doc = Promise.resolve(this._docFactory.getEmpty());
|
|
776
|
+
await Promise.resolve(this._listeners.onRoomDestroyed({
|
|
777
|
+
..."_meta" in this._platform && !!this._platform._meta ? { _meta: this._platform._meta } : {},
|
|
778
|
+
context,
|
|
779
|
+
platform: this._platform,
|
|
780
|
+
room: this.id
|
|
781
|
+
}));
|
|
782
|
+
if (this._storageInitializedViaSession) {
|
|
783
|
+
await Promise.resolve(this._listeners.onStorageDestroyed({
|
|
784
|
+
..."_meta" in this._platform && !!this._platform._meta ? { _meta: this._platform._meta } : {},
|
|
785
|
+
context,
|
|
786
|
+
encodedState,
|
|
787
|
+
platform: this._platform,
|
|
788
|
+
room: this.id
|
|
789
|
+
}));
|
|
790
|
+
this._storageInitializedViaSession = false;
|
|
791
|
+
}
|
|
792
|
+
this._uninitialize = null;
|
|
793
|
+
};
|
|
794
|
+
return {
|
|
795
|
+
doc,
|
|
796
|
+
uninitialize
|
|
797
|
+
};
|
|
798
|
+
})();
|
|
799
|
+
this._doc = promise.then((result) => result.doc);
|
|
800
|
+
this._uninitialize = promise.then((result) => result.uninitialize);
|
|
801
|
+
return {
|
|
802
|
+
doc: this._doc,
|
|
803
|
+
uninitialize: this._uninitialize
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
_logDebug(...data) {
|
|
807
|
+
if (this._debug) console.log(...data);
|
|
808
|
+
}
|
|
809
|
+
_onClose(webSocket) {
|
|
810
|
+
return async () => {
|
|
811
|
+
if (!await this._initialized) return;
|
|
812
|
+
await this._closeWebSockets([webSocket]);
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
_onMessage(webSocket) {
|
|
816
|
+
return async (event) => {
|
|
817
|
+
if (!await this._initialized) return;
|
|
818
|
+
const pluvWs = this._getAbstractWs(webSocket);
|
|
819
|
+
if (!pluvWs) throw new Error("Could not get session");
|
|
820
|
+
const session = this._getSession(pluvWs);
|
|
821
|
+
const sessions = this._getSessions();
|
|
822
|
+
const setPresence = (params) => {
|
|
823
|
+
this._setPresence.bind(this)(params);
|
|
824
|
+
};
|
|
825
|
+
const [doc, context] = await Promise.all([this._doc, this._getContext()]);
|
|
826
|
+
const eventContext = {
|
|
827
|
+
context,
|
|
828
|
+
doc,
|
|
829
|
+
garbageCollect: async () => {
|
|
830
|
+
await this.garbageCollect();
|
|
831
|
+
},
|
|
832
|
+
platform: this._platform,
|
|
833
|
+
get presence() {
|
|
834
|
+
return session.presence;
|
|
835
|
+
},
|
|
836
|
+
set presence(presence) {
|
|
837
|
+
const sessionId$1 = this.session?.id;
|
|
838
|
+
if (!sessionId$1) return;
|
|
839
|
+
setPresence({
|
|
840
|
+
presence,
|
|
841
|
+
sessionId: sessionId$1,
|
|
842
|
+
timer: this.time
|
|
843
|
+
});
|
|
844
|
+
},
|
|
845
|
+
room: this.id,
|
|
846
|
+
session,
|
|
847
|
+
sessions,
|
|
848
|
+
time: (/* @__PURE__ */ new Date()).getTime()
|
|
849
|
+
};
|
|
850
|
+
if (pluvWs.state.quit) {
|
|
851
|
+
await this._closeWebSockets([pluvWs]).catch(() => null);
|
|
852
|
+
pluvWs.close(1011, "WebSocket broken.");
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const message = this._parseMessage(event);
|
|
856
|
+
if (!message) return;
|
|
857
|
+
const procedure = this._getProcedure(message);
|
|
858
|
+
this._listeners.onMessage({
|
|
859
|
+
..."_meta" in this._platform && !!this._platform._meta ? { _meta: this._platform._meta } : {},
|
|
860
|
+
context,
|
|
861
|
+
encodedState: doc.getEncodedState(),
|
|
862
|
+
message,
|
|
863
|
+
platform: this._platform,
|
|
864
|
+
room: this.id,
|
|
865
|
+
user: session.user,
|
|
866
|
+
webSocket: session.webSocket.webSocket
|
|
867
|
+
});
|
|
868
|
+
const sessionId = session.id;
|
|
869
|
+
const user = session.user;
|
|
870
|
+
if (!procedure) {
|
|
871
|
+
await this._broadcast({
|
|
872
|
+
message,
|
|
873
|
+
senderId: sessionId
|
|
874
|
+
});
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
let inputs;
|
|
878
|
+
try {
|
|
879
|
+
inputs = this._getProcedureInputs(message);
|
|
880
|
+
} catch (error) {
|
|
881
|
+
pluvWs.handleError({
|
|
882
|
+
error,
|
|
883
|
+
message: "Invalid input",
|
|
884
|
+
room: this.id,
|
|
885
|
+
session
|
|
886
|
+
});
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
await Promise.all([
|
|
890
|
+
procedure.config.broadcast?.(inputs, eventContext),
|
|
891
|
+
procedure.config.self?.(inputs, eventContext),
|
|
892
|
+
procedure.config.sync?.(inputs, eventContext)
|
|
893
|
+
]).then(async ([broadcast, self, sync]) => {
|
|
894
|
+
const handleBroadcast = async () => {
|
|
895
|
+
if (!broadcast) return;
|
|
896
|
+
const messages = Object.entries(broadcast).map(async ([type, data]) => {
|
|
897
|
+
await this._broadcast({
|
|
898
|
+
message: {
|
|
899
|
+
data,
|
|
900
|
+
type
|
|
901
|
+
},
|
|
902
|
+
senderId: sessionId
|
|
903
|
+
});
|
|
904
|
+
});
|
|
905
|
+
await Promise.all(messages);
|
|
906
|
+
};
|
|
907
|
+
const handleSelf = async () => {
|
|
908
|
+
if (!self) return;
|
|
909
|
+
const messages = Object.entries(self).map(async ([type, data]) => {
|
|
910
|
+
await this._sendSelfMessage({
|
|
911
|
+
data,
|
|
912
|
+
type
|
|
913
|
+
}, {
|
|
914
|
+
sessionId,
|
|
915
|
+
user
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
await Promise.all(messages);
|
|
919
|
+
};
|
|
920
|
+
const handleSync = async () => {
|
|
921
|
+
if (!sync) return;
|
|
922
|
+
await this._platform.pubSub.publish(this.id, {
|
|
923
|
+
connectionId: session.id,
|
|
924
|
+
options: { type: "sync" },
|
|
925
|
+
room: this.id,
|
|
926
|
+
user,
|
|
927
|
+
...message
|
|
928
|
+
});
|
|
929
|
+
const messages = Object.entries(sync).map(async ([type, data]) => {
|
|
930
|
+
await this._sendSelfMessage({
|
|
931
|
+
data,
|
|
932
|
+
type
|
|
933
|
+
}, {
|
|
934
|
+
sessionId,
|
|
935
|
+
user
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
await Promise.all(messages);
|
|
939
|
+
};
|
|
940
|
+
await Promise.all([
|
|
941
|
+
handleBroadcast(),
|
|
942
|
+
handleSelf(),
|
|
943
|
+
handleSync()
|
|
944
|
+
]);
|
|
945
|
+
});
|
|
946
|
+
await this._checkStorageInitializedViaSession();
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
_parseMessage(message) {
|
|
950
|
+
try {
|
|
951
|
+
const parsed = this._platform.parseData(message.data);
|
|
952
|
+
if (typeof parsed !== "object") return null;
|
|
953
|
+
if (typeof parsed.type !== "string") return null;
|
|
954
|
+
if (typeof parsed.data !== "object") return null;
|
|
955
|
+
return parsed;
|
|
956
|
+
} catch {
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
_setPresence(params) {
|
|
961
|
+
const { presence, sessionId, timer: _timer } = params;
|
|
962
|
+
const timer = _timer ?? (/* @__PURE__ */ new Date()).getTime();
|
|
963
|
+
const pluvWs = this._sessions.get(sessionId) ?? null;
|
|
964
|
+
if (!pluvWs) return;
|
|
965
|
+
const user = pluvWs.session.user;
|
|
966
|
+
(user ? new Set([...this._userSessionss.get(user.id) ?? [], sessionId]) : new Set([sessionId])).forEach((sId) => {
|
|
967
|
+
const session = this._sessions.get(sId)?.session;
|
|
968
|
+
if (!session) return;
|
|
969
|
+
const prevState = session.webSocket.state;
|
|
970
|
+
this._platform.setSerializedState(session.webSocket, {
|
|
971
|
+
...prevState,
|
|
972
|
+
presence,
|
|
973
|
+
timers: {
|
|
974
|
+
...prevState.timers,
|
|
975
|
+
presence: timer
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
_removeUserSession(userId, sessionId) {
|
|
981
|
+
const set = this._userSessionss.get(userId);
|
|
982
|
+
if (!set) return null;
|
|
983
|
+
set.delete(sessionId);
|
|
984
|
+
if (!!set.size) return set;
|
|
985
|
+
this._userSessionss.delete(userId);
|
|
986
|
+
return set;
|
|
987
|
+
}
|
|
988
|
+
async _checkStorageInitializedViaSession() {
|
|
989
|
+
if (this._storageInitializedViaSession) return;
|
|
990
|
+
const doc = await this._doc;
|
|
991
|
+
if (this._wasDocEmptyOnInit && !doc.isEmpty() && this._sessions.size > 0) this._storageInitializedViaSession = true;
|
|
992
|
+
}
|
|
993
|
+
async _sendMessage(pluvWs, message) {
|
|
994
|
+
if (!await this._initialized) return;
|
|
995
|
+
await Promise.resolve(pluvWs.sendMessage(message));
|
|
996
|
+
}
|
|
997
|
+
async _sendBroadcastMessage(message, sender, sessionIds) {
|
|
998
|
+
const { data, type } = message;
|
|
999
|
+
const connectionId = sender?.sessionId ?? null;
|
|
1000
|
+
const room = this.id;
|
|
1001
|
+
if (typeof connectionId !== "string") return;
|
|
1002
|
+
const webSockets = sessionIds?.reduce((dict, id) => {
|
|
1003
|
+
const pluvWs = this._sessions.get(id);
|
|
1004
|
+
return pluvWs ? dict.set(id, pluvWs) : dict;
|
|
1005
|
+
}, /* @__PURE__ */ new Map()) ?? this._sessions;
|
|
1006
|
+
const promises = Array.from(webSockets.values()).map(async (pluvWs) => {
|
|
1007
|
+
const user = pluvWs.session.user ?? null;
|
|
1008
|
+
this._sendMessage(pluvWs, {
|
|
1009
|
+
connectionId,
|
|
1010
|
+
data,
|
|
1011
|
+
room,
|
|
1012
|
+
type,
|
|
1013
|
+
user
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
await Promise.all(promises);
|
|
1017
|
+
}
|
|
1018
|
+
async _sendSelfMessage(message, sender) {
|
|
1019
|
+
const senderId = sender?.sessionId ?? null;
|
|
1020
|
+
if (typeof senderId !== "string") return;
|
|
1021
|
+
const pluvWs = this._sessions.get(senderId);
|
|
1022
|
+
if (!pluvWs) return;
|
|
1023
|
+
/**
|
|
1024
|
+
* @description Note that this will not fire for Cloudflare's hibernatable websockets
|
|
1025
|
+
* because the server will auto-respond with the $pong event without calling any
|
|
1026
|
+
* functions.
|
|
1027
|
+
* @date April 18, 2025
|
|
1028
|
+
*/
|
|
1029
|
+
if (message.type === "$pong") {
|
|
1030
|
+
if (Date.now() - this._lastGarbageCollectMs > GARBAGE_COLLECT_INTERVAL_MS) await this.garbageCollect();
|
|
1031
|
+
}
|
|
1032
|
+
const user = pluvWs.session.user ?? null;
|
|
1033
|
+
this._sendMessage(pluvWs, {
|
|
1034
|
+
connectionId: senderId,
|
|
1035
|
+
room: this.id,
|
|
1036
|
+
user,
|
|
1037
|
+
...message
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
async _sendSyncMessage(message, sender) {
|
|
1041
|
+
if (!sender) return;
|
|
1042
|
+
const connectionId = sender.sessionId;
|
|
1043
|
+
if (typeof connectionId !== "string") return;
|
|
1044
|
+
const [doc, context] = await Promise.all([this._doc, this._getContext()]);
|
|
1045
|
+
const resolverCtx = {
|
|
1046
|
+
context,
|
|
1047
|
+
doc,
|
|
1048
|
+
garbageCollect: async () => {
|
|
1049
|
+
await this.garbageCollect();
|
|
1050
|
+
},
|
|
1051
|
+
platform: this._platform,
|
|
1052
|
+
get presence() {
|
|
1053
|
+
return null;
|
|
1054
|
+
},
|
|
1055
|
+
set presence(presence) {},
|
|
1056
|
+
room: this.id,
|
|
1057
|
+
session: null,
|
|
1058
|
+
sessions: this._getSessions(),
|
|
1059
|
+
time: (/* @__PURE__ */ new Date()).getTime()
|
|
1060
|
+
};
|
|
1061
|
+
const resolver = this._getProcedure(message)?.config.sync;
|
|
1062
|
+
if (!resolver) return;
|
|
1063
|
+
let inputs;
|
|
1064
|
+
try {
|
|
1065
|
+
inputs = this._getProcedureInputs(message);
|
|
1066
|
+
} catch {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
Promise.resolve(resolver(inputs, resolverCtx)).then((output) => {
|
|
1070
|
+
if (!output) return;
|
|
1071
|
+
Object.keys(output).forEach((type) => {
|
|
1072
|
+
const data = output[type] ?? {};
|
|
1073
|
+
this._platform.pubSub.publish(this.id, {
|
|
1074
|
+
connectionId,
|
|
1075
|
+
data,
|
|
1076
|
+
options: { type: "self" },
|
|
1077
|
+
room: this.id,
|
|
1078
|
+
type,
|
|
1079
|
+
user: sender.user ?? null
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1153
1084
|
};
|
|
1154
1085
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1086
|
+
//#endregion
|
|
1087
|
+
//#region src/version.generated.ts
|
|
1088
|
+
const version = "4.0.1";
|
|
1157
1089
|
|
|
1158
|
-
|
|
1090
|
+
//#endregion
|
|
1091
|
+
//#region src/PluvServer.ts
|
|
1159
1092
|
var PluvServer = class {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
})
|
|
1296
|
-
}));
|
|
1297
|
-
return { $pong: {} };
|
|
1298
|
-
}),
|
|
1299
|
-
$updatePresence: this._procedure.broadcast((data, context) => {
|
|
1300
|
-
const presence = data == null ? void 0 : data.presence;
|
|
1301
|
-
const { session } = context;
|
|
1302
|
-
if (!session) return {};
|
|
1303
|
-
const cleanedPatch = pickBy(
|
|
1304
|
-
presence != null ? presence : {},
|
|
1305
|
-
(value) => typeof value !== "undefined"
|
|
1306
|
-
);
|
|
1307
|
-
const updated = Object.assign(/* @__PURE__ */ Object.create(null), session.presence, cleanedPatch);
|
|
1308
|
-
const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
|
|
1309
|
-
if (!!this._config.limits.presenceMaxSize && bytes > this._config.limits.presenceMaxSize) {
|
|
1310
|
-
throw new Error(oneLine`
|
|
1093
|
+
version = version;
|
|
1094
|
+
_config;
|
|
1095
|
+
_docFactory;
|
|
1096
|
+
get fetch() {
|
|
1097
|
+
return (...args) => {
|
|
1098
|
+
const platform = this._config.platform();
|
|
1099
|
+
platform.validateConfig(this._config);
|
|
1100
|
+
if (!platform._fetch) throw new Error(`\`${platform._name}\` does not support \`fetch\``);
|
|
1101
|
+
return platform._fetch(...args);
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* @ignore
|
|
1106
|
+
* @readonly
|
|
1107
|
+
* @deprecated Internal use only. Changes to this will never be marked as breaking.
|
|
1108
|
+
*/
|
|
1109
|
+
get _defs() {
|
|
1110
|
+
return {
|
|
1111
|
+
authorize: this._config.authorize,
|
|
1112
|
+
context: this._config.context,
|
|
1113
|
+
crdt: this._config.crdt,
|
|
1114
|
+
events: this._router._defs.events,
|
|
1115
|
+
platform: this._config.platform()
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
get _baseRouter() {
|
|
1119
|
+
const listeners = this._getListeners();
|
|
1120
|
+
return new PluvRouter({
|
|
1121
|
+
$getOthers: this._procedure.sync((data, { room, session, sessions }) => {
|
|
1122
|
+
const currentTime = Date.now();
|
|
1123
|
+
return { $othersReceived: { others: sessions.filter((wsSession) => {
|
|
1124
|
+
if (wsSession.id === session?.id) return false;
|
|
1125
|
+
if (wsSession.quit) return false;
|
|
1126
|
+
if (currentTime - wsSession.timers.ping > PING_TIMEOUT_MS) return false;
|
|
1127
|
+
return true;
|
|
1128
|
+
}).reduce((acc, { id, presence, timers, user }) => ({
|
|
1129
|
+
...acc,
|
|
1130
|
+
[id]: {
|
|
1131
|
+
connectionId: id,
|
|
1132
|
+
presence,
|
|
1133
|
+
room,
|
|
1134
|
+
timers: { presence: timers.presence },
|
|
1135
|
+
user
|
|
1136
|
+
}
|
|
1137
|
+
}), {}) } };
|
|
1138
|
+
}),
|
|
1139
|
+
$initializeSession: this._procedure.broadcast((data, { session }) => {
|
|
1140
|
+
const presence = data?.presence;
|
|
1141
|
+
const timers = session.timers;
|
|
1142
|
+
if (!session) return {};
|
|
1143
|
+
session.presence = presence;
|
|
1144
|
+
return { $userJoined: {
|
|
1145
|
+
connectionId: session.id,
|
|
1146
|
+
user: session.user,
|
|
1147
|
+
presence,
|
|
1148
|
+
timers: { presence: timers.presence }
|
|
1149
|
+
} };
|
|
1150
|
+
}).self(async (data, { context, doc, platform, room, session }) => {
|
|
1151
|
+
const oldState = await platform.persistence.getStorageState(room);
|
|
1152
|
+
/**
|
|
1153
|
+
* !HACK
|
|
1154
|
+
* @description This is the frontend's initialStorage. We only want to
|
|
1155
|
+
* apply this if the server's storage is empty (i.e. no initial storage
|
|
1156
|
+
* has been applied)
|
|
1157
|
+
*/
|
|
1158
|
+
const update = data?.update;
|
|
1159
|
+
if (!oldState) {
|
|
1160
|
+
const loadedState = await this._getInitialStorage({
|
|
1161
|
+
context,
|
|
1162
|
+
room
|
|
1163
|
+
});
|
|
1164
|
+
if (!!loadedState && !this._docFactory.isEmpty(loadedState)) doc.applyEncodedState({ update: loadedState });
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* @description Storage was already initialized. Don't overwrite the current
|
|
1168
|
+
* storage state with the incoming initial storage. Return what the current state
|
|
1169
|
+
* is without changes.
|
|
1170
|
+
* @date May 7, 2025
|
|
1171
|
+
*/
|
|
1172
|
+
if (!doc.isEmpty()) return { $storageReceived: {
|
|
1173
|
+
changeKind: "unchanged",
|
|
1174
|
+
state: doc.getEncodedState()
|
|
1175
|
+
} };
|
|
1176
|
+
/**
|
|
1177
|
+
* @description Storage was never initialized, and there is an incoming
|
|
1178
|
+
* initialStorage from the client. Apply the initialStorage and persist the state
|
|
1179
|
+
* as an update.
|
|
1180
|
+
* @date May 7, 2025
|
|
1181
|
+
*/
|
|
1182
|
+
if (!!update) {
|
|
1183
|
+
const encodedState = doc.applyEncodedState({ update }).getEncodedState();
|
|
1184
|
+
const storageSize = new TextEncoder().encode(encodedState).length;
|
|
1185
|
+
if (!!this._config.limits.storageMaxSize && storageSize > this._config.limits.storageMaxSize) throw new Error("Storage has exceeded the size limit");
|
|
1186
|
+
await platform.persistence.setStorageState(room, encodedState).catch((error) => {
|
|
1187
|
+
this._logDebug(error);
|
|
1188
|
+
});
|
|
1189
|
+
listeners.onStorageUpdated({
|
|
1190
|
+
context,
|
|
1191
|
+
encodedState,
|
|
1192
|
+
platform,
|
|
1193
|
+
room,
|
|
1194
|
+
user: session?.user,
|
|
1195
|
+
webSocket: session?.webSocket.webSocket
|
|
1196
|
+
});
|
|
1197
|
+
return { $storageReceived: {
|
|
1198
|
+
changeKind: "initialized",
|
|
1199
|
+
state: encodedState
|
|
1200
|
+
} };
|
|
1201
|
+
}
|
|
1202
|
+
return { $storageReceived: {
|
|
1203
|
+
changeKind: "empty",
|
|
1204
|
+
state: doc.getEncodedState()
|
|
1205
|
+
} };
|
|
1206
|
+
}),
|
|
1207
|
+
$ping: this._procedure.self((data, { platform, session }) => {
|
|
1208
|
+
if (!session) return {};
|
|
1209
|
+
const currentTime = (/* @__PURE__ */ new Date()).getTime();
|
|
1210
|
+
const prevState = session.webSocket.state;
|
|
1211
|
+
platform.setSerializedState(session.webSocket, {
|
|
1212
|
+
...prevState,
|
|
1213
|
+
timers: {
|
|
1214
|
+
...prevState.timers,
|
|
1215
|
+
ping: currentTime
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
return { $pong: {} };
|
|
1219
|
+
}),
|
|
1220
|
+
$updatePresence: this._procedure.broadcast((data, context) => {
|
|
1221
|
+
const presence = data?.presence;
|
|
1222
|
+
const { session } = context;
|
|
1223
|
+
if (!session) return {};
|
|
1224
|
+
const cleanedPatch = pickBy(presence ?? {}, (value) => typeof value !== "undefined");
|
|
1225
|
+
const updated = Object.assign(Object.create(null), session.presence, cleanedPatch);
|
|
1226
|
+
const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
|
|
1227
|
+
if (!!this._config.limits.presenceMaxSize && bytes > this._config.limits.presenceMaxSize) throw new Error(oneLine`
|
|
1311
1228
|
Large presence. Presence must be at most
|
|
1312
1229
|
${this._config.limits.presenceMaxSize.toLocaleString()} bytes.
|
|
1313
1230
|
Current size: ${bytes.toLocaleString()} bytes
|
|
1314
1231
|
`);
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
if (origin === "$initialized" && Object.keys(doc.toJson()).length) return {};
|
|
1329
|
-
const updated = update === null ? doc : doc.applyEncodedState({ update });
|
|
1330
|
-
const encodedState = updated.getEncodedState();
|
|
1331
|
-
const storageSize = new TextEncoder().encode(encodedState).length;
|
|
1332
|
-
if (!!this._config.limits.storageMaxSize && storageSize > this._config.limits.storageMaxSize) {
|
|
1333
|
-
throw new Error(oneLine`
|
|
1232
|
+
context.presence = updated;
|
|
1233
|
+
return { $presenceUpdated: {
|
|
1234
|
+
presence: updated,
|
|
1235
|
+
timers: { presence: session.timers.presence }
|
|
1236
|
+
} };
|
|
1237
|
+
}),
|
|
1238
|
+
$updateStorage: this._procedure.broadcast((data, { context, doc, platform, room }) => {
|
|
1239
|
+
const origin = data?.origin;
|
|
1240
|
+
const update = data?.update ?? null;
|
|
1241
|
+
if (origin === "$initialized" && Object.keys(doc.toJson()).length) return {};
|
|
1242
|
+
const encodedState = (update === null ? doc : doc.applyEncodedState({ update })).getEncodedState();
|
|
1243
|
+
const storageSize = new TextEncoder().encode(encodedState).length;
|
|
1244
|
+
if (!!this._config.limits.storageMaxSize && storageSize > this._config.limits.storageMaxSize) throw new Error(oneLine`
|
|
1334
1245
|
Large Storage. Storage must be at most
|
|
1335
1246
|
${this._config.limits.storageMaxSize.toLocaleString()} bytes.
|
|
1336
1247
|
Current size: ${storageSize.toLocaleString()} bytes
|
|
1337
1248
|
`);
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1249
|
+
platform.persistence.setStorageState(room, encodedState).then(() => {
|
|
1250
|
+
listeners.onStorageUpdated({
|
|
1251
|
+
context,
|
|
1252
|
+
encodedState,
|
|
1253
|
+
platform,
|
|
1254
|
+
room
|
|
1255
|
+
});
|
|
1256
|
+
});
|
|
1257
|
+
return { $storageUpdated: { state: encodedState } };
|
|
1258
|
+
})
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
get _procedure() {
|
|
1262
|
+
return new PluvProcedure();
|
|
1263
|
+
}
|
|
1264
|
+
get _router() {
|
|
1265
|
+
return this._config.router ? PluvRouter.merge(this._baseRouter, this._config.router) : this._baseRouter;
|
|
1266
|
+
}
|
|
1267
|
+
constructor(options) {
|
|
1268
|
+
this._config = {
|
|
1269
|
+
crdt: noop,
|
|
1270
|
+
debug: false,
|
|
1271
|
+
router: new PluvRouter({}),
|
|
1272
|
+
...options
|
|
1273
|
+
};
|
|
1274
|
+
const { onRoomDestroyed, onRoomMessage, onStorageDestroyed, onStorageUpdated, onUserConnected, onUserDisconnected } = options;
|
|
1275
|
+
this._docFactory = this._config.crdt.doc(() => ({}));
|
|
1276
|
+
this._listeners = {
|
|
1277
|
+
onRoomDestroyed: (event) => onRoomDestroyed?.(event),
|
|
1278
|
+
onRoomMessage: (event) => onRoomMessage?.(event),
|
|
1279
|
+
onStorageDestroyed: (event) => onStorageDestroyed?.(event),
|
|
1280
|
+
onStorageUpdated: (event) => onStorageUpdated?.(event),
|
|
1281
|
+
onUserConnected: (event) => onUserConnected?.(event),
|
|
1282
|
+
onUserDisconnected: (event) => onUserDisconnected?.(event)
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
createRoom(room, ...options) {
|
|
1286
|
+
const { _meta, debug, ...platformRoomContext } = options[0] ?? {};
|
|
1287
|
+
const platform = this._config.platform();
|
|
1288
|
+
platform.validateConfig(this._config);
|
|
1289
|
+
if (platform._config.handleMode !== "io") throw new Error(`\`createRoom\` is unsupported for \`${platform._name}\``);
|
|
1290
|
+
if (!/^[a-z0-9](?:[a-z0-9-_]*[a-z0-9])?$/i.test(room)) throw new Error("Unsupported room name");
|
|
1291
|
+
const roomContext = platformRoomContext;
|
|
1292
|
+
const listeners = this._getListeners();
|
|
1293
|
+
const logDebug = this._logDebug.bind(this);
|
|
1294
|
+
const newRoom = new IORoom(room, {
|
|
1295
|
+
...!!_meta ? { _meta } : {},
|
|
1296
|
+
authorize: this._config.authorize ?? void 0,
|
|
1297
|
+
context: this._config.context,
|
|
1298
|
+
crdt: this._config.crdt,
|
|
1299
|
+
debug: debug ?? this._config.debug,
|
|
1300
|
+
getInitialStorage: this._getInitialStorage,
|
|
1301
|
+
async onRoomDestroyed(event) {
|
|
1302
|
+
logDebug(`${colors.blue("Deleting empty room:")} ${room}`);
|
|
1303
|
+
await Promise.resolve(listeners.onRoomDestroyed(event));
|
|
1304
|
+
logDebug(`${colors.blue("Deleted room:")} ${room}`);
|
|
1305
|
+
},
|
|
1306
|
+
async onStorageDestroyed(event) {
|
|
1307
|
+
logDebug(`${colors.blue("Destroying storage for room:")} ${room}`);
|
|
1308
|
+
await Promise.resolve(listeners.onStorageDestroyed(event));
|
|
1309
|
+
await event.platform.persistence.deleteStorageState(room);
|
|
1310
|
+
logDebug(`${colors.blue("Destroyed storage for room:")} ${room}`);
|
|
1311
|
+
},
|
|
1312
|
+
async onMessage(event) {
|
|
1313
|
+
await Promise.resolve(listeners.onRoomMessage(event));
|
|
1314
|
+
},
|
|
1315
|
+
async onUserConnected(event) {
|
|
1316
|
+
await Promise.resolve(listeners.onUserConnected(event));
|
|
1317
|
+
},
|
|
1318
|
+
async onUserDisconnected(event) {
|
|
1319
|
+
await Promise.resolve(listeners.onUserDisconnected(event));
|
|
1320
|
+
},
|
|
1321
|
+
platform,
|
|
1322
|
+
roomContext,
|
|
1323
|
+
router: this._router
|
|
1324
|
+
});
|
|
1325
|
+
this._logDebug(`${colors.blue("Created room:")} ${room}`);
|
|
1326
|
+
return newRoom;
|
|
1327
|
+
}
|
|
1328
|
+
async createToken(params) {
|
|
1329
|
+
return await this._config.io.createToken(params);
|
|
1330
|
+
}
|
|
1331
|
+
_getInitialStorage = (...args) => {
|
|
1332
|
+
return (typeof this._config.getInitialStorage !== "string" ? this._config.getInitialStorage ?? (() => null) : (() => null))(...args);
|
|
1333
|
+
};
|
|
1334
|
+
_getListeners() {
|
|
1335
|
+
return this._listeners;
|
|
1336
|
+
}
|
|
1337
|
+
_logDebug(...data) {
|
|
1338
|
+
if (this._config.debug) console.log(...data);
|
|
1339
|
+
}
|
|
1419
1340
|
};
|
|
1420
1341
|
|
|
1421
|
-
|
|
1342
|
+
//#endregion
|
|
1343
|
+
//#region src/PluvIO.ts
|
|
1422
1344
|
var PluvIO = class {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1345
|
+
version = version;
|
|
1346
|
+
_authorize = null;
|
|
1347
|
+
_context = {};
|
|
1348
|
+
_crdt;
|
|
1349
|
+
_debug;
|
|
1350
|
+
_limits;
|
|
1351
|
+
_platform;
|
|
1352
|
+
get procedure() {
|
|
1353
|
+
return new PluvProcedure();
|
|
1354
|
+
}
|
|
1355
|
+
constructor(options) {
|
|
1356
|
+
const { authorize: authorize$1, context, crdt = noop, debug = false, limits, platform } = options;
|
|
1357
|
+
this._crdt = crdt;
|
|
1358
|
+
this._debug = debug;
|
|
1359
|
+
this._limits = {
|
|
1360
|
+
presenceMaxSize: MAX_PRESENCE_SIZE_BYTES,
|
|
1361
|
+
storageMaxSize: MAX_STORAGE_SIZE_BYTES,
|
|
1362
|
+
userIdMaxLength: MAX_USER_ID_LENGTH,
|
|
1363
|
+
userMaxSize: MAX_USER_SIZE_BYTES,
|
|
1364
|
+
...limits
|
|
1365
|
+
};
|
|
1366
|
+
this._platform = platform;
|
|
1367
|
+
if (authorize$1) this._authorize = authorize$1;
|
|
1368
|
+
if (context) this._context = context;
|
|
1369
|
+
}
|
|
1370
|
+
async createToken(params) {
|
|
1371
|
+
if (!this._authorize) throw new Error("IO does not specify authorize during initialization.");
|
|
1372
|
+
const ioAuthorize = this._getIOAuthorize(params);
|
|
1373
|
+
const user = params.user;
|
|
1374
|
+
const parsed = !!ioAuthorize ? ioAuthorize.user.parse(user) : user;
|
|
1375
|
+
if (!!this._limits.userIdMaxLength && user.id.length > this._limits.userIdMaxLength) throw new Error(oneLine`
|
|
1454
1376
|
createToken was called with a long user id. User ID must be at
|
|
1455
1377
|
most 128 characters. Current length: ${user.id.length.toLocaleString()}
|
|
1456
1378
|
`);
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
if (!!this._limits.userMaxSize && bytes > this._limits.userMaxSize) {
|
|
1460
|
-
throw new Error(oneLine`
|
|
1379
|
+
const bytes = new TextEncoder().encode(JSON.stringify(parsed)).length;
|
|
1380
|
+
if (!!this._limits.userMaxSize && bytes > this._limits.userMaxSize) throw new Error(oneLine`
|
|
1461
1381
|
createToken called with large payload. User must be at most 512
|
|
1462
1382
|
bytes. Current size: ${bytes.toLocaleString()}
|
|
1463
1383
|
`);
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1384
|
+
const platform = this._platform();
|
|
1385
|
+
/**
|
|
1386
|
+
* !HACK
|
|
1387
|
+
* @description Allow the platform to overwrite this behavior as needed. This is introduced
|
|
1388
|
+
* to support platformPluv
|
|
1389
|
+
* @date December 15, 2024
|
|
1390
|
+
*/
|
|
1391
|
+
if (platform._createToken) return await platform._createToken({
|
|
1392
|
+
...params,
|
|
1393
|
+
authorize: ioAuthorize
|
|
1394
|
+
});
|
|
1395
|
+
const secret = ioAuthorize?.secret ?? null;
|
|
1396
|
+
if (!secret) throw new Error("`authorize` was specified without a valid secret");
|
|
1397
|
+
return await authorize({
|
|
1398
|
+
platform,
|
|
1399
|
+
secret
|
|
1400
|
+
}).encode(params);
|
|
1401
|
+
}
|
|
1402
|
+
mergeRouters(...routers) {
|
|
1403
|
+
return PluvRouter.merge(...routers);
|
|
1404
|
+
}
|
|
1405
|
+
router(events) {
|
|
1406
|
+
const invalidName = Object.keys(events).find((name) => name.includes("$"));
|
|
1407
|
+
if (typeof invalidName === "string") throw new Error(oneLine`
|
|
1483
1408
|
Invalid event name. Event names must not contain $: "${invalidName}"
|
|
1484
1409
|
`);
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
return this._authorize(options);
|
|
1504
|
-
}
|
|
1505
|
-
return this._authorize;
|
|
1506
|
-
}
|
|
1410
|
+
return new PluvRouter(events);
|
|
1411
|
+
}
|
|
1412
|
+
server(...config) {
|
|
1413
|
+
return new PluvServer({
|
|
1414
|
+
...config[0] ?? {},
|
|
1415
|
+
authorize: this._authorize ?? void 0,
|
|
1416
|
+
context: this._context,
|
|
1417
|
+
crdt: this._crdt,
|
|
1418
|
+
debug: this._debug,
|
|
1419
|
+
io: this,
|
|
1420
|
+
limits: this._limits,
|
|
1421
|
+
platform: this._platform
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
_getIOAuthorize(options) {
|
|
1425
|
+
if (typeof this._authorize === "function") return this._authorize(options);
|
|
1426
|
+
return this._authorize;
|
|
1427
|
+
}
|
|
1507
1428
|
};
|
|
1508
1429
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
};
|
|
1521
|
-
export {
|
|
1522
|
-
AbstractPersistence,
|
|
1523
|
-
AbstractPlatform,
|
|
1524
|
-
AbstractPubSub,
|
|
1525
|
-
AbstractWebSocket,
|
|
1526
|
-
PluvIO,
|
|
1527
|
-
PluvProcedure,
|
|
1528
|
-
PluvRouter,
|
|
1529
|
-
PluvServer,
|
|
1530
|
-
authorize,
|
|
1531
|
-
createIO
|
|
1430
|
+
//#endregion
|
|
1431
|
+
//#region src/createIO.ts
|
|
1432
|
+
const createIO = (params) => {
|
|
1433
|
+
const { authorize: authorize$1, context, crdt, debug, limits, platform } = params;
|
|
1434
|
+
return new PluvIO({
|
|
1435
|
+
authorize: authorize$1,
|
|
1436
|
+
context,
|
|
1437
|
+
crdt,
|
|
1438
|
+
debug,
|
|
1439
|
+
limits,
|
|
1440
|
+
platform
|
|
1441
|
+
});
|
|
1532
1442
|
};
|
|
1443
|
+
|
|
1444
|
+
//#endregion
|
|
1445
|
+
export { AbstractPersistence, AbstractPlatform, AbstractPubSub, AbstractWebSocket, PluvIO, PluvProcedure, PluvRouter, PluvServer, authorize, createIO };
|
|
1446
|
+
//# sourceMappingURL=index.mjs.map
|