@pluv/client 3.2.2 → 4.0.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/dist/index.mjs CHANGED
@@ -1,1917 +1,1790 @@
1
- var __defProp = Object.defineProperty;
2
- var __defProps = Object.defineProperties;
3
- var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
- var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
- var __typeError = (msg) => {
8
- throw TypeError(msg);
9
- };
10
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
11
- var __spreadValues = (a, b) => {
12
- for (var prop in b || (b = {}))
13
- if (__hasOwnProp.call(b, prop))
14
- __defNormalProp(a, prop, b[prop]);
15
- if (__getOwnPropSymbols)
16
- for (var prop of __getOwnPropSymbols(b)) {
17
- if (__propIsEnum.call(b, prop))
18
- __defNormalProp(a, prop, b[prop]);
19
- }
20
- return a;
21
- };
22
- var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
23
- var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
24
- var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
25
- var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
26
- var __async = (__this, __arguments, generator) => {
27
- return new Promise((resolve, reject) => {
28
- var fulfilled = (value) => {
29
- try {
30
- step(generator.next(value));
31
- } catch (e) {
32
- reject(e);
33
- }
34
- };
35
- var rejected = (value) => {
36
- try {
37
- step(generator.throw(value));
38
- } catch (e) {
39
- reject(e);
40
- }
41
- };
42
- var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
43
- step((generator = generator.apply(__this, __arguments)).next());
44
- });
45
- };
46
-
47
- // src/index.ts
48
- import { ConnectionState as ConnectionState3 } from "@pluv/types";
1
+ import { ConnectionState, ConnectionState as ConnectionState$1, StorageState } from "@pluv/types";
2
+ import { noop } from "@pluv/crdt";
3
+ import { makeSubject, subscribe } from "wonka";
49
4
 
50
- // src/AbstractStorageStore.ts
5
+ //#region src/AbstractStorageStore.ts
51
6
  var AbstractStorageStore = class {
52
- constructor(room) {
53
- this.room = room;
54
- }
7
+ constructor(room) {
8
+ this.room = room;
9
+ }
55
10
  };
56
11
 
57
- // src/constants.ts
58
- var MAX_PRESENCE_SIZE_BYTES = 512;
59
- var PLUV_PRESENCE_META_KEY = "__pluv";
12
+ //#endregion
13
+ //#region src/constants.ts
14
+ const MAX_PRESENCE_SIZE_BYTES = 512;
15
+ const PLUV_PRESENCE_META_KEY = "__pluv";
60
16
 
61
- // src/PluvProcedure.ts
62
- var PluvProcedure = class _PluvProcedure {
63
- constructor(config = {}) {
64
- this._broadcast = null;
65
- this._input = null;
66
- const { broadcast, input } = config;
67
- this._broadcast = broadcast != null ? broadcast : null;
68
- this._input = input != null ? input : null;
69
- }
70
- get config() {
71
- var _a, _b, _c;
72
- return {
73
- broadcast: (_b = (_a = this._broadcast) == null ? void 0 : _a.bind(this)) != null ? _b : null,
74
- input: (_c = this._input) != null ? _c : null,
75
- resolver: this._resolver()
76
- };
77
- }
78
- broadcast(resolver) {
79
- const { broadcast } = this.config;
80
- if (!!broadcast) throw new Error("Broadcast was already defined for this procedure");
81
- return new _PluvProcedure(__spreadProps(__spreadValues({}, this.config), {
82
- broadcast: resolver
83
- }));
84
- }
85
- input(input) {
86
- return new _PluvProcedure({ input });
87
- }
88
- _resolver() {
89
- return (data, context) => {
90
- var _a;
91
- return (_a = this._broadcast) == null ? void 0 : _a.call(this, data, context);
92
- };
93
- }
17
+ //#endregion
18
+ //#region src/PluvProcedure.ts
19
+ var PluvProcedure = class PluvProcedure {
20
+ get config() {
21
+ return {
22
+ broadcast: this._broadcast?.bind(this) ?? null,
23
+ input: this._input ?? null,
24
+ resolver: this._resolver()
25
+ };
26
+ }
27
+ constructor(config = {}) {
28
+ this._broadcast = null;
29
+ this._input = null;
30
+ const { broadcast, input } = config;
31
+ this._broadcast = broadcast ?? null;
32
+ this._input = input ?? null;
33
+ }
34
+ broadcast(resolver) {
35
+ const { broadcast } = this.config;
36
+ if (!!broadcast) throw new Error("Broadcast was already defined for this procedure");
37
+ return new PluvProcedure({
38
+ ...this.config,
39
+ broadcast: resolver
40
+ });
41
+ }
42
+ input(input) {
43
+ return new PluvProcedure({ input });
44
+ }
45
+ _resolver() {
46
+ return (data, context) => this._broadcast?.(data, context);
47
+ }
94
48
  };
95
49
 
96
- // src/PluvRoom.ts
97
- import { ConnectionState, StorageState } from "@pluv/types";
98
-
99
- // src/CrdtManager.ts
100
- import { noop } from "@pluv/crdt";
50
+ //#endregion
51
+ //#region src/CrdtManager.ts
101
52
  var CrdtManager = class {
102
- constructor(options) {
103
- this.initialized = false;
104
- const { initialStorage = noop.doc({}) } = options;
105
- this._docFactory = initialStorage;
106
- this.doc = this._docFactory.getEmpty();
107
- }
108
- /**
109
- * !HACK
110
- * @description If this is called before the doc is initialized. Omit the
111
- * update. This unfortunately means updates can be dropped entirely
112
- * @date June 13, 2023
113
- */
114
- applyUpdate(params) {
115
- const { origin, update } = params;
116
- if (!this.initialized) return this;
117
- const updates = typeof update === "string" ? [update] : update;
118
- this._applyDocUpdates(this.doc, updates, origin);
119
- return this;
120
- }
121
- destroy() {
122
- this.initialized = false;
123
- this.doc.destroy();
124
- this.doc = this._docFactory.getEmpty();
125
- }
126
- get(key) {
127
- return this.doc.get(key);
128
- }
129
- getInitialState() {
130
- return this._docFactory.getInitialized().getEncodedState();
131
- }
132
- initialize(params) {
133
- const { origin, update } = params;
134
- if (this.initialized) {
135
- console.warn("Attempted to initialize storage multiple times");
136
- return this;
137
- }
138
- if (!this.doc.isEmpty()) {
139
- console.warn("Attempted to initialize over a non-empty storage");
140
- return this;
141
- }
142
- const updates = typeof update === "string" ? [update] : update;
143
- if (!updates.length) return this;
144
- const initialized = this._docFactory.getInitialized();
145
- const reference = initialized.get();
146
- this.doc = this._applyDocUpdates(this.doc, updates, origin).rebuildStorage(reference);
147
- this.initialized = true;
148
- initialized.destroy();
149
- return this;
150
- }
151
- resolveEncodedState(updates) {
152
- return this._docFactory.resolveEncodedState(updates);
153
- }
154
- _applyDocUpdates(doc, updates, origin) {
155
- doc.transact(() => {
156
- doc.batchApplyEncodedState({ origin, updates });
157
- }, origin);
158
- return doc;
159
- }
53
+ constructor(options) {
54
+ this.initialized = false;
55
+ const { initialStorage = noop.doc({}) } = options;
56
+ this._docFactory = initialStorage;
57
+ this.doc = this._docFactory.getEmpty();
58
+ }
59
+ /**
60
+ * !HACK
61
+ * @description If this is called before the doc is initialized. Omit the
62
+ * update. This unfortunately means updates can be dropped entirely
63
+ * @date June 13, 2023
64
+ */
65
+ applyUpdate(params) {
66
+ const { origin, update } = params;
67
+ if (!this.initialized) return this;
68
+ const updates = typeof update === "string" ? [update] : update;
69
+ this._applyDocUpdates(this.doc, updates, origin);
70
+ return this;
71
+ }
72
+ destroy() {
73
+ this.initialized = false;
74
+ this.doc.destroy();
75
+ this.doc = this._docFactory.getEmpty();
76
+ }
77
+ get(key) {
78
+ return this.doc.get(key);
79
+ }
80
+ getInitialState() {
81
+ return this._docFactory.getInitialized().getEncodedState();
82
+ }
83
+ initialize(params) {
84
+ const { origin, update } = params;
85
+ /**
86
+ * !HACK
87
+ * @description If the doc was already initialized, we don't want to re-initialize
88
+ * the doc and overwrite what's already there. Skip the initialization and log a
89
+ * warning.
90
+ * @date May 7, 2025
91
+ */
92
+ if (this.initialized) {
93
+ console.warn("Attempted to initialize storage multiple times");
94
+ return this;
95
+ }
96
+ /**
97
+ * !HACK
98
+ * @description If the doc is already populated, somehow we're attempting to overwrite
99
+ * a doc that was presumably initialized but not identified as initialized. Skip the
100
+ * initialization and log a warning.
101
+ * @date May 7, 2025
102
+ */
103
+ if (!this.doc.isEmpty()) {
104
+ console.warn("Attempted to initialize over a non-empty storage");
105
+ return this;
106
+ }
107
+ const updates = typeof update === "string" ? [update] : update;
108
+ if (!updates.length) return this;
109
+ const initialized = this._docFactory.getInitialized();
110
+ const reference = initialized.get();
111
+ this.doc = this._applyDocUpdates(this.doc, updates, origin).rebuildStorage(reference);
112
+ this.initialized = true;
113
+ initialized.destroy();
114
+ return this;
115
+ }
116
+ resolveEncodedState(updates) {
117
+ return this._docFactory.resolveEncodedState(updates);
118
+ }
119
+ _applyDocUpdates(doc, updates, origin) {
120
+ doc.transact(() => {
121
+ doc.batchApplyEncodedState({
122
+ origin,
123
+ updates
124
+ });
125
+ }, origin);
126
+ return doc;
127
+ }
160
128
  };
161
129
 
162
- // src/CrdtNotifier.ts
163
- import { makeSubject, subscribe } from "wonka";
130
+ //#endregion
131
+ //#region src/CrdtNotifier.ts
164
132
  var CrdtNotifier = class {
165
- constructor() {
166
- this.rootSubject = makeSubject();
167
- this.subjects = /* @__PURE__ */ new Map();
168
- }
169
- subject(key) {
170
- const subject = this.subjects.get(key);
171
- if (subject) {
172
- return subject;
173
- }
174
- const newSubject = makeSubject();
175
- this.subjects.set(key, newSubject);
176
- return newSubject;
177
- }
178
- subcribeRoot(callback) {
179
- return subscribe(callback)(this.rootSubject.source).unsubscribe;
180
- }
181
- subscribe(key, callback) {
182
- const subject = this.subject(key);
183
- return subscribe(callback)(subject.source).unsubscribe;
184
- }
133
+ constructor() {
134
+ this.rootSubject = makeSubject();
135
+ this.subjects = /* @__PURE__ */ new Map();
136
+ }
137
+ subject(key) {
138
+ const subject = this.subjects.get(key);
139
+ if (subject) return subject;
140
+ const newSubject = makeSubject();
141
+ this.subjects.set(key, newSubject);
142
+ return newSubject;
143
+ }
144
+ subcribeRoot(callback) {
145
+ return subscribe(callback)(this.rootSubject.source).unsubscribe;
146
+ }
147
+ subscribe(key, callback) {
148
+ const subject = this.subject(key);
149
+ return subscribe(callback)(subject.source).unsubscribe;
150
+ }
185
151
  };
186
152
 
187
- // src/EventNotifier.ts
188
- import { makeSubject as makeSubject2, subscribe as subscribe2 } from "wonka";
153
+ //#endregion
154
+ //#region src/EventNotifier.ts
189
155
  var EventNotifier = class {
190
- constructor() {
191
- this.subjects = /* @__PURE__ */ new Map();
192
- }
193
- subject(name) {
194
- const subject = this.subjects.get(name);
195
- if (subject) return subject;
196
- const newSubject = makeSubject2();
197
- this.subjects.set(name, newSubject);
198
- return newSubject;
199
- }
200
- subscribe(name, callback) {
201
- const subject = this.subject(name);
202
- return subscribe2(callback)(subject.source).unsubscribe;
203
- }
156
+ constructor() {
157
+ this.subjects = /* @__PURE__ */ new Map();
158
+ }
159
+ subject(name) {
160
+ const subject = this.subjects.get(name);
161
+ if (subject) return subject;
162
+ const newSubject = makeSubject();
163
+ this.subjects.set(name, newSubject);
164
+ return newSubject;
165
+ }
166
+ subscribe(name, callback) {
167
+ const subject = this.subject(name);
168
+ return subscribe(callback)(subject.source).unsubscribe;
169
+ }
204
170
  };
205
171
 
206
- // src/ListenerManager.ts
207
- import { makeSubject as makeSubject3, subscribe as subscribe3 } from "wonka";
172
+ //#endregion
173
+ //#region src/ListenerManager.ts
208
174
  var ListenerManager = class {
209
- constructor() {
210
- this.subjects = {
211
- close: makeSubject3(),
212
- error: makeSubject3(),
213
- message: makeSubject3(),
214
- open: makeSubject3()
215
- };
216
- }
217
- subscribe(kind, callback) {
218
- var _a;
219
- const source = (_a = this.subjects[kind]) == null ? void 0 : _a.source;
220
- if (!source) return () => void 0;
221
- return subscribe3(callback)(source).unsubscribe;
222
- }
175
+ constructor() {
176
+ this.subjects = {
177
+ close: makeSubject(),
178
+ error: makeSubject(),
179
+ message: makeSubject(),
180
+ open: makeSubject()
181
+ };
182
+ }
183
+ subscribe(kind, callback) {
184
+ const source = this.subjects[kind]?.source;
185
+ if (!source) return () => void 0;
186
+ return subscribe(callback)(source).unsubscribe;
187
+ }
223
188
  };
224
189
 
225
- // src/PluvRouter.ts
226
- var PluvRouter = class _PluvRouter {
227
- constructor(events) {
228
- this._defs = { events: {} };
229
- const invalidName = Object.keys(events).find((name) => !this._isValidEventName(name));
230
- if (typeof invalidName === "string") {
231
- throw new Error(
232
- `Invalid event name. Event names must be formatted as valid JavaScript variable names: "${invalidName}"`
233
- );
234
- }
235
- this._defs = { events };
236
- }
237
- static merge(...routers) {
238
- const events = Object.assign(
239
- /* @__PURE__ */ Object.create(null),
240
- ...routers.map((router) => router._defs.events)
241
- );
242
- if (Object.keys(events).some((key) => key.startsWith("$"))) {
243
- throw new Error('Procedures may not start with "$"');
244
- }
245
- return new _PluvRouter(events);
246
- }
247
- _isValidEventName(name) {
248
- return /^[a-z_$][a-z0-9_$]*$/gi.test(name);
249
- }
190
+ //#endregion
191
+ //#region src/PluvRouter.ts
192
+ var PluvRouter = class PluvRouter {
193
+ constructor(events) {
194
+ this._defs = { events: {} };
195
+ const invalidName = Object.keys(events).find((name) => !this._isValidEventName(name));
196
+ if (typeof invalidName === "string") throw new Error(`Invalid event name. Event names must be formatted as valid JavaScript variable names: "${invalidName}"`);
197
+ this._defs = { events };
198
+ }
199
+ static merge(...routers) {
200
+ const events = Object.assign(Object.create(null), ...routers.map((router) => router._defs.events));
201
+ if (Object.keys(events).some((key) => key.startsWith("$"))) throw new Error("Procedures may not start with \"$\"");
202
+ return new PluvRouter(events);
203
+ }
204
+ _isValidEventName(name) {
205
+ return /^[a-z_$][a-z0-9_$]*$/gi.test(name);
206
+ }
250
207
  };
251
208
 
252
- // src/StateNotifier.ts
253
- import { makeSubject as makeSubject4, subscribe as subscribe4 } from "wonka";
209
+ //#endregion
210
+ //#region src/StateNotifier.ts
254
211
  var StateNotifier = class {
255
- constructor() {
256
- this.subjects = {
257
- connection: makeSubject4(),
258
- "my-presence": makeSubject4(),
259
- myself: makeSubject4(),
260
- others: makeSubject4(),
261
- "storage-loaded": makeSubject4()
262
- };
263
- }
264
- subscribe(name, callback) {
265
- var _a;
266
- const source = (_a = this.subjects[name]) == null ? void 0 : _a.source;
267
- if (!source) throw new Error(`Could not subscribe to source: ${name}`);
268
- return subscribe4(callback)(source).unsubscribe;
269
- }
212
+ constructor() {
213
+ this.subjects = {
214
+ connection: makeSubject(),
215
+ "my-presence": makeSubject(),
216
+ myself: makeSubject(),
217
+ others: makeSubject(),
218
+ "storage-loaded": makeSubject()
219
+ };
220
+ }
221
+ subscribe(name, callback) {
222
+ const source = this.subjects[name]?.source;
223
+ if (!source) throw new Error(`Could not subscribe to source: ${name}`);
224
+ return subscribe(callback)(source).unsubscribe;
225
+ }
270
226
  };
271
227
 
272
- // src/StorageStore.ts
228
+ //#endregion
229
+ //#region src/StorageStore.ts
273
230
  var StorageStore = class extends AbstractStorageStore {
274
- addUpdate() {
275
- return Promise.resolve();
276
- }
277
- applyUpdates() {
278
- return Promise.resolve();
279
- }
280
- destroy() {
281
- return Promise.resolve();
282
- }
283
- flatten() {
284
- return Promise.resolve();
285
- }
286
- getShouldFlatten() {
287
- return Promise.resolve(false);
288
- }
289
- getUpdates() {
290
- return Promise.resolve([]);
291
- }
292
- initialize() {
293
- return Promise.resolve();
294
- }
231
+ addUpdate() {
232
+ return Promise.resolve();
233
+ }
234
+ applyUpdates() {
235
+ return Promise.resolve();
236
+ }
237
+ destroy() {
238
+ return Promise.resolve();
239
+ }
240
+ flatten() {
241
+ return Promise.resolve();
242
+ }
243
+ getShouldFlatten() {
244
+ return Promise.resolve(false);
245
+ }
246
+ getUpdates() {
247
+ return Promise.resolve([]);
248
+ }
249
+ initialize() {
250
+ return Promise.resolve();
251
+ }
295
252
  };
296
253
 
297
- // src/utils/debounce.ts
298
- var debounce = (fn, options) => {
299
- const { wait, immediate = false } = options != null ? options : {};
300
- let prevResult;
301
- let timeout = null;
302
- return (...args) => {
303
- if (typeof timeout === "number") clearTimeout(timeout);
304
- timeout = setTimeout(() => {
305
- timeout = null;
306
- if (!immediate) prevResult = fn(args);
307
- }, wait);
308
- if (immediate && !timeout) {
309
- fn(args);
310
- return;
311
- }
312
- return prevResult;
313
- };
254
+ //#endregion
255
+ //#region src/utils/debounce.ts
256
+ const debounce = (fn, options) => {
257
+ const { wait, immediate = false } = options ?? {};
258
+ let prevResult;
259
+ let timeout = null;
260
+ return (...args) => {
261
+ if (typeof timeout === "number") clearTimeout(timeout);
262
+ timeout = setTimeout(() => {
263
+ timeout = null;
264
+ if (!immediate) prevResult = fn(args);
265
+ }, wait);
266
+ if (immediate && !timeout) {
267
+ fn(args);
268
+ return;
269
+ }
270
+ return prevResult;
271
+ };
314
272
  };
315
273
 
316
- // src/utils/pickBy.ts
317
- var pickBy = (obj, by) => {
318
- const result = {};
319
- const keys = Object.keys(obj);
320
- for (let i = 0; i < keys.length; ++i) {
321
- const key = keys[i];
322
- const value = obj[key];
323
- if (by(value)) result[key] = value;
324
- }
325
- return result;
274
+ //#endregion
275
+ //#region src/utils/pickBy.ts
276
+ const pickBy = (obj, by) => {
277
+ const result = {};
278
+ const keys = Object.keys(obj);
279
+ for (let i = 0; i < keys.length; ++i) {
280
+ const key = keys[i];
281
+ const value = obj[key];
282
+ if (by(value)) result[key] = value;
283
+ }
284
+ return result;
326
285
  };
327
286
 
328
- // src/UsersManager.ts
287
+ //#endregion
288
+ //#region src/UsersManager.ts
329
289
  var UsersManager = class {
330
- constructor(config) {
331
- this._idMap = {
332
- fromConnectionId: /* @__PURE__ */ new Map(),
333
- fromClientId: /* @__PURE__ */ new Map()
334
- };
335
- /**
336
- * @description This is only set when the user is connected.
337
- */
338
- this._myself = null;
339
- this._others = /* @__PURE__ */ new Map();
340
- this._presence = null;
341
- const { initialPresence, limits, presence = null } = config;
342
- this.initialPresence = initialPresence;
343
- this._limits = limits;
344
- this._myPresence = initialPresence;
345
- this._presence = presence;
346
- }
347
- get myPresence() {
348
- return this._myPresence;
349
- }
350
- get myself() {
351
- return this._myself;
352
- }
353
- addConnection(userInfo) {
354
- var _a;
355
- const cleaned = pickBy((_a = userInfo.presence) != null ? _a : {}, (value) => typeof value !== "undefined");
356
- const presence = __spreadValues(__spreadValues({}, this.initialPresence), cleaned);
357
- const data = __spreadProps(__spreadValues({}, userInfo), { presence });
358
- const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
359
- const clientId = this.getClientId(data);
360
- const remaining = this._setConnectionId(userInfo.connectionId, clientId).size;
361
- if (myClientId === clientId) {
362
- const result2 = {
363
- clientId,
364
- data,
365
- isMyself: true,
366
- remaining
367
- };
368
- return result2;
369
- }
370
- const other = this._others.get(clientId);
371
- if (!other) this._others.set(clientId, data);
372
- const result = {
373
- clientId,
374
- data,
375
- isMyself: false,
376
- remaining
377
- };
378
- return result;
379
- }
380
- clearConnections() {
381
- this.removeMyself();
382
- this._others.clear();
383
- this._idMap.fromClientId.clear();
384
- this._idMap.fromConnectionId.clear();
385
- }
386
- deleteConnection(connectionId) {
387
- var _a, _b, _c;
388
- const clientId = this.getClientId(connectionId);
389
- this._deleteConnectionId(connectionId);
390
- if (!clientId) return null;
391
- const userInfo = (_a = this._others.get(clientId)) != null ? _a : null;
392
- if (!userInfo) return null;
393
- const remaining = (_c = (_b = this._getConnectionIds(clientId)) == null ? void 0 : _b.size) != null ? _c : 0;
394
- const result = {
395
- clientId,
396
- remaining,
397
- data: userInfo
398
- };
399
- if (!remaining) this._others.delete(clientId);
400
- return result;
401
- }
402
- getClientId(input) {
403
- var _a, _b, _c, _d;
404
- if (typeof input !== "string") {
405
- const userInfo = input;
406
- const connectionId2 = userInfo.connectionId;
407
- const userId = (_b = (_a = userInfo.user) == null ? void 0 : _a.id) != null ? _b : null;
408
- return userId != null ? userId : connectionId2;
409
- }
410
- const connectionId = input;
411
- if (((_c = this._myself) == null ? void 0 : _c.connectionId) === connectionId) {
412
- return this.getClientId(this._myself);
413
- }
414
- const clientId = (_d = this._idMap.fromConnectionId.get(connectionId)) != null ? _d : null;
415
- return clientId;
416
- }
417
- getOther(connectionId) {
418
- var _a;
419
- const clientId = this.getClientId(connectionId);
420
- if (!clientId) return null;
421
- return (_a = this._others.get(clientId)) != null ? _a : null;
422
- }
423
- getOthers() {
424
- return Array.from(this._others.values());
425
- }
426
- patchPresence(connectionId, patch) {
427
- var _a;
428
- const clientId = this.getClientId(connectionId);
429
- const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
430
- if (!clientId) return null;
431
- if (myClientId === clientId) return this.updateMyPresence(patch);
432
- const other = this._others.get(clientId);
433
- if (!other) return null;
434
- const cleanedPatch = pickBy(patch != null ? patch : {}, (value) => typeof value !== "undefined");
435
- const cleanedPresence = pickBy(
436
- (_a = other.presence) != null ? _a : {},
437
- (value) => typeof value !== "undefined"
438
- );
439
- const presence = __spreadValues(__spreadValues(__spreadValues({}, this.initialPresence), cleanedPresence), cleanedPatch);
440
- const validated = this._presence ? this._presence.parse(presence) : presence;
441
- if (!!presence[PLUV_PRESENCE_META_KEY]) {
442
- validated[PLUV_PRESENCE_META_KEY] = presence[PLUV_PRESENCE_META_KEY];
443
- }
444
- this._others.set(clientId, __spreadProps(__spreadValues({}, other), { presence: validated }));
445
- return validated;
446
- }
447
- removeMyself() {
448
- if (!this._myself) return;
449
- const connectionId = this._myself.connectionId;
450
- this._deleteConnectionId(connectionId);
451
- this._myself = null;
452
- }
453
- setMyself(userInfo) {
454
- var _a;
455
- const connectionId = userInfo.connectionId;
456
- const presence = (_a = userInfo.presence) != null ? _a : this.initialPresence;
457
- this._myself = __spreadProps(__spreadValues({}, userInfo), { presence });
458
- this._myPresence = presence;
459
- const clientId = this.getClientId(this._myself);
460
- this._setConnectionId(connectionId, clientId);
461
- }
462
- setPresence(connectionId, presence) {
463
- const clientId = this.getClientId(connectionId);
464
- const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
465
- if (!clientId) return;
466
- if (myClientId === clientId) {
467
- if (!this._myself) return;
468
- this._myself.presence = presence;
469
- this._myPresence = presence;
470
- return;
471
- }
472
- const other = this._others.get(clientId);
473
- if (!other) return;
474
- this._others.set(clientId, __spreadProps(__spreadValues({}, other), { presence }));
475
- }
476
- /**
477
- * @description This method need not care about being connected. This is because a user should
478
- * be able to view and update their own presence without being online
479
- * @date April 20, 2025
480
- */
481
- updateMyPresence(patch) {
482
- const cleanedPatch = pickBy(patch, (value) => typeof value !== "undefined");
483
- const cleanedPresence = pickBy(this._myPresence, (value) => typeof value !== "undefined");
484
- const updated = __spreadValues(__spreadValues(__spreadValues({}, this.initialPresence), cleanedPresence), cleanedPatch);
485
- const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
486
- if (!!this._limits.presenceMaxSize && bytes > this._limits.presenceMaxSize) {
487
- throw new Error(
488
- `Large presence. Presence must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`
489
- );
490
- }
491
- this._myPresence = updated;
492
- if (!this._myself) return updated;
493
- this._myself.presence = updated;
494
- return updated;
495
- }
496
- _deleteConnectionId(connectionId) {
497
- var _a;
498
- const clientId = this.getClientId(connectionId);
499
- this._idMap.fromConnectionId.delete(connectionId);
500
- if (!clientId) return;
501
- const set = (_a = this._idMap.fromClientId.get(clientId)) != null ? _a : null;
502
- if (!set) return;
503
- set.delete(connectionId);
504
- if (!!set.size) return;
505
- this._idMap.fromClientId.delete(clientId);
506
- }
507
- _getConnectionIds(clientId) {
508
- const set = this._idMap.fromClientId.get(clientId);
509
- return set != null ? set : null;
510
- }
511
- _setConnectionId(connectionId, clientId) {
512
- var _a;
513
- const set = (_a = this._idMap.fromClientId.get(clientId)) != null ? _a : /* @__PURE__ */ new Set();
514
- const updated = set.add(connectionId);
515
- this._idMap.fromConnectionId.set(connectionId, clientId);
516
- this._idMap.fromClientId.set(clientId, updated);
517
- return updated;
518
- }
290
+ constructor(config) {
291
+ this._idMap = {
292
+ fromConnectionId: /* @__PURE__ */ new Map(),
293
+ fromClientId: /* @__PURE__ */ new Map()
294
+ };
295
+ this._myself = null;
296
+ this._others = /* @__PURE__ */ new Map();
297
+ this._presence = null;
298
+ const { initialPresence, limits, presence = null } = config;
299
+ this.initialPresence = initialPresence;
300
+ this._limits = limits;
301
+ this._myPresence = initialPresence;
302
+ this._presence = presence;
303
+ }
304
+ get myPresence() {
305
+ return this._myPresence;
306
+ }
307
+ get myself() {
308
+ return this._myself;
309
+ }
310
+ addConnection(userInfo) {
311
+ /**
312
+ * @description Ensure that the presence is complete, so that it does not fail schema
313
+ * validation
314
+ * @date April 20, 2025
315
+ */
316
+ const cleaned = pickBy(userInfo.presence ?? {}, (value) => typeof value !== "undefined");
317
+ const presence = {
318
+ ...this.initialPresence,
319
+ ...cleaned
320
+ };
321
+ const data = {
322
+ ...userInfo,
323
+ presence
324
+ };
325
+ const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
326
+ const clientId = this.getClientId(data);
327
+ const remaining = this._setConnectionId(userInfo.connectionId, clientId).size;
328
+ if (myClientId === clientId) return {
329
+ clientId,
330
+ data,
331
+ isMyself: true,
332
+ remaining
333
+ };
334
+ /**
335
+ * !HACK
336
+ * @description Don't override the other user, so that we ensure the user's presence isn't
337
+ * overwritten by whatever incoming connection.
338
+ * @date April 16, 2025
339
+ */
340
+ if (!this._others.get(clientId)) this._others.set(clientId, data);
341
+ return {
342
+ clientId,
343
+ data,
344
+ isMyself: false,
345
+ remaining
346
+ };
347
+ }
348
+ clearConnections() {
349
+ this.removeMyself();
350
+ this._others.clear();
351
+ this._idMap.fromClientId.clear();
352
+ this._idMap.fromConnectionId.clear();
353
+ }
354
+ deleteConnection(connectionId) {
355
+ const clientId = this.getClientId(connectionId);
356
+ this._deleteConnectionId(connectionId);
357
+ if (!clientId) return null;
358
+ const userInfo = this._others.get(clientId) ?? null;
359
+ if (!userInfo) return null;
360
+ const remaining = this._getConnectionIds(clientId)?.size ?? 0;
361
+ const result = {
362
+ clientId,
363
+ remaining,
364
+ data: userInfo
365
+ };
366
+ /**
367
+ * @description A user may have multiple websocket connections open. If one connection is
368
+ * dropped, the user may still be connected via another websocket (e.g. on anther browser
369
+ * tab). In this case, we just want to remove the connection mapping for the connection
370
+ * that is dropped, but keep the remaining connections and the user.
371
+ * @date April 16, 2025
372
+ */
373
+ if (!remaining) this._others.delete(clientId);
374
+ return result;
375
+ }
376
+ getClientId(input) {
377
+ if (typeof input !== "string") {
378
+ const userInfo = input;
379
+ const connectionId$1 = userInfo.connectionId;
380
+ return userInfo.user?.id ?? null ?? connectionId$1;
381
+ }
382
+ const connectionId = input;
383
+ if (this._myself?.connectionId === connectionId) return this.getClientId(this._myself);
384
+ return this._idMap.fromConnectionId.get(connectionId) ?? null;
385
+ }
386
+ getOther(connectionId) {
387
+ const clientId = this.getClientId(connectionId);
388
+ if (!clientId) return null;
389
+ return this._others.get(clientId) ?? null;
390
+ }
391
+ getOthers() {
392
+ return Array.from(this._others.values());
393
+ }
394
+ patchPresence(connectionId, patch) {
395
+ const clientId = this.getClientId(connectionId);
396
+ const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
397
+ if (!clientId) return null;
398
+ if (myClientId === clientId) return this.updateMyPresence(patch);
399
+ const other = this._others.get(clientId);
400
+ if (!other) return null;
401
+ const cleanedPatch = pickBy(patch ?? {}, (value) => typeof value !== "undefined");
402
+ const cleanedPresence = pickBy(other.presence ?? {}, (value) => typeof value !== "undefined");
403
+ const presence = {
404
+ ...this.initialPresence,
405
+ ...cleanedPresence,
406
+ ...cleanedPatch
407
+ };
408
+ const validated = this._presence ? this._presence.parse(presence) : presence;
409
+ /**
410
+ * !HACK
411
+ * @description We're patching internal metadata back into the presence field so it doesn't
412
+ * get stripped from validation
413
+ * @date May 13, 2025
414
+ */
415
+ if (!!presence[PLUV_PRESENCE_META_KEY]) validated[PLUV_PRESENCE_META_KEY] = presence[PLUV_PRESENCE_META_KEY];
416
+ this._others.set(clientId, {
417
+ ...other,
418
+ presence: validated
419
+ });
420
+ return validated;
421
+ }
422
+ removeMyself() {
423
+ if (!this._myself) return;
424
+ const connectionId = this._myself.connectionId;
425
+ this._deleteConnectionId(connectionId);
426
+ this._myself = null;
427
+ }
428
+ setMyself(userInfo) {
429
+ const connectionId = userInfo.connectionId;
430
+ const presence = userInfo.presence ?? this.initialPresence;
431
+ this._myself = {
432
+ ...userInfo,
433
+ presence
434
+ };
435
+ this._myPresence = presence;
436
+ const clientId = this.getClientId(this._myself);
437
+ this._setConnectionId(connectionId, clientId);
438
+ }
439
+ setPresence(connectionId, presence) {
440
+ const clientId = this.getClientId(connectionId);
441
+ const myClientId = !!this._myself ? this.getClientId(this._myself) : null;
442
+ if (!clientId) return;
443
+ if (myClientId === clientId) {
444
+ if (!this._myself) return;
445
+ this._myself.presence = presence;
446
+ this._myPresence = presence;
447
+ return;
448
+ }
449
+ const other = this._others.get(clientId);
450
+ if (!other) return;
451
+ this._others.set(clientId, {
452
+ ...other,
453
+ presence
454
+ });
455
+ }
456
+ /**
457
+ * @description This method need not care about being connected. This is because a user should
458
+ * be able to view and update their own presence without being online
459
+ * @date April 20, 2025
460
+ */
461
+ updateMyPresence(patch) {
462
+ const cleanedPatch = pickBy(patch, (value) => typeof value !== "undefined");
463
+ const cleanedPresence = pickBy(this._myPresence, (value) => typeof value !== "undefined");
464
+ const updated = {
465
+ ...this.initialPresence,
466
+ ...cleanedPresence,
467
+ ...cleanedPatch
468
+ };
469
+ const bytes = new TextEncoder().encode(JSON.stringify(updated)).length;
470
+ if (!!this._limits.presenceMaxSize && bytes > this._limits.presenceMaxSize) throw new Error(`Large presence. Presence must be at most 512 bytes. Current size: ${bytes.toLocaleString()}`);
471
+ this._myPresence = updated;
472
+ if (!this._myself) return updated;
473
+ this._myself.presence = updated;
474
+ return updated;
475
+ }
476
+ _deleteConnectionId(connectionId) {
477
+ const clientId = this.getClientId(connectionId);
478
+ this._idMap.fromConnectionId.delete(connectionId);
479
+ if (!clientId) return;
480
+ const set = this._idMap.fromClientId.get(clientId) ?? null;
481
+ if (!set) return;
482
+ set.delete(connectionId);
483
+ if (!!set.size) return;
484
+ this._idMap.fromClientId.delete(clientId);
485
+ }
486
+ _getConnectionIds(clientId) {
487
+ return this._idMap.fromClientId.get(clientId) ?? null;
488
+ }
489
+ _setConnectionId(connectionId, clientId) {
490
+ const updated = (this._idMap.fromClientId.get(clientId) ?? /* @__PURE__ */ new Set()).add(connectionId);
491
+ this._idMap.fromConnectionId.set(connectionId, clientId);
492
+ this._idMap.fromClientId.set(clientId, updated);
493
+ return updated;
494
+ }
519
495
  };
520
496
 
521
- // src/UsersNotifier.ts
522
- import { makeSubject as makeSubject5, subscribe as subscribe5 } from "wonka";
497
+ //#endregion
498
+ //#region src/UsersNotifier.ts
523
499
  var UsersNotifier = class {
524
- constructor() {
525
- this.others = makeSubject5();
526
- this._otherSubjects = /* @__PURE__ */ new Map();
527
- }
528
- clear() {
529
- this._otherSubjects.forEach((subject) => {
530
- subject.next(null);
531
- });
532
- this._otherSubjects.clear();
533
- }
534
- delete(clientId) {
535
- this.other(clientId).next(null);
536
- this._otherSubjects.delete(clientId);
537
- }
538
- other(clientId) {
539
- const subject = this._otherSubjects.get(clientId);
540
- if (subject) return subject;
541
- const newSubject = makeSubject5();
542
- this._otherSubjects.set(clientId, newSubject);
543
- return newSubject;
544
- }
545
- subscribeOther(clientId, callback) {
546
- const source = this.other(clientId).source;
547
- const subscription = subscribe5(callback)(source);
548
- return subscription.unsubscribe;
549
- }
550
- subscribeOthers(callback) {
551
- const subscription = subscribe5(
552
- ({ others, event }) => callback(others, event)
553
- )(this.others.source);
554
- return subscription.unsubscribe;
555
- }
500
+ constructor() {
501
+ this.others = makeSubject();
502
+ this._otherSubjects = /* @__PURE__ */ new Map();
503
+ }
504
+ clear() {
505
+ this._otherSubjects.forEach((subject) => {
506
+ subject.next(null);
507
+ });
508
+ this._otherSubjects.clear();
509
+ }
510
+ delete(clientId) {
511
+ this.other(clientId).next(null);
512
+ this._otherSubjects.delete(clientId);
513
+ }
514
+ other(clientId) {
515
+ const subject = this._otherSubjects.get(clientId);
516
+ if (subject) return subject;
517
+ const newSubject = makeSubject();
518
+ this._otherSubjects.set(clientId, newSubject);
519
+ return newSubject;
520
+ }
521
+ subscribeOther(clientId, callback) {
522
+ const source = this.other(clientId).source;
523
+ return subscribe(callback)(source).unsubscribe;
524
+ }
525
+ subscribeOthers(callback) {
526
+ return subscribe(({ others, event }) => callback(others, event))(this.others.source).unsubscribe;
527
+ }
556
528
  };
557
529
 
558
- // src/PluvRoom.ts
559
- var ADD_TO_STORAGE_STATE_DEBOUNCE_MS = 1e3;
560
- var HEARTBEAT_INTERVAL_MS = 1e4;
561
- var PONG_TIMEOUT_MS = 2e3;
562
- var RECONNECT_TIMEOUT_MS = 5e3;
563
- var MIN_RECONNECT_TIMEOUT_MS = 1e3;
564
- var MAX_RECONNECT_TIMEOUT_MS = 6e4;
565
- var ORIGIN_INITIALIZED = "$initialized";
566
- var ORIGIN_STORAGE_UPDATED = "$storageUpdated";
567
- var DEFAULT_PLUV_CLIENT_ADDON = (input) => ({
568
- storage: new StorageStore(input.room.id)
569
- });
570
- var __storage;
530
+ //#endregion
531
+ //#region src/PluvRoom.ts
532
+ const ADD_TO_STORAGE_STATE_DEBOUNCE_MS = 1e3;
533
+ const HEARTBEAT_INTERVAL_MS = 1e4;
534
+ const PONG_TIMEOUT_MS = 2e3;
535
+ const RECONNECT_TIMEOUT_MS = 5e3;
536
+ const MIN_RECONNECT_TIMEOUT_MS = 1e3;
537
+ const MAX_RECONNECT_TIMEOUT_MS = 6e4;
538
+ const ORIGIN_INITIALIZED = "$initialized";
539
+ const ORIGIN_STORAGE_UPDATED = "$storageUpdated";
540
+ const DEFAULT_PLUV_CLIENT_ADDON = (input) => ({ storage: new StorageStore(input.room.id) });
571
541
  var PluvRoom = class {
572
- constructor(room, options) {
573
- this._crdtNotifier = new CrdtNotifier();
574
- this._eventNotifier = new EventNotifier();
575
- this._intervals = {
576
- heartbeat: null
577
- };
578
- this._listenerManager = new ListenerManager();
579
- this._publicKey = null;
580
- this._stateNotifier = new StateNotifier();
581
- this._subscriptions = {
582
- observeCrdt: null
583
- };
584
- this._timeouts = {
585
- pong: null,
586
- reconnect: null
587
- };
588
- this._usersNotifier = new UsersNotifier();
589
- this._lastMetadata = null;
590
- this._state = {
591
- authorization: {
592
- token: null,
593
- user: null
594
- },
595
- connection: {
596
- attempts: 0,
597
- id: null,
598
- state: ConnectionState.Untouched
599
- },
600
- storage: {
601
- state: StorageState.Unavailable
602
- },
603
- webSocket: null
604
- };
605
- this._windowListeners = null;
606
- this._wsListeners = null;
607
- this.broadcast = new Proxy(
608
- (event, data) => __async(this, null, function* () {
609
- if (!this._state.webSocket) return;
610
- if (this._state.connection.state !== ConnectionState.Open) return;
611
- const type = event.toString();
612
- const procedure = this._router._defs.events[type];
613
- if (!(procedure == null ? void 0 : procedure.config.broadcast)) {
614
- this._sendMessage({ data, type });
615
- return;
616
- }
617
- const myself = this._usersManager.myself;
618
- if (!myself) return;
619
- const parsed = procedure.config.input ? procedure.config.input.parse(data) : data;
620
- const context = {
621
- doc: this._crdtManager.doc,
622
- others: this._usersManager.getOthers(),
623
- room: this.id,
624
- user: myself
625
- };
626
- const output = yield procedure.config.broadcast(parsed, context);
627
- Object.entries(output).forEach(([_type, _data]) => {
628
- this._sendMessage({ data: _data, type: _type });
629
- });
630
- }),
631
- {
632
- get(fn, prop) {
633
- return (data) => __async(null, null, function* () {
634
- return yield fn(prop, data);
635
- });
636
- }
637
- }
638
- );
639
- this.canRedo = () => {
640
- return !!this._crdtManager.doc.canRedo();
641
- };
642
- this.canUndo = () => {
643
- return !!this._crdtManager.doc.canUndo();
644
- };
645
- this.getConnection = () => {
646
- return Object.freeze(JSON.parse(JSON.stringify(this._state.connection)));
647
- };
648
- this.getDoc = () => {
649
- return this._crdtManager.doc;
650
- };
651
- this.getMyPresence = () => {
652
- return this._usersManager.myPresence;
653
- };
654
- this.getMyself = () => {
655
- return this._usersManager.myself;
656
- };
657
- this.getOther = (connectionId) => {
658
- return this._usersManager.getOther(connectionId);
659
- };
660
- this.getOthers = () => {
661
- return this._usersManager.getOthers();
662
- };
663
- this.getStorage = (type) => {
664
- const sharedType = this._crdtManager.get(type);
665
- if (typeof sharedType === "undefined") return null;
666
- return sharedType;
667
- };
668
- this.getStorageLoaded = () => {
669
- return this._state.storage.state === StorageState.Offline || this._state.storage.state === StorageState.Synced;
670
- };
671
- this.redo = () => {
672
- this._crdtManager.doc.redo();
673
- };
674
- this.subscribe = new Proxy(
675
- (name, callback) => this._stateNotifier.subscribe(name, callback),
676
- {
677
- get: (fn, prop) => {
678
- if (prop === "connection") {
679
- return (callback) => {
680
- return fn("connection", callback);
681
- };
682
- }
683
- if (prop === "myPresence") {
684
- return (callback) => {
685
- return fn("my-presence", callback);
686
- };
687
- }
688
- if (prop === "myself") {
689
- return (callback) => {
690
- return fn("myself", callback);
691
- };
692
- }
693
- if (prop === "others") {
694
- return (callback) => {
695
- return this._usersNotifier.subscribeOthers(callback);
696
- };
697
- }
698
- if (prop === "storageLoaded") {
699
- return (callback) => {
700
- return fn("storage-loaded", callback);
701
- };
702
- }
703
- if (prop === "event") return this._event;
704
- if (prop === "other") return this._other;
705
- if (prop === "storage") return __privateGet(this, __storage);
706
- }
707
- }
708
- );
709
- this.transact = (fn, origin) => {
710
- const _origin = origin != null ? origin : this._state.connection.id;
711
- if (typeof _origin !== "string") return;
712
- this._crdtManager.doc.transact(() => {
713
- const storage = this._crdtManager.doc.get();
714
- fn(storage);
715
- }, _origin);
716
- };
717
- this.undo = () => {
718
- this._crdtManager.doc.undo();
719
- };
720
- this.updateMyPresence = (presence) => {
721
- var _a;
722
- const newPresence = typeof presence === "function" ? presence(this.getMyPresence()) : presence;
723
- this._usersManager.updateMyPresence(newPresence);
724
- const myPresence = this._usersManager.myPresence;
725
- const myself = (_a = this._usersManager.myself) != null ? _a : null;
726
- this._stateNotifier.subjects["my-presence"].next(myPresence);
727
- if (!!myself) this._stateNotifier.subjects.myself.next(myself);
728
- this.broadcast(
729
- "$updatePresence",
730
- { presence: newPresence }
731
- );
732
- };
733
- this._event = new Proxy(
734
- (event, callback) => this._eventNotifier.subscribe(event, callback),
735
- {
736
- get(fn, prop) {
737
- return (callback) => fn(prop, callback);
738
- }
739
- }
740
- );
741
- this._flattenStorageStore = debounce(
742
- () => __async(this, null, function* () {
743
- const shouldFlatten = yield this._storageStore.getShouldFlatten();
744
- if (!shouldFlatten) return;
745
- const updates = yield this._storageStore.getUpdates();
746
- this._crdtManager.applyUpdate({ update: updates, origin: this });
747
- const encodedState = this._crdtManager.doc.getEncodedState();
748
- yield this._storageStore.flatten(encodedState);
749
- }),
750
- { wait: ADD_TO_STORAGE_STATE_DEBOUNCE_MS }
751
- );
752
- this._getAddon = (addons) => {
753
- return addons.reduce(
754
- (acc, addon) => () => __spreadValues(__spreadValues({}, acc({ room: this })), addon({ room: this })),
755
- DEFAULT_PLUV_CLIENT_ADDON
756
- );
757
- };
758
- this._other = (connectionId, callback) => {
759
- const clientId = this._usersManager.getClientId(connectionId);
760
- if (!clientId) return () => void 0;
761
- return this._usersNotifier.subscribeOther(clientId, callback);
762
- };
763
- __privateAdd(this, __storage, new Proxy(
764
- (key, callback) => this._crdtNotifier.subscribe(key, callback),
765
- {
766
- get: (fn, prop) => {
767
- if (!!prop) {
768
- return (callback) => {
769
- return this._crdtNotifier.subcribeRoot(callback);
770
- };
771
- }
772
- return (callback) => {
773
- return fn(prop, callback);
774
- };
775
- }
776
- }
777
- ));
778
- const {
779
- addons = [],
780
- authEndpoint,
781
- debug = false,
782
- initialPresence,
783
- initialStorage,
784
- limits,
785
- metadata,
786
- onAuthorizationFail,
787
- presence,
788
- publicKey,
789
- reconnectTimeoutMs = RECONNECT_TIMEOUT_MS,
790
- router,
791
- wsEndpoint
792
- } = options;
793
- this.id = room;
794
- this.metadata = metadata;
795
- const addon = this._getAddon(addons);
796
- const { storage } = __spreadValues(__spreadValues({}, DEFAULT_PLUV_CLIENT_ADDON({ room: this })), addon({ room: this }));
797
- this._storageStore = storage;
798
- this._debug = debug;
799
- this._endpoints = { authEndpoint, wsEndpoint };
800
- this._limits = limits;
801
- this._reconnectTimeoutMs = reconnectTimeoutMs;
802
- if (!!publicKey) this._publicKey = publicKey;
803
- this._listeners = {
804
- onAuthorizationFail: (error) => {
805
- onAuthorizationFail == null ? void 0 : onAuthorizationFail(error);
806
- }
807
- };
808
- this._router = router != null ? router : new PluvRouter({});
809
- this._usersManager = new UsersManager({
810
- initialPresence,
811
- limits: this._limits,
812
- presence
813
- });
814
- this._crdtManager = new CrdtManager({ initialStorage });
815
- }
816
- get webSocket() {
817
- return this._state.webSocket;
818
- }
819
- addEventListener(kind, handler) {
820
- return this._listenerManager.subscribe(kind, handler);
821
- }
822
- connect(...args) {
823
- return __async(this, null, function* () {
824
- var _a;
825
- const params = (_a = args[0]) != null ? _a : {};
826
- const metadata = params.metadata;
827
- this._setMetadata(metadata);
828
- const canConnect = [
829
- ConnectionState.Closed,
830
- ConnectionState.Unavailable,
831
- ConnectionState.Untouched
832
- ].some((state) => this._state.connection.state === state);
833
- if (!canConnect) return;
834
- this._clearTimeout(this._timeouts.reconnect);
835
- this._updateState((oldState) => {
836
- oldState.connection.state = ConnectionState.Connecting;
837
- return oldState;
838
- });
839
- yield this._storageStore.initialize();
840
- const wsEndpoint = this._getWsEndpoint(this.id, params);
841
- const url = new URL(wsEndpoint);
842
- let authToken = null;
843
- let webSocket;
844
- try {
845
- const publicKey = this._getPublicKey(params);
846
- authToken = yield this._getAuthorization(this.id, params);
847
- if (authToken) url.searchParams.set("token", encodeURIComponent(authToken));
848
- if (publicKey) url.searchParams.set("public_key", encodeURIComponent(publicKey));
849
- webSocket = new WebSocket(url.toString());
850
- } catch (err) {
851
- this._updateState((oldState) => {
852
- oldState.connection.attempts += 1;
853
- return oldState;
854
- });
855
- yield this._storageStore.destroy();
856
- this._pollReconnect();
857
- yield Promise.resolve(this._onAuthorizationFail(err));
858
- return;
859
- }
860
- this._updateState((oldState) => {
861
- oldState.connection.attempts = 0;
862
- oldState.authorization.token = authToken;
863
- oldState.storage.state = StorageState.Loading;
864
- oldState.webSocket = webSocket;
865
- return oldState;
866
- });
867
- this._attachWindowListeners();
868
- this._attachWsListeners();
869
- });
870
- }
871
- disconnect() {
872
- return __async(this, null, function* () {
873
- if (!this._state.webSocket) return;
874
- const canDisconnect = [ConnectionState.Connecting, ConnectionState.Open].some(
875
- (state) => this._state.connection.state === state
876
- );
877
- if (!canDisconnect) return;
878
- this._closeWs();
879
- this._clearInterval(this._intervals.heartbeat);
880
- this._clearTimeout(this._timeouts.reconnect);
881
- this._updateState((oldState) => {
882
- oldState.authorization.token = null;
883
- oldState.connection.state = ConnectionState.Closed;
884
- oldState.storage.state = oldState.storage.state === StorageState.Synced ? StorageState.Offline : StorageState.Unavailable;
885
- oldState.webSocket = null;
886
- return oldState;
887
- });
888
- yield this._storageStore.destroy();
889
- this._crdtManager.destroy();
890
- this._stateNotifier.subjects["my-presence"].next(null);
891
- });
892
- }
893
- getStorageJson(type) {
894
- if (this._state.connection.id === null) return null;
895
- if (typeof type === "undefined") return this._crdtManager.doc.toJson();
896
- const sharedType = this._crdtManager.get(type);
897
- if (typeof sharedType === "undefined") return null;
898
- return this._crdtManager.doc.toJson(type);
899
- }
900
- _addToStorageStore(update) {
901
- return __async(this, null, function* () {
902
- yield this._storageStore.addUpdate(update);
903
- this._flattenStorageStore();
904
- });
905
- }
906
- _attachWindowListeners() {
907
- if (typeof window === "undefined") return;
908
- if (typeof document === "undefined") return;
909
- this._windowListeners = this._windowListeners || {
910
- onNavigatorOffline: this._onNavigatorOffline.bind(this),
911
- onNavigatorOnline: this._onNavigatorOnline.bind(this),
912
- onVisibilityChange: this._onVisibilityChange.bind(this),
913
- onWindowFocus: this._onWindowFocus.bind(this)
914
- };
915
- window.addEventListener("offline", this._windowListeners.onNavigatorOffline);
916
- window.addEventListener("online", this._windowListeners.onNavigatorOnline);
917
- document.addEventListener("visibilitychange", this._windowListeners.onVisibilityChange);
918
- window.addEventListener("focus", this._windowListeners.onWindowFocus);
919
- }
920
- _attachWsListeners() {
921
- if (!this._state.webSocket) return;
922
- this._wsListeners = this._wsListeners || {
923
- onClose: this._onClose.bind(this),
924
- onError: this._onError.bind(this),
925
- onMessage: this._onMessage.bind(this),
926
- onOpen: this._onOpen.bind(this)
927
- };
928
- this._state.webSocket.addEventListener("close", this._wsListeners.onClose);
929
- this._state.webSocket.addEventListener("error", this._wsListeners.onError);
930
- this._state.webSocket.addEventListener("message", this._wsListeners.onMessage);
931
- this._state.webSocket.addEventListener("open", this._wsListeners.onOpen);
932
- }
933
- _clearInterval(interval) {
934
- if (Number.isNaN(interval)) return;
935
- clearInterval(interval);
936
- }
937
- _clearTimeout(timeout) {
938
- if (Number.isNaN(timeout)) return;
939
- clearTimeout(timeout);
940
- }
941
- _closeWs() {
942
- var _a, _b, _c;
943
- (_b = (_a = this._subscriptions).observeCrdt) == null ? void 0 : _b.call(_a);
944
- this._clearInterval(this._intervals.heartbeat);
945
- this._clearTimeout(this._timeouts.pong);
946
- this._detachWindowListeners();
947
- this._usersManager.removeMyself();
948
- this._usersManager.clearConnections();
949
- this._usersNotifier.clear();
950
- this._stateNotifier.subjects.myself.next(null);
951
- this._stateNotifier.subjects.others.next([]);
952
- this._usersNotifier.others.next({ others: [], event: { kind: "clear" } });
953
- (_c = this._state.webSocket) == null ? void 0 : _c.close();
954
- this._detachWsListeners();
955
- }
956
- _detachWindowListeners() {
957
- if (!this._windowListeners) return;
958
- if (typeof window === "undefined") return;
959
- if (typeof document === "undefined") return;
960
- window.removeEventListener("offline", this._windowListeners.onNavigatorOffline);
961
- window.removeEventListener("online", this._windowListeners.onNavigatorOnline);
962
- document.removeEventListener("visibilitychange", this._windowListeners.onVisibilityChange);
963
- window.removeEventListener("focus", this._windowListeners.onWindowFocus);
964
- this._windowListeners = null;
965
- }
966
- _detachWsListeners() {
967
- if (!this._state.webSocket || !this._wsListeners) return;
968
- this._state.webSocket.removeEventListener("close", this._wsListeners.onClose);
969
- this._state.webSocket.removeEventListener("error", this._wsListeners.onError);
970
- this._state.webSocket.removeEventListener("message", this._wsListeners.onMessage);
971
- this._state.webSocket.removeEventListener("open", this._wsListeners.onOpen);
972
- this._wsListeners = null;
973
- }
974
- _emitSharedTypes() {
975
- const sharedTypes = this._crdtManager.doc.get();
976
- const storageRoot = Object.keys(sharedTypes).reduce(
977
- (acc, prop) => {
978
- const serialized = this._crdtManager.doc.toJson(prop);
979
- this._crdtNotifier.subject(prop).next(serialized);
980
- return __spreadProps(__spreadValues({}, acc), { [prop]: serialized });
981
- },
982
- {}
983
- );
984
- this._crdtNotifier.rootSubject.next(storageRoot);
985
- }
986
- _getAuthFetchOptions(room, params) {
987
- const metadata = params.metadata;
988
- if (typeof this._endpoints.authEndpoint === "undefined") return null;
989
- if (this._endpoints.authEndpoint === true) {
990
- return {
991
- url: `/api/pluv/authorize?room=${room}`,
992
- options: {}
993
- };
994
- }
995
- if (typeof this._endpoints.authEndpoint === "string") {
996
- return {
997
- url: this._endpoints.authEndpoint,
998
- options: {}
999
- };
1000
- }
1001
- const result = this._endpoints.authEndpoint({ metadata, room });
1002
- return typeof result === "string" ? { url: result, options: {} } : result;
1003
- }
1004
- _getAuthorization(room, params) {
1005
- return __async(this, null, function* () {
1006
- const fetchOptions = this._getAuthFetchOptions(room, params);
1007
- if (!fetchOptions) return null;
1008
- const { url, options } = fetchOptions;
1009
- const res = yield fetch(url, options);
1010
- if (!res.ok || res.status !== 200) throw new Error("Room is unauthorized");
1011
- try {
1012
- return yield res.text().then((text) => text.trim());
1013
- } catch (err) {
1014
- throw new Error(err instanceof Error ? err.message : "Room is unauthorized");
1015
- }
1016
- });
1017
- }
1018
- _getPublicKey(params) {
1019
- const metadata = params.metadata;
1020
- if (!this._publicKey) return null;
1021
- if (typeof this._publicKey === "string") return this._publicKey;
1022
- return this._publicKey({ metadata });
1023
- }
1024
- _getWsEndpoint(room, params) {
1025
- const metadata = params.metadata;
1026
- switch (typeof this._endpoints.wsEndpoint) {
1027
- case "undefined":
1028
- return !!this._getPublicKey(params) ? `wss://rooms.pluv.io/api/room/${room}` : `/api/pluv/room/${room}`;
1029
- case "string":
1030
- return this._endpoints.wsEndpoint;
1031
- default:
1032
- return this._endpoints.wsEndpoint({ metadata, room });
1033
- }
1034
- }
1035
- _handleExit(message) {
1036
- const { connectionId } = message;
1037
- if (!connectionId) return;
1038
- if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1039
- const clientId = this._usersManager.getClientId(connectionId);
1040
- const deleted = this._usersManager.deleteConnection(connectionId);
1041
- const others = this._usersManager.getOthers();
1042
- this._stateNotifier.subjects.others.next(others);
1043
- if (!deleted) {
1044
- console.warn("Could not identify exited connection");
1045
- return;
1046
- }
1047
- const { data: user, remaining } = deleted;
1048
- this._usersNotifier.others.next({
1049
- others,
1050
- event: { kind: "leave", user }
1051
- });
1052
- if (!remaining && !!clientId) this._usersNotifier.delete(clientId);
1053
- }
1054
- _handlePresenceUpdatedMessage(message) {
1055
- var _a;
1056
- const { connectionId } = message;
1057
- if (!connectionId) return;
1058
- if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1059
- const data = message.data;
1060
- const myself = (_a = this._usersManager.myself) != null ? _a : null;
1061
- if ((myself == null ? void 0 : myself.connectionId) === connectionId) return;
1062
- const updated = this._usersManager.patchPresence(connectionId, data.presence);
1063
- const myClientId = !!myself ? this._usersManager.getClientId(myself) : null;
1064
- const clientId = this._usersManager.getClientId(connectionId);
1065
- if (!!clientId && myClientId === clientId) {
1066
- this._stateNotifier.subjects["my-presence"].next(updated);
1067
- this._stateNotifier.subjects.myself.next(this._usersManager.myself);
1068
- return;
1069
- }
1070
- if (!!clientId) {
1071
- const other2 = this._usersManager.getOther(connectionId);
1072
- const others2 = this._usersManager.getOthers();
1073
- this._usersNotifier.other(clientId).next(other2);
1074
- this._stateNotifier.subjects.others.next(others2);
1075
- if (!!other2) {
1076
- this._usersNotifier.others.next({
1077
- others: others2,
1078
- event: { kind: "update", user: other2 }
1079
- });
1080
- }
1081
- return;
1082
- }
1083
- const added = this._usersManager.addConnection({
1084
- connectionId,
1085
- presence: data.presence,
1086
- user: message.user
1087
- });
1088
- const other = this._usersManager.getOther(connectionId);
1089
- const others = this._usersManager.getOthers();
1090
- this._usersNotifier.other(added.clientId).next(other);
1091
- this._stateNotifier.subjects.others.next(others);
1092
- this._usersNotifier.others.next({
1093
- others,
1094
- event: { kind: "update", user: added.data }
1095
- });
1096
- }
1097
- _handleReceiveOthers(message) {
1098
- if (!message.connectionId) return;
1099
- if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1100
- const data = message.data;
1101
- Object.keys(data.others).forEach((connectionId) => {
1102
- const { presence, user } = data.others[connectionId];
1103
- const result = this._usersManager.addConnection({
1104
- connectionId,
1105
- presence,
1106
- user
1107
- });
1108
- if (!!presence) this._usersManager.patchPresence(connectionId, presence);
1109
- if (result.isMyself) return;
1110
- const clientId = result.clientId;
1111
- const other = this._usersManager.getOther(connectionId);
1112
- this._usersNotifier.other(clientId).next(other);
1113
- });
1114
- const others = this._usersManager.getOthers();
1115
- this._stateNotifier.subjects.others.next(others);
1116
- this._usersNotifier.others.next({
1117
- others,
1118
- event: { kind: "sync", users: others }
1119
- });
1120
- }
1121
- _handleRegisteredMessage(message) {
1122
- return __async(this, null, function* () {
1123
- var _a, _b;
1124
- const { connectionId } = message;
1125
- const user = message.user;
1126
- if (!connectionId) return;
1127
- if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1128
- const data = message.data;
1129
- const state = data.state;
1130
- this._updateState((oldState) => {
1131
- oldState.connection.id = connectionId;
1132
- oldState.authorization.user = user != null ? user : null;
1133
- return oldState;
1134
- });
1135
- const userInfo = {
1136
- connectionId,
1137
- presence: (_a = data.presence) != null ? _a : void 0,
1138
- user
1139
- };
1140
- this._usersManager.setMyself(userInfo);
1141
- const presence = this._usersManager.myPresence;
1142
- const myself = (_b = this._usersManager.myself) != null ? _b : null;
1143
- this._stateNotifier.subjects["my-presence"].next(presence);
1144
- this._stateNotifier.subjects.myself.next(myself);
1145
- const update = yield (() => __async(this, null, function* () {
1146
- if (!!state) {
1147
- this._logDebug("Retrieving initial state");
1148
- return this._crdtManager.getInitialState();
1149
- }
1150
- const resolved = this._crdtManager.resolveEncodedState(
1151
- yield this._storageStore.getUpdates()
1152
- );
1153
- if (!!resolved) {
1154
- this._logDebug("Retrieving storage store state");
1155
- return resolved;
1156
- }
1157
- this._logDebug("Retrieving initial state");
1158
- return this._crdtManager.getInitialState();
1159
- }))();
1160
- this._sendMessage({
1161
- type: "$initializeSession",
1162
- data: { presence, update }
1163
- });
1164
- });
1165
- }
1166
- _handleStorageReceivedMessage(message) {
1167
- return __async(this, null, function* () {
1168
- const { connectionId } = message;
1169
- if (!connectionId) return;
1170
- if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1171
- const data = message.data;
1172
- const changeKind = data.changeKind;
1173
- const state = data.state;
1174
- const updates = yield this._storageStore.getUpdates();
1175
- const origin = ORIGIN_INITIALIZED;
1176
- const updateStorage = (update) => {
1177
- return this._crdtManager.initialized ? this._crdtManager.applyUpdate({ update, origin }) : this._crdtManager.initialize({ update, origin });
1178
- };
1179
- if (changeKind === "empty") {
1180
- this._crdtManager.destroy();
1181
- this._crdtManager.initialize({ origin, update: updates });
1182
- } else if (changeKind === "initialized") {
1183
- if (!!updates.length) updateStorage(updates);
1184
- else {
1185
- const encodedState2 = updateStorage(state).doc.getEncodedState();
1186
- this._addToStorageStore(encodedState2);
1187
- }
1188
- } else {
1189
- const encodedState2 = updateStorage(state).doc.getEncodedState();
1190
- if (!!updates.length) this._crdtManager.applyUpdate({ update: updates, origin });
1191
- yield this._addToStorageStore(encodedState2);
1192
- }
1193
- const encodedState = this._crdtManager.doc.getEncodedState();
1194
- this._emitSharedTypes();
1195
- this._observeCrdt();
1196
- this._updateState((oldState) => {
1197
- oldState.storage.state = StorageState.Synced;
1198
- return oldState;
1199
- });
1200
- this._stateNotifier.subjects["storage-loaded"].next(true);
1201
- this._sendMessage({
1202
- type: "$updateStorage",
1203
- data: { origin, update: encodedState }
1204
- });
1205
- });
1206
- }
1207
- _handleStorageUpdatedMessage(message) {
1208
- const { connectionId } = message;
1209
- if (!connectionId) return;
1210
- if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1211
- const data = message.data;
1212
- this._crdtManager.doc.applyEncodedState({
1213
- origin: ORIGIN_STORAGE_UPDATED,
1214
- update: data.state
1215
- });
1216
- const sharedTypes = this._crdtManager.doc.get();
1217
- const storageRoot = Object.keys(sharedTypes).reduce(
1218
- (acc, prop) => {
1219
- const serialized = this._crdtManager.doc.toJson(prop);
1220
- this._crdtNotifier.subject(prop).next(serialized);
1221
- return __spreadProps(__spreadValues({}, acc), { [prop]: serialized });
1222
- },
1223
- {}
1224
- );
1225
- this._crdtNotifier.rootSubject.next(storageRoot);
1226
- }
1227
- _handleSyncStateReceived(message) {
1228
- var _a;
1229
- const { connectionId } = message;
1230
- if (!connectionId) return;
1231
- if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1232
- if (!this._usersManager.myself) return;
1233
- const data = message.data;
1234
- const active = new Set((_a = data == null ? void 0 : data.connectionIds) != null ? _a : []);
1235
- const quitters = this._usersManager.getOthers().filter((other) => !active.has(other.connectionId));
1236
- quitters.forEach((quitter) => {
1237
- this._usersManager.deleteConnection(quitter.connectionId);
1238
- const remaining2 = this._usersManager.getOthers();
1239
- this._usersNotifier.others.next({
1240
- others: remaining2,
1241
- event: { kind: "leave", user: quitter }
1242
- });
1243
- });
1244
- const remaining = this._usersManager.getOthers();
1245
- this._stateNotifier.subjects.others.next(remaining);
1246
- }
1247
- _handleUserJoinedMessage(message) {
1248
- const { connectionId } = message;
1249
- if (!connectionId) return;
1250
- if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1251
- if (!this._usersManager.myself) return;
1252
- const data = message.data;
1253
- const myself = this._usersManager.myself;
1254
- if (myself.connectionId === connectionId) {
1255
- this._sendMessage({ type: "$getOthers", data: {} });
1256
- return;
1257
- }
1258
- const added = this._usersManager.addConnection({
1259
- connectionId,
1260
- presence: data.presence,
1261
- user: data.user
1262
- });
1263
- const other = this._usersManager.getOther(connectionId);
1264
- const others = this._usersManager.getOthers();
1265
- this._usersNotifier.other(added.clientId).next(other);
1266
- this._stateNotifier.subjects.others.next(others);
1267
- this._usersNotifier.others.next({
1268
- others,
1269
- event: { kind: "enter", user: added.data }
1270
- });
1271
- }
1272
- _heartbeat() {
1273
- this._clearTimeout(this._timeouts.pong);
1274
- this._timeouts.pong = setTimeout(
1275
- this._reconnect.bind(this),
1276
- PONG_TIMEOUT_MS
1277
- );
1278
- this._sendMessage('{"type":"$ping","data":{}}');
1279
- }
1280
- _logDebug(...data) {
1281
- var _a;
1282
- if (typeof process === "undefined") return;
1283
- if (((_a = process.env) == null ? void 0 : _a.NODE_ENV) === "production") return;
1284
- if (this._debug) console.log(...data);
1285
- }
1286
- _observeCrdt() {
1287
- var _a, _b;
1288
- (_b = (_a = this._subscriptions).observeCrdt) == null ? void 0 : _b.call(_a);
1289
- const unsubscribe = this._crdtManager.doc.subscribe((event) => {
1290
- var _a2;
1291
- const origin = (_a2 = event.origin) != null ? _a2 : null;
1292
- this._emitSharedTypes();
1293
- if (origin === ORIGIN_INITIALIZED || origin === ORIGIN_STORAGE_UPDATED) {
1294
- return;
1295
- }
1296
- this._addToStorageStore(event.update);
1297
- if (!this._state.webSocket) return;
1298
- if (!this._state.connection.id) return;
1299
- if (this._state.webSocket.readyState !== WebSocket.OPEN) return;
1300
- this._sendMessage({
1301
- type: "$updateStorage",
1302
- data: { origin, update: event.update }
1303
- });
1304
- });
1305
- this._subscriptions.observeCrdt = unsubscribe;
1306
- }
1307
- _onAuthorizationFail(error) {
1308
- const authError = error instanceof Error ? error : new Error("Authorization failed");
1309
- this._listeners.onAuthorizationFail(authError);
1310
- this._updateState((oldState) => {
1311
- oldState.connection.state = ConnectionState.Unavailable;
1312
- oldState.storage.state = StorageState.Unavailable;
1313
- return oldState;
1314
- });
1315
- }
1316
- _onClose(event) {
1317
- return __async(this, null, function* () {
1318
- this._listenerManager.subjects.close.next(event);
1319
- this._logDebug("WebSocket closed");
1320
- if (!!event.reason) this._logDebug(event.reason);
1321
- const shouldRetry = [
1322
- // Going away: Client/server is shutting down or navigating
1323
- 1001,
1324
- // Abnormal closure: Usually network error
1325
- 1006,
1326
- // Internal error: may be a larger problem, but try reconnecting for now
1327
- 1011,
1328
- // Service restart: Server restarted
1329
- 1012,
1330
- // Try again later: server overloaded
1331
- 1013,
1332
- // Bad gateway
1333
- 1014
1334
- ].some((okToRetryCode) => event.code === okToRetryCode);
1335
- this._updateState((oldState) => {
1336
- oldState.authorization.token = null;
1337
- oldState.connection.state = shouldRetry ? ConnectionState.Unavailable : (
1338
- /**
1339
- * !HACK
1340
- * @description Assume these are not recoverable. We're just going to mark this
1341
- * as Closed to avoid reconnect attempts.
1342
- * @date April 28, 2025
1343
- */
1344
- ConnectionState.Closed
1345
- );
1346
- oldState.storage.state = oldState.storage.state === StorageState.Synced ? StorageState.Offline : StorageState.Unavailable;
1347
- oldState.webSocket = null;
1348
- return oldState;
1349
- });
1350
- if (shouldRetry) {
1351
- yield this._reconnect();
1352
- return;
1353
- }
1354
- this._closeWs();
1355
- this._clearTimeout(this._timeouts.reconnect);
1356
- });
1357
- }
1358
- /**
1359
- * TODO
1360
- * @description Handle websocket errors in a meaningful way, should they occur
1361
- * @date August 19, 2022
1362
- */
1363
- _onError(event) {
1364
- this._listenerManager.subjects.error.next(event);
1365
- }
1366
- _onMessage(event) {
1367
- this._listenerManager.subjects.message.next(event);
1368
- const message = this._parseMessage(event);
1369
- if (!message) return;
1370
- const shouldLog = typeof this._debug === "boolean" ? this._debug : this._debug.output.find((value) => value === message.type);
1371
- if (shouldLog) {
1372
- this._logDebug("WebSocket event received: ", message.type, message);
1373
- }
1374
- this._eventNotifier.subject(message.type).next(message);
1375
- switch (message.type) {
1376
- case "$exit": {
1377
- this._handleExit(message);
1378
- return;
1379
- }
1380
- case "$othersReceived": {
1381
- this._handleReceiveOthers(message);
1382
- return;
1383
- }
1384
- case "$pong": {
1385
- this._clearTimeout(this._timeouts.pong);
1386
- return;
1387
- }
1388
- case "$presenceUpdated": {
1389
- this._handlePresenceUpdatedMessage(message);
1390
- return;
1391
- }
1392
- case "$registered": {
1393
- this._handleRegisteredMessage(message);
1394
- return;
1395
- }
1396
- case "$storageReceived": {
1397
- this._handleStorageReceivedMessage(message);
1398
- return;
1399
- }
1400
- case "$storageUpdated": {
1401
- this._handleStorageUpdatedMessage(message);
1402
- return;
1403
- }
1404
- case "$syncStateReceived": {
1405
- this._handleSyncStateReceived(message);
1406
- return;
1407
- }
1408
- case "$userJoined": {
1409
- this._handleUserJoinedMessage(message);
1410
- return;
1411
- }
1412
- default:
1413
- }
1414
- }
1415
- _onOpen(event) {
1416
- this._listenerManager.subjects.open.next(event);
1417
- if (this._state.connection.state !== ConnectionState.Connecting) return;
1418
- this._logDebug("WebSocket connected");
1419
- this._updateState((oldState) => {
1420
- oldState.connection.state = ConnectionState.Open;
1421
- return oldState;
1422
- });
1423
- this._clearInterval(this._intervals.heartbeat);
1424
- this._intervals.heartbeat = setInterval(
1425
- this._heartbeat.bind(this),
1426
- HEARTBEAT_INTERVAL_MS
1427
- );
1428
- }
1429
- _onNavigatorOffline() {
1430
- return __async(this, null, function* () {
1431
- if (this._state.connection.state === ConnectionState.Closed) return;
1432
- this._updateState((oldState) => {
1433
- oldState.connection.state = ConnectionState.Unavailable;
1434
- oldState.storage.state = oldState.storage.state === StorageState.Synced ? StorageState.Offline : StorageState.Unavailable;
1435
- return oldState;
1436
- });
1437
- });
1438
- }
1439
- _onNavigatorOnline() {
1440
- return __async(this, null, function* () {
1441
- if (this._state.connection.state !== ConnectionState.Unavailable) return;
1442
- yield this._reconnect();
1443
- });
1444
- }
1445
- _onVisibilityChange() {
1446
- return __async(this, null, function* () {
1447
- if (typeof document === "undefined") return;
1448
- if (document.visibilityState !== "visible") return;
1449
- switch (this._state.connection.state) {
1450
- case ConnectionState.Open: {
1451
- this._clearInterval(this._intervals.heartbeat);
1452
- setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS);
1453
- return;
1454
- }
1455
- case ConnectionState.Unavailable: {
1456
- yield this._reconnect();
1457
- return;
1458
- }
1459
- default:
1460
- }
1461
- });
1462
- }
1463
- _onWindowFocus() {
1464
- return __async(this, null, function* () {
1465
- return yield this._onVisibilityChange();
1466
- });
1467
- }
1468
- _parseMessage(message) {
1469
- if (typeof message.data !== "string") return null;
1470
- try {
1471
- const config = JSON.parse(message.data);
1472
- if (typeof config !== "object") return null;
1473
- if (typeof config.type !== "string") return null;
1474
- if (typeof config.data !== "object") return null;
1475
- return config;
1476
- } catch (e) {
1477
- return null;
1478
- }
1479
- }
1480
- _pollReconnect() {
1481
- this._clearTimeout(this._timeouts.reconnect);
1482
- const timeoutMs = typeof this._reconnectTimeoutMs === "number" ? this._reconnectTimeoutMs : this._reconnectTimeoutMs({ attempts: this._state.connection.attempts });
1483
- const clampedMs = Math.max(
1484
- Math.min(MIN_RECONNECT_TIMEOUT_MS, timeoutMs),
1485
- MAX_RECONNECT_TIMEOUT_MS
1486
- );
1487
- this._timeouts.reconnect = setTimeout(
1488
- this._reconnect.bind(this),
1489
- clampedMs
1490
- );
1491
- }
1492
- _reconnect() {
1493
- return __async(this, null, function* () {
1494
- this._closeWs();
1495
- if (this._state.connection.state === ConnectionState.Closed) return;
1496
- const metadata = this._lastMetadata;
1497
- const params = typeof metadata === "undefined" ? [] : [{ metadata }];
1498
- yield this.connect(...params);
1499
- });
1500
- }
1501
- _sendMessage(data) {
1502
- const webSocket = this._state.webSocket;
1503
- if ((webSocket == null ? void 0 : webSocket.readyState) !== WebSocket.OPEN) return;
1504
- const dataType = (() => {
1505
- var _a, _b, _c;
1506
- if (typeof data === "object") return (_a = data.type) != null ? _a : null;
1507
- if (typeof data !== "string") return null;
1508
- try {
1509
- return (_c = (_b = JSON.parse(data)) == null ? void 0 : _b.type) != null ? _c : null;
1510
- } catch (e) {
1511
- return null;
1512
- }
1513
- })();
1514
- const shouldLog = typeof this._debug === "boolean" ? this._debug : !dataType ? false : this._debug.input.some((value) => value === data.type);
1515
- if (shouldLog) this._logDebug("WebSocket event sent: ", data.type, data);
1516
- const message = typeof data === "string" ? data : JSON.stringify(data);
1517
- webSocket.send(message);
1518
- }
1519
- _setMetadata(metadata) {
1520
- const parsed = this.metadata ? this.metadata.parse(metadata) : metadata;
1521
- this._lastMetadata = parsed;
1522
- return parsed;
1523
- }
1524
- _updateState(updater) {
1525
- let authorization;
1526
- try {
1527
- authorization = JSON.parse(JSON.stringify(this._state.authorization));
1528
- } catch (e) {
1529
- throw new Error("User is not JSON serializable");
1530
- }
1531
- const oldState = __spreadProps(__spreadValues({}, this._state), {
1532
- authorization,
1533
- connection: JSON.parse(JSON.stringify(this._state.connection)),
1534
- storage: JSON.parse(JSON.stringify(this._state.storage)),
1535
- webSocket: this._state.webSocket
1536
- });
1537
- const newState = updater(oldState);
1538
- this._state = newState;
1539
- this._logDebug("WebSocket new state: ", newState);
1540
- this._stateNotifier.subjects.connection.next(newState);
1541
- return newState;
1542
- }
542
+ constructor(room, options) {
543
+ this._crdtNotifier = new CrdtNotifier();
544
+ this._eventNotifier = new EventNotifier();
545
+ this._intervals = { heartbeat: null };
546
+ this._listenerManager = new ListenerManager();
547
+ this._publicKey = null;
548
+ this._stateNotifier = new StateNotifier();
549
+ this._subscriptions = { observeCrdt: null };
550
+ this._timeouts = {
551
+ pong: null,
552
+ reconnect: null
553
+ };
554
+ this._usersNotifier = new UsersNotifier();
555
+ this._lastMetadata = null;
556
+ this._state = {
557
+ authorization: {
558
+ token: null,
559
+ user: null
560
+ },
561
+ connection: {
562
+ attempts: 0,
563
+ id: null,
564
+ state: ConnectionState$1.Untouched
565
+ },
566
+ storage: { state: StorageState.Unavailable },
567
+ webSocket: null
568
+ };
569
+ this._windowListeners = null;
570
+ this._wsListeners = null;
571
+ this.broadcast = new Proxy(async (event, data) => {
572
+ if (!this._state.webSocket) return;
573
+ if (this._state.connection.state !== ConnectionState$1.Open) return;
574
+ const type = event.toString();
575
+ const procedure = this._router._defs.events[type];
576
+ if (!procedure?.config.broadcast) {
577
+ this._sendMessage({
578
+ data,
579
+ type
580
+ });
581
+ return;
582
+ }
583
+ const myself = this._usersManager.myself;
584
+ if (!myself) return;
585
+ const parsed = procedure.config.input ? procedure.config.input.parse(data) : data;
586
+ const context = {
587
+ doc: this._crdtManager.doc,
588
+ others: this._usersManager.getOthers(),
589
+ room: this.id,
590
+ user: myself
591
+ };
592
+ const output = await procedure.config.broadcast(parsed, context);
593
+ Object.entries(output).forEach(([_type, _data]) => {
594
+ this._sendMessage({
595
+ data: _data,
596
+ type: _type
597
+ });
598
+ });
599
+ }, { get(fn, prop) {
600
+ return async (data) => {
601
+ return await fn(prop, data);
602
+ };
603
+ } });
604
+ this.canRedo = () => {
605
+ return !!this._crdtManager.doc.canRedo();
606
+ };
607
+ this.canUndo = () => {
608
+ return !!this._crdtManager.doc.canUndo();
609
+ };
610
+ this.getConnection = () => {
611
+ return Object.freeze(JSON.parse(JSON.stringify(this._state.connection)));
612
+ };
613
+ this.getDoc = () => {
614
+ return this._crdtManager.doc;
615
+ };
616
+ this.getMyPresence = () => {
617
+ return this._usersManager.myPresence;
618
+ };
619
+ this.getMyself = () => {
620
+ return this._usersManager.myself;
621
+ };
622
+ this.getOther = (connectionId) => {
623
+ return this._usersManager.getOther(connectionId);
624
+ };
625
+ this.getOthers = () => {
626
+ return this._usersManager.getOthers();
627
+ };
628
+ this.getStorage = (type) => {
629
+ const sharedType = this._crdtManager.get(type);
630
+ if (typeof sharedType === "undefined") return null;
631
+ return sharedType;
632
+ };
633
+ this.getStorageLoaded = () => {
634
+ return this._state.storage.state === StorageState.Offline || this._state.storage.state === StorageState.Synced;
635
+ };
636
+ this.redo = () => {
637
+ this._crdtManager.doc.redo();
638
+ };
639
+ this.subscribe = new Proxy((name, callback) => this._stateNotifier.subscribe(name, callback), { get: (fn, prop) => {
640
+ if (prop === "connection") return (callback) => {
641
+ return fn("connection", callback);
642
+ };
643
+ if (prop === "myPresence") return (callback) => {
644
+ return fn("my-presence", callback);
645
+ };
646
+ if (prop === "myself") return (callback) => {
647
+ return fn("myself", callback);
648
+ };
649
+ if (prop === "others") return (callback) => {
650
+ return this._usersNotifier.subscribeOthers(callback);
651
+ };
652
+ if (prop === "storageLoaded") return (callback) => {
653
+ return fn("storage-loaded", callback);
654
+ };
655
+ if (prop === "event") return this._event;
656
+ if (prop === "other") return this._other;
657
+ if (prop === "storage") return this.#_storage;
658
+ } });
659
+ this.transact = (fn, origin) => {
660
+ const _origin = origin ?? this._state.connection.id;
661
+ /**
662
+ * !HACK
663
+ * @description Don't transact anything if there is no origin, because
664
+ * that means the user isn't connected yet. This will mean events are
665
+ * lost unfortunately.
666
+ * @date September 23, 2023
667
+ */
668
+ if (typeof _origin !== "string") return;
669
+ this._crdtManager.doc.transact(() => {
670
+ fn(this._crdtManager.doc.get());
671
+ }, _origin);
672
+ };
673
+ this.undo = () => {
674
+ this._crdtManager.doc.undo();
675
+ };
676
+ this.updateMyPresence = (presence$1) => {
677
+ const newPresence = typeof presence$1 === "function" ? presence$1(this.getMyPresence()) : presence$1;
678
+ this._usersManager.updateMyPresence(newPresence);
679
+ const myPresence = this._usersManager.myPresence;
680
+ const myself = this._usersManager.myself ?? null;
681
+ this._stateNotifier.subjects["my-presence"].next(myPresence);
682
+ if (!!myself) this._stateNotifier.subjects.myself.next(myself);
683
+ this.broadcast("$updatePresence", { presence: newPresence });
684
+ };
685
+ this._event = new Proxy((event, callback) => this._eventNotifier.subscribe(event, callback), { get(fn, prop) {
686
+ return (callback) => fn(prop, callback);
687
+ } });
688
+ this._flattenStorageStore = debounce(async () => {
689
+ if (!await this._storageStore.getShouldFlatten()) return;
690
+ const updates = await this._storageStore.getUpdates();
691
+ this._crdtManager.applyUpdate({
692
+ update: updates,
693
+ origin: this
694
+ });
695
+ const encodedState = this._crdtManager.doc.getEncodedState();
696
+ await this._storageStore.flatten(encodedState);
697
+ }, { wait: ADD_TO_STORAGE_STATE_DEBOUNCE_MS });
698
+ this._getAddon = (addons$1) => {
699
+ return addons$1.reduce((acc, addon$1) => () => ({
700
+ ...acc({ room: this }),
701
+ ...addon$1({ room: this })
702
+ }), DEFAULT_PLUV_CLIENT_ADDON);
703
+ };
704
+ this._other = (connectionId, callback) => {
705
+ const clientId = this._usersManager.getClientId(connectionId);
706
+ if (!clientId) return () => void 0;
707
+ return this._usersNotifier.subscribeOther(clientId, callback);
708
+ };
709
+ const { addons = [], authEndpoint, debug = false, initialPresence, initialStorage, limits, metadata, onAuthorizationFail, presence, publicKey, reconnectTimeoutMs = RECONNECT_TIMEOUT_MS, router, wsEndpoint } = options;
710
+ this.id = room;
711
+ this.metadata = metadata;
712
+ const addon = this._getAddon(addons);
713
+ const { storage } = {
714
+ ...DEFAULT_PLUV_CLIENT_ADDON({ room: this }),
715
+ ...addon({ room: this })
716
+ };
717
+ this._storageStore = storage;
718
+ this._debug = debug;
719
+ this._endpoints = {
720
+ authEndpoint,
721
+ wsEndpoint
722
+ };
723
+ this._limits = limits;
724
+ this._reconnectTimeoutMs = reconnectTimeoutMs;
725
+ if (!!publicKey) this._publicKey = publicKey;
726
+ this._listeners = { onAuthorizationFail: (error) => {
727
+ onAuthorizationFail?.(error);
728
+ } };
729
+ this._router = router ?? new PluvRouter({});
730
+ this._usersManager = new UsersManager({
731
+ initialPresence,
732
+ limits: this._limits,
733
+ presence
734
+ });
735
+ this._crdtManager = new CrdtManager({ initialStorage });
736
+ }
737
+ get webSocket() {
738
+ return this._state.webSocket;
739
+ }
740
+ addEventListener(kind, handler) {
741
+ return this._listenerManager.subscribe(kind, handler);
742
+ }
743
+ async connect(...args) {
744
+ const params = args[0] ?? {};
745
+ const metadata = params.metadata;
746
+ this._setMetadata(metadata);
747
+ if (![
748
+ ConnectionState$1.Closed,
749
+ ConnectionState$1.Unavailable,
750
+ ConnectionState$1.Untouched
751
+ ].some((state) => this._state.connection.state === state)) return;
752
+ this._clearTimeout(this._timeouts.reconnect);
753
+ this._updateState((oldState) => {
754
+ oldState.connection.state = ConnectionState$1.Connecting;
755
+ return oldState;
756
+ });
757
+ await this._storageStore.initialize();
758
+ const wsEndpoint = this._getWsEndpoint(this.id, params);
759
+ const url = new URL(wsEndpoint);
760
+ let authToken = null;
761
+ let webSocket;
762
+ try {
763
+ const publicKey = this._getPublicKey(params);
764
+ authToken = await this._getAuthorization(this.id, params);
765
+ if (authToken) url.searchParams.set("token", encodeURIComponent(authToken));
766
+ if (publicKey) url.searchParams.set("public_key", encodeURIComponent(publicKey));
767
+ webSocket = new WebSocket(url.toString());
768
+ } catch (err) {
769
+ this._updateState((oldState) => {
770
+ oldState.connection.attempts += 1;
771
+ return oldState;
772
+ });
773
+ await this._storageStore.destroy();
774
+ this._pollReconnect();
775
+ await Promise.resolve(this._onAuthorizationFail(err));
776
+ return;
777
+ }
778
+ this._updateState((oldState) => {
779
+ oldState.connection.attempts = 0;
780
+ oldState.authorization.token = authToken;
781
+ oldState.storage.state = StorageState.Loading;
782
+ oldState.webSocket = webSocket;
783
+ return oldState;
784
+ });
785
+ this._attachWindowListeners();
786
+ this._attachWsListeners();
787
+ }
788
+ async disconnect() {
789
+ if (!this._state.webSocket) return;
790
+ if (![ConnectionState$1.Connecting, ConnectionState$1.Open].some((state) => this._state.connection.state === state)) return;
791
+ this._closeWs();
792
+ this._clearInterval(this._intervals.heartbeat);
793
+ this._clearTimeout(this._timeouts.reconnect);
794
+ this._updateState((oldState) => {
795
+ oldState.authorization.token = null;
796
+ oldState.connection.state = ConnectionState$1.Closed;
797
+ oldState.storage.state = oldState.storage.state === StorageState.Synced ? StorageState.Offline : StorageState.Unavailable;
798
+ oldState.webSocket = null;
799
+ return oldState;
800
+ });
801
+ await this._storageStore.destroy();
802
+ this._crdtManager.destroy();
803
+ this._stateNotifier.subjects["my-presence"].next(null);
804
+ }
805
+ getStorageJson(type) {
806
+ if (this._state.connection.id === null) return null;
807
+ if (typeof type === "undefined") return this._crdtManager.doc.toJson();
808
+ if (typeof this._crdtManager.get(type) === "undefined") return null;
809
+ return this._crdtManager.doc.toJson(type);
810
+ }
811
+ async _addToStorageStore(update) {
812
+ await this._storageStore.addUpdate(update);
813
+ this._flattenStorageStore();
814
+ }
815
+ _attachWindowListeners() {
816
+ if (typeof window === "undefined") return;
817
+ if (typeof document === "undefined") return;
818
+ this._windowListeners = this._windowListeners || {
819
+ onNavigatorOffline: this._onNavigatorOffline.bind(this),
820
+ onNavigatorOnline: this._onNavigatorOnline.bind(this),
821
+ onVisibilityChange: this._onVisibilityChange.bind(this),
822
+ onWindowFocus: this._onWindowFocus.bind(this)
823
+ };
824
+ window.addEventListener("offline", this._windowListeners.onNavigatorOffline);
825
+ window.addEventListener("online", this._windowListeners.onNavigatorOnline);
826
+ document.addEventListener("visibilitychange", this._windowListeners.onVisibilityChange);
827
+ window.addEventListener("focus", this._windowListeners.onWindowFocus);
828
+ }
829
+ _attachWsListeners() {
830
+ if (!this._state.webSocket) return;
831
+ this._wsListeners = this._wsListeners || {
832
+ onClose: this._onClose.bind(this),
833
+ onError: this._onError.bind(this),
834
+ onMessage: this._onMessage.bind(this),
835
+ onOpen: this._onOpen.bind(this)
836
+ };
837
+ this._state.webSocket.addEventListener("close", this._wsListeners.onClose);
838
+ this._state.webSocket.addEventListener("error", this._wsListeners.onError);
839
+ this._state.webSocket.addEventListener("message", this._wsListeners.onMessage);
840
+ this._state.webSocket.addEventListener("open", this._wsListeners.onOpen);
841
+ }
842
+ _clearInterval(interval) {
843
+ if (Number.isNaN(interval)) return;
844
+ clearInterval(interval);
845
+ }
846
+ _clearTimeout(timeout) {
847
+ if (Number.isNaN(timeout)) return;
848
+ clearTimeout(timeout);
849
+ }
850
+ _closeWs() {
851
+ this._subscriptions.observeCrdt?.();
852
+ this._clearInterval(this._intervals.heartbeat);
853
+ this._clearTimeout(this._timeouts.pong);
854
+ this._detachWindowListeners();
855
+ this._usersManager.removeMyself();
856
+ this._usersManager.clearConnections();
857
+ this._usersNotifier.clear();
858
+ this._stateNotifier.subjects.myself.next(null);
859
+ this._stateNotifier.subjects.others.next([]);
860
+ this._usersNotifier.others.next({
861
+ others: [],
862
+ event: { kind: "clear" }
863
+ });
864
+ this._state.webSocket?.close();
865
+ this._detachWsListeners();
866
+ }
867
+ _detachWindowListeners() {
868
+ if (!this._windowListeners) return;
869
+ if (typeof window === "undefined") return;
870
+ if (typeof document === "undefined") return;
871
+ window.removeEventListener("offline", this._windowListeners.onNavigatorOffline);
872
+ window.removeEventListener("online", this._windowListeners.onNavigatorOnline);
873
+ document.removeEventListener("visibilitychange", this._windowListeners.onVisibilityChange);
874
+ window.removeEventListener("focus", this._windowListeners.onWindowFocus);
875
+ this._windowListeners = null;
876
+ }
877
+ _detachWsListeners() {
878
+ if (!this._state.webSocket || !this._wsListeners) return;
879
+ this._state.webSocket.removeEventListener("close", this._wsListeners.onClose);
880
+ this._state.webSocket.removeEventListener("error", this._wsListeners.onError);
881
+ this._state.webSocket.removeEventListener("message", this._wsListeners.onMessage);
882
+ this._state.webSocket.removeEventListener("open", this._wsListeners.onOpen);
883
+ this._wsListeners = null;
884
+ }
885
+ _emitSharedTypes() {
886
+ const sharedTypes = this._crdtManager.doc.get();
887
+ const storageRoot = Object.keys(sharedTypes).reduce((acc, prop) => {
888
+ const serialized = this._crdtManager.doc.toJson(prop);
889
+ this._crdtNotifier.subject(prop).next(serialized);
890
+ return {
891
+ ...acc,
892
+ [prop]: serialized
893
+ };
894
+ }, {});
895
+ this._crdtNotifier.rootSubject.next(storageRoot);
896
+ }
897
+ _getAuthFetchOptions(room, params) {
898
+ const metadata = params.metadata;
899
+ if (typeof this._endpoints.authEndpoint === "undefined") return null;
900
+ if (this._endpoints.authEndpoint === true) return {
901
+ url: `/api/pluv/authorize?room=${room}`,
902
+ options: {}
903
+ };
904
+ if (typeof this._endpoints.authEndpoint === "string") return {
905
+ url: this._endpoints.authEndpoint,
906
+ options: {}
907
+ };
908
+ const result = this._endpoints.authEndpoint({
909
+ metadata,
910
+ room
911
+ });
912
+ return typeof result === "string" ? {
913
+ url: result,
914
+ options: {}
915
+ } : result;
916
+ }
917
+ async _getAuthorization(room, params) {
918
+ const fetchOptions = this._getAuthFetchOptions(room, params);
919
+ if (!fetchOptions) return null;
920
+ const { url, options } = fetchOptions;
921
+ const res = await fetch(url, options);
922
+ if (!res.ok || res.status !== 200) throw new Error("Room is unauthorized");
923
+ try {
924
+ return await res.text().then((text) => text.trim());
925
+ } catch (err) {
926
+ throw new Error(err instanceof Error ? err.message : "Room is unauthorized");
927
+ }
928
+ }
929
+ _getPublicKey(params) {
930
+ const metadata = params.metadata;
931
+ if (!this._publicKey) return null;
932
+ if (typeof this._publicKey === "string") return this._publicKey;
933
+ return this._publicKey({ metadata });
934
+ }
935
+ _getWsEndpoint(room, params) {
936
+ const metadata = params.metadata;
937
+ switch (typeof this._endpoints.wsEndpoint) {
938
+ case "undefined": return !!this._getPublicKey(params) ? `wss://rooms.pluv.io/api/room/${room}` : `/api/pluv/room/${room}`;
939
+ case "string": return this._endpoints.wsEndpoint;
940
+ default: return this._endpoints.wsEndpoint({
941
+ metadata,
942
+ room
943
+ });
944
+ }
945
+ }
946
+ _handleExit(message) {
947
+ const { connectionId } = message;
948
+ if (!connectionId) return;
949
+ if (!this._state.webSocket) throw new Error("Could not find WebSocket");
950
+ const clientId = this._usersManager.getClientId(connectionId);
951
+ const deleted = this._usersManager.deleteConnection(connectionId);
952
+ const others = this._usersManager.getOthers();
953
+ this._stateNotifier.subjects.others.next(others);
954
+ if (!deleted) {
955
+ console.warn("Could not identify exited connection");
956
+ return;
957
+ }
958
+ const { data: user, remaining } = deleted;
959
+ this._usersNotifier.others.next({
960
+ others,
961
+ event: {
962
+ kind: "leave",
963
+ user
964
+ }
965
+ });
966
+ /**
967
+ * @description A single user can have multiple connections. So we're checking that
968
+ * there isn't a remaining connection before choosing to delete that user for other
969
+ * connected participants.
970
+ * @date April 16, 2025
971
+ */
972
+ if (!remaining && !!clientId) this._usersNotifier.delete(clientId);
973
+ }
974
+ _handlePresenceUpdatedMessage(message) {
975
+ const { connectionId } = message;
976
+ if (!connectionId) return;
977
+ if (!this._state.webSocket) throw new Error("Could not find WebSocket");
978
+ const data = message.data;
979
+ const myself = this._usersManager.myself ?? null;
980
+ /**
981
+ * !HACK
982
+ * @description We're going to have the user's own presence be patched only via local calls
983
+ * to this.updateMyPresence. So we'll not update the user's presence in this handler to
984
+ * avoid weird update delays to the user's own presence.
985
+ *
986
+ * However, we will patch the user's own presence if the user has updated their own
987
+ * presence via another connection that is not this one (e.g. if the user has opened
988
+ * another connection in another browser tab/window somewhere).
989
+ * @date April 2, 2025
990
+ */
991
+ if (myself?.connectionId === connectionId) return;
992
+ const updated = this._usersManager.patchPresence(connectionId, data.presence);
993
+ const myClientId = !!myself ? this._usersManager.getClientId(myself) : null;
994
+ const clientId = this._usersManager.getClientId(connectionId);
995
+ if (!!clientId && myClientId === clientId) {
996
+ this._stateNotifier.subjects["my-presence"].next(updated);
997
+ this._stateNotifier.subjects.myself.next(this._usersManager.myself);
998
+ return;
999
+ }
1000
+ if (!!clientId) {
1001
+ const other$1 = this._usersManager.getOther(connectionId);
1002
+ const others$1 = this._usersManager.getOthers();
1003
+ this._usersNotifier.other(clientId).next(other$1);
1004
+ this._stateNotifier.subjects.others.next(others$1);
1005
+ if (!!other$1) this._usersNotifier.others.next({
1006
+ others: others$1,
1007
+ event: {
1008
+ kind: "update",
1009
+ user: other$1
1010
+ }
1011
+ });
1012
+ return;
1013
+ }
1014
+ /**
1015
+ * !HACK
1016
+ * @description User could not be found. Add the connection to keep others up-to-date
1017
+ * @date April 19, 2025
1018
+ */
1019
+ const added = this._usersManager.addConnection({
1020
+ connectionId,
1021
+ presence: data.presence,
1022
+ user: message.user
1023
+ });
1024
+ const other = this._usersManager.getOther(connectionId);
1025
+ const others = this._usersManager.getOthers();
1026
+ this._usersNotifier.other(added.clientId).next(other);
1027
+ this._stateNotifier.subjects.others.next(others);
1028
+ this._usersNotifier.others.next({
1029
+ others,
1030
+ event: {
1031
+ kind: "update",
1032
+ user: added.data
1033
+ }
1034
+ });
1035
+ }
1036
+ _handleReceiveOthers(message) {
1037
+ if (!message.connectionId) return;
1038
+ if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1039
+ const data = message.data;
1040
+ Object.keys(data.others).forEach((connectionId) => {
1041
+ const { presence, user } = data.others[connectionId];
1042
+ const result = this._usersManager.addConnection({
1043
+ connectionId,
1044
+ presence,
1045
+ user
1046
+ });
1047
+ if (!!presence) this._usersManager.patchPresence(connectionId, presence);
1048
+ if (result.isMyself) return;
1049
+ const clientId = result.clientId;
1050
+ const other = this._usersManager.getOther(connectionId);
1051
+ this._usersNotifier.other(clientId).next(other);
1052
+ });
1053
+ const others = this._usersManager.getOthers();
1054
+ this._stateNotifier.subjects.others.next(others);
1055
+ this._usersNotifier.others.next({
1056
+ others,
1057
+ event: {
1058
+ kind: "sync",
1059
+ users: others
1060
+ }
1061
+ });
1062
+ }
1063
+ async _handleRegisteredMessage(message) {
1064
+ const { connectionId } = message;
1065
+ const user = message.user;
1066
+ if (!connectionId) return;
1067
+ if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1068
+ const data = message.data;
1069
+ const state = data.state;
1070
+ this._updateState((oldState) => {
1071
+ oldState.connection.id = connectionId;
1072
+ oldState.authorization.user = user ?? null;
1073
+ return oldState;
1074
+ });
1075
+ const userInfo = {
1076
+ connectionId,
1077
+ presence: data.presence ?? void 0,
1078
+ user
1079
+ };
1080
+ this._usersManager.setMyself(userInfo);
1081
+ const presence = this._usersManager.myPresence;
1082
+ const myself = this._usersManager.myself ?? null;
1083
+ this._stateNotifier.subjects["my-presence"].next(presence);
1084
+ this._stateNotifier.subjects.myself.next(myself);
1085
+ const update = await (async () => {
1086
+ if (!!state) {
1087
+ this._logDebug("Retrieving initial state");
1088
+ return this._crdtManager.getInitialState();
1089
+ }
1090
+ const resolved = this._crdtManager.resolveEncodedState(await this._storageStore.getUpdates());
1091
+ if (!!resolved) {
1092
+ this._logDebug("Retrieving storage store state");
1093
+ return resolved;
1094
+ }
1095
+ this._logDebug("Retrieving initial state");
1096
+ return this._crdtManager.getInitialState();
1097
+ })();
1098
+ this._sendMessage({
1099
+ type: "$initializeSession",
1100
+ data: {
1101
+ presence,
1102
+ update
1103
+ }
1104
+ });
1105
+ }
1106
+ async _handleStorageReceivedMessage(message) {
1107
+ const { connectionId } = message;
1108
+ if (!connectionId) return;
1109
+ if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1110
+ const data = message.data;
1111
+ const changeKind = data.changeKind;
1112
+ const state = data.state;
1113
+ const updates = await this._storageStore.getUpdates();
1114
+ const origin = ORIGIN_INITIALIZED;
1115
+ const updateStorage = (update) => {
1116
+ return this._crdtManager.initialized ? this._crdtManager.applyUpdate({
1117
+ update,
1118
+ origin
1119
+ }) : this._crdtManager.initialize({
1120
+ update,
1121
+ origin
1122
+ });
1123
+ };
1124
+ if (changeKind === "empty") {
1125
+ this._crdtManager.destroy();
1126
+ this._crdtManager.initialize({
1127
+ origin,
1128
+ update: updates
1129
+ });
1130
+ } else if (changeKind === "initialized") if (!!updates.length) updateStorage(updates);
1131
+ else {
1132
+ const encodedState$1 = updateStorage(state).doc.getEncodedState();
1133
+ this._addToStorageStore(encodedState$1);
1134
+ }
1135
+ else {
1136
+ const encodedState$1 = updateStorage(state).doc.getEncodedState();
1137
+ if (!!updates.length) this._crdtManager.applyUpdate({
1138
+ update: updates,
1139
+ origin
1140
+ });
1141
+ await this._addToStorageStore(encodedState$1);
1142
+ }
1143
+ const encodedState = this._crdtManager.doc.getEncodedState();
1144
+ this._emitSharedTypes();
1145
+ this._observeCrdt();
1146
+ this._updateState((oldState) => {
1147
+ oldState.storage.state = StorageState.Synced;
1148
+ return oldState;
1149
+ });
1150
+ this._stateNotifier.subjects["storage-loaded"].next(true);
1151
+ this._sendMessage({
1152
+ type: "$updateStorage",
1153
+ data: {
1154
+ origin,
1155
+ update: encodedState
1156
+ }
1157
+ });
1158
+ }
1159
+ _handleStorageUpdatedMessage(message) {
1160
+ const { connectionId } = message;
1161
+ if (!connectionId) return;
1162
+ if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1163
+ const data = message.data;
1164
+ this._crdtManager.doc.applyEncodedState({
1165
+ origin: ORIGIN_STORAGE_UPDATED,
1166
+ update: data.state
1167
+ });
1168
+ const sharedTypes = this._crdtManager.doc.get();
1169
+ const storageRoot = Object.keys(sharedTypes).reduce((acc, prop) => {
1170
+ const serialized = this._crdtManager.doc.toJson(prop);
1171
+ this._crdtNotifier.subject(prop).next(serialized);
1172
+ return {
1173
+ ...acc,
1174
+ [prop]: serialized
1175
+ };
1176
+ }, {});
1177
+ this._crdtNotifier.rootSubject.next(storageRoot);
1178
+ }
1179
+ _handleSyncStateReceived(message) {
1180
+ const { connectionId } = message;
1181
+ if (!connectionId) return;
1182
+ if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1183
+ if (!this._usersManager.myself) return;
1184
+ const data = message.data;
1185
+ const active = new Set(data?.connectionIds ?? []);
1186
+ this._usersManager.getOthers().filter((other) => !active.has(other.connectionId)).forEach((quitter) => {
1187
+ this._usersManager.deleteConnection(quitter.connectionId);
1188
+ const remaining$1 = this._usersManager.getOthers();
1189
+ this._usersNotifier.others.next({
1190
+ others: remaining$1,
1191
+ event: {
1192
+ kind: "leave",
1193
+ user: quitter
1194
+ }
1195
+ });
1196
+ });
1197
+ const remaining = this._usersManager.getOthers();
1198
+ this._stateNotifier.subjects.others.next(remaining);
1199
+ }
1200
+ _handleUserJoinedMessage(message) {
1201
+ const { connectionId } = message;
1202
+ if (!connectionId) return;
1203
+ if (!this._state.webSocket) throw new Error("Could not find WebSocket");
1204
+ if (!this._usersManager.myself) return;
1205
+ const data = message.data;
1206
+ if (this._usersManager.myself.connectionId === connectionId) {
1207
+ this._sendMessage({
1208
+ type: "$getOthers",
1209
+ data: {}
1210
+ });
1211
+ return;
1212
+ }
1213
+ const added = this._usersManager.addConnection({
1214
+ connectionId,
1215
+ presence: data.presence,
1216
+ user: data.user
1217
+ });
1218
+ const other = this._usersManager.getOther(connectionId);
1219
+ const others = this._usersManager.getOthers();
1220
+ this._usersNotifier.other(added.clientId).next(other);
1221
+ this._stateNotifier.subjects.others.next(others);
1222
+ this._usersNotifier.others.next({
1223
+ others,
1224
+ event: {
1225
+ kind: "enter",
1226
+ user: added.data
1227
+ }
1228
+ });
1229
+ }
1230
+ _heartbeat() {
1231
+ this._clearTimeout(this._timeouts.pong);
1232
+ this._timeouts.pong = setTimeout(this._reconnect.bind(this), PONG_TIMEOUT_MS);
1233
+ /**
1234
+ * !HACK
1235
+ * @description Send data as a stable string so that the server can react to this in a
1236
+ * consistent way.
1237
+ * @date July 8, 2024
1238
+ */
1239
+ this._sendMessage("{\"type\":\"$ping\",\"data\":{}}");
1240
+ }
1241
+ _logDebug(...data) {
1242
+ if (typeof process === "undefined") return;
1243
+ if (process.env?.NODE_ENV === "production") return;
1244
+ if (this._debug) console.log(...data);
1245
+ }
1246
+ _observeCrdt() {
1247
+ this._subscriptions.observeCrdt?.();
1248
+ const unsubscribe = this._crdtManager.doc.subscribe((event) => {
1249
+ const origin = event.origin ?? null;
1250
+ this._emitSharedTypes();
1251
+ if (origin === ORIGIN_INITIALIZED || origin === ORIGIN_STORAGE_UPDATED) return;
1252
+ this._addToStorageStore(event.update);
1253
+ if (!this._state.webSocket) return;
1254
+ if (!this._state.connection.id) return;
1255
+ if (this._state.webSocket.readyState !== WebSocket.OPEN) return;
1256
+ this._sendMessage({
1257
+ type: "$updateStorage",
1258
+ data: {
1259
+ origin,
1260
+ update: event.update
1261
+ }
1262
+ });
1263
+ });
1264
+ this._subscriptions.observeCrdt = unsubscribe;
1265
+ }
1266
+ _onAuthorizationFail(error) {
1267
+ const authError = error instanceof Error ? error : /* @__PURE__ */ new Error("Authorization failed");
1268
+ this._listeners.onAuthorizationFail(authError);
1269
+ this._updateState((oldState) => {
1270
+ oldState.connection.state = ConnectionState$1.Unavailable;
1271
+ oldState.storage.state = StorageState.Unavailable;
1272
+ return oldState;
1273
+ });
1274
+ }
1275
+ async _onClose(event) {
1276
+ this._listenerManager.subjects.close.next(event);
1277
+ this._logDebug("WebSocket closed");
1278
+ if (!!event.reason) this._logDebug(event.reason);
1279
+ const shouldRetry = [
1280
+ 1001,
1281
+ 1006,
1282
+ 1011,
1283
+ 1012,
1284
+ 1013,
1285
+ 1014
1286
+ ].some((okToRetryCode) => event.code === okToRetryCode);
1287
+ this._updateState((oldState) => {
1288
+ oldState.authorization.token = null;
1289
+ oldState.connection.state = shouldRetry ? ConnectionState$1.Unavailable : ConnectionState$1.Closed;
1290
+ oldState.storage.state = oldState.storage.state === StorageState.Synced ? StorageState.Offline : StorageState.Unavailable;
1291
+ oldState.webSocket = null;
1292
+ return oldState;
1293
+ });
1294
+ if (shouldRetry) {
1295
+ await this._reconnect();
1296
+ return;
1297
+ }
1298
+ this._closeWs();
1299
+ this._clearTimeout(this._timeouts.reconnect);
1300
+ }
1301
+ /**
1302
+ * TODO
1303
+ * @description Handle websocket errors in a meaningful way, should they occur
1304
+ * @date August 19, 2022
1305
+ */
1306
+ _onError(event) {
1307
+ this._listenerManager.subjects.error.next(event);
1308
+ }
1309
+ _onMessage(event) {
1310
+ this._listenerManager.subjects.message.next(event);
1311
+ const message = this._parseMessage(event);
1312
+ if (!message) return;
1313
+ if (typeof this._debug === "boolean" ? this._debug : this._debug.output.find((value) => value === message.type)) this._logDebug("WebSocket event received: ", message.type, message);
1314
+ this._eventNotifier.subject(message.type).next(message);
1315
+ switch (message.type) {
1316
+ case "$exit":
1317
+ this._handleExit(message);
1318
+ return;
1319
+ case "$othersReceived":
1320
+ this._handleReceiveOthers(message);
1321
+ return;
1322
+ case "$pong":
1323
+ this._clearTimeout(this._timeouts.pong);
1324
+ return;
1325
+ case "$presenceUpdated":
1326
+ this._handlePresenceUpdatedMessage(message);
1327
+ return;
1328
+ case "$registered":
1329
+ this._handleRegisteredMessage(message);
1330
+ return;
1331
+ case "$storageReceived":
1332
+ this._handleStorageReceivedMessage(message);
1333
+ return;
1334
+ case "$storageUpdated":
1335
+ this._handleStorageUpdatedMessage(message);
1336
+ return;
1337
+ case "$syncStateReceived":
1338
+ this._handleSyncStateReceived(message);
1339
+ return;
1340
+ case "$userJoined":
1341
+ this._handleUserJoinedMessage(message);
1342
+ return;
1343
+ default:
1344
+ }
1345
+ }
1346
+ _onOpen(event) {
1347
+ this._listenerManager.subjects.open.next(event);
1348
+ if (this._state.connection.state !== ConnectionState$1.Connecting) return;
1349
+ this._logDebug("WebSocket connected");
1350
+ this._updateState((oldState) => {
1351
+ oldState.connection.state = ConnectionState$1.Open;
1352
+ return oldState;
1353
+ });
1354
+ this._clearInterval(this._intervals.heartbeat);
1355
+ this._intervals.heartbeat = setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS);
1356
+ }
1357
+ async _onNavigatorOffline() {
1358
+ if (this._state.connection.state === ConnectionState$1.Closed) return;
1359
+ this._updateState((oldState) => {
1360
+ oldState.connection.state = ConnectionState$1.Unavailable;
1361
+ oldState.storage.state = oldState.storage.state === StorageState.Synced ? StorageState.Offline : StorageState.Unavailable;
1362
+ return oldState;
1363
+ });
1364
+ }
1365
+ async _onNavigatorOnline() {
1366
+ if (this._state.connection.state !== ConnectionState$1.Unavailable) return;
1367
+ await this._reconnect();
1368
+ }
1369
+ async _onVisibilityChange() {
1370
+ if (typeof document === "undefined") return;
1371
+ if (document.visibilityState !== "visible") return;
1372
+ switch (this._state.connection.state) {
1373
+ case ConnectionState$1.Open:
1374
+ this._clearInterval(this._intervals.heartbeat);
1375
+ setInterval(this._heartbeat.bind(this), HEARTBEAT_INTERVAL_MS);
1376
+ return;
1377
+ case ConnectionState$1.Unavailable:
1378
+ await this._reconnect();
1379
+ return;
1380
+ default:
1381
+ }
1382
+ }
1383
+ async _onWindowFocus() {
1384
+ return await this._onVisibilityChange();
1385
+ }
1386
+ _parseMessage(message) {
1387
+ /**
1388
+ * !HACK
1389
+ * @description We'll only handle stringified JSONs for now
1390
+ * @date August 9, 2022
1391
+ */
1392
+ if (typeof message.data !== "string") return null;
1393
+ try {
1394
+ const config = JSON.parse(message.data);
1395
+ if (typeof config !== "object") return null;
1396
+ if (typeof config.type !== "string") return null;
1397
+ if (typeof config.data !== "object") return null;
1398
+ return config;
1399
+ } catch {
1400
+ return null;
1401
+ }
1402
+ }
1403
+ _pollReconnect() {
1404
+ this._clearTimeout(this._timeouts.reconnect);
1405
+ const timeoutMs = typeof this._reconnectTimeoutMs === "number" ? this._reconnectTimeoutMs : this._reconnectTimeoutMs({ attempts: this._state.connection.attempts });
1406
+ const clampedMs = Math.max(Math.min(MIN_RECONNECT_TIMEOUT_MS, timeoutMs), MAX_RECONNECT_TIMEOUT_MS);
1407
+ this._timeouts.reconnect = setTimeout(this._reconnect.bind(this), clampedMs);
1408
+ }
1409
+ async _reconnect() {
1410
+ this._closeWs();
1411
+ /**
1412
+ * @description User has manually closed this connection. We're not going to retry, since
1413
+ * the user probably intends for this to stay closed. They will need to invoke connect
1414
+ * themselves.
1415
+ * @date April 28, 2025
1416
+ */
1417
+ if (this._state.connection.state === ConnectionState$1.Closed) return;
1418
+ const metadata = this._lastMetadata;
1419
+ const params = typeof metadata === "undefined" ? [] : [{ metadata }];
1420
+ await this.connect(...params);
1421
+ }
1422
+ _sendMessage(data) {
1423
+ const webSocket = this._state.webSocket;
1424
+ if (webSocket?.readyState !== WebSocket.OPEN) return;
1425
+ const dataType = (() => {
1426
+ if (typeof data === "object") return data.type ?? null;
1427
+ if (typeof data !== "string") return null;
1428
+ try {
1429
+ return JSON.parse(data)?.type ?? null;
1430
+ } catch {
1431
+ return null;
1432
+ }
1433
+ })();
1434
+ if (typeof this._debug === "boolean" ? this._debug : !dataType ? false : this._debug.input.some((value) => value === data.type)) this._logDebug("WebSocket event sent: ", data.type, data);
1435
+ const message = typeof data === "string" ? data : JSON.stringify(data);
1436
+ webSocket.send(message);
1437
+ }
1438
+ _setMetadata(metadata) {
1439
+ const parsed = this.metadata ? this.metadata.parse(metadata) : metadata;
1440
+ this._lastMetadata = parsed;
1441
+ return parsed;
1442
+ }
1443
+ #_storage = new Proxy((key, callback) => this._crdtNotifier.subscribe(key, callback), { get: (fn, prop) => {
1444
+ if (!!prop) return (callback) => {
1445
+ return this._crdtNotifier.subcribeRoot(callback);
1446
+ };
1447
+ return (callback) => {
1448
+ return fn(prop, callback);
1449
+ };
1450
+ } });
1451
+ _updateState(updater) {
1452
+ let authorization;
1453
+ try {
1454
+ authorization = JSON.parse(JSON.stringify(this._state.authorization));
1455
+ } catch {
1456
+ throw new Error("User is not JSON serializable");
1457
+ }
1458
+ const newState = updater({
1459
+ ...this._state,
1460
+ authorization,
1461
+ connection: JSON.parse(JSON.stringify(this._state.connection)),
1462
+ storage: JSON.parse(JSON.stringify(this._state.storage)),
1463
+ webSocket: this._state.webSocket
1464
+ });
1465
+ this._state = newState;
1466
+ this._logDebug("WebSocket new state: ", newState);
1467
+ this._stateNotifier.subjects.connection.next(newState);
1468
+ return newState;
1469
+ }
1543
1470
  };
1544
- __storage = new WeakMap();
1545
1471
 
1546
- // src/PluvClient.ts
1472
+ //#endregion
1473
+ //#region src/PluvClient.ts
1547
1474
  var PluvClient = class {
1548
- constructor(options) {
1549
- this._publicKey = null;
1550
- this._rooms = /* @__PURE__ */ new Map();
1551
- this.createRoom = (room, options = {}) => {
1552
- var _a, _b;
1553
- const oldRoom = this.getRoom(room);
1554
- if (oldRoom) return oldRoom;
1555
- const newRoom = new PluvRoom(room, {
1556
- addons: options.addons,
1557
- authEndpoint: this._authEndpoint,
1558
- debug: options.debug,
1559
- initialPresence: options.initialPresence,
1560
- initialStorage: (_a = options.initialStorage) != null ? _a : this._initialStorage,
1561
- limits: this._limits,
1562
- metadata: this.metadata,
1563
- onAuthorizationFail: options.onAuthorizationFail,
1564
- presence: this._presence,
1565
- publicKey: (_b = this._publicKey) != null ? _b : void 0,
1566
- reconnectTimeoutMs: options.reconnectTimeoutMs,
1567
- router: options.router,
1568
- wsEndpoint: this._wsEndpoint
1569
- });
1570
- this._rooms.set(room, newRoom);
1571
- this._logDebug(`New room was created: ${room}`);
1572
- return newRoom;
1573
- };
1574
- this.enter = (room, ...args) => __async(this, null, function* () {
1575
- const toEnter = typeof room === "string" ? this.getRoom(room) : room;
1576
- if (!toEnter) throw new Error(`Could not find room: ${room}.`);
1577
- this._rooms.set(toEnter.id, toEnter);
1578
- yield toEnter.connect(...args);
1579
- this._logDebug(`Entered room: ${room}`);
1580
- return toEnter;
1581
- });
1582
- this.getRoom = (room) => {
1583
- const found = this._rooms.get(room);
1584
- return found != null ? found : null;
1585
- };
1586
- this.getRooms = () => {
1587
- return Array.from(this._rooms.values());
1588
- };
1589
- this.leave = (room) => __async(this, null, function* () {
1590
- const toLeave = typeof room === "string" ? this.getRoom(room) : room;
1591
- if (!toLeave) return;
1592
- yield toLeave.disconnect();
1593
- this._rooms.delete(toLeave.id);
1594
- this._logDebug(`Left and deleted room: ${room}`);
1595
- });
1596
- const {
1597
- authEndpoint,
1598
- debug = false,
1599
- initialStorage,
1600
- limits,
1601
- metadata,
1602
- presence,
1603
- publicKey,
1604
- wsEndpoint
1605
- } = options;
1606
- this.metadata = metadata;
1607
- this._authEndpoint = authEndpoint;
1608
- this._debug = debug;
1609
- this._initialStorage = initialStorage;
1610
- this._limits = __spreadValues({
1611
- presenceMaxSize: MAX_PRESENCE_SIZE_BYTES
1612
- }, limits);
1613
- this._presence = presence;
1614
- this._wsEndpoint = wsEndpoint;
1615
- if (!!publicKey) this._publicKey = publicKey;
1616
- }
1617
- get _defs() {
1618
- return {
1619
- initialStorage: this._initialStorage
1620
- };
1621
- }
1622
- get procedure() {
1623
- return new PluvProcedure();
1624
- }
1625
- router(events) {
1626
- const invalidName = Object.keys(events).find((name) => name.includes("$"));
1627
- if (typeof invalidName === "string") {
1628
- throw new Error(`Invalid event name. Event names must not contain $: "${invalidName}"`);
1629
- }
1630
- return new PluvRouter(events);
1631
- }
1632
- _logDebug(...data) {
1633
- var _a;
1634
- if (typeof process === "undefined") return;
1635
- if (((_a = process.env) == null ? void 0 : _a.NODE_ENV) === "production") return;
1636
- if (this._debug) console.log(...data);
1637
- }
1475
+ get _defs() {
1476
+ return { initialStorage: this._initialStorage };
1477
+ }
1478
+ get procedure() {
1479
+ return new PluvProcedure();
1480
+ }
1481
+ constructor(options) {
1482
+ this._publicKey = null;
1483
+ this._rooms = /* @__PURE__ */ new Map();
1484
+ this.createRoom = (room, options$1 = {}) => {
1485
+ const oldRoom = this.getRoom(room);
1486
+ if (oldRoom) return oldRoom;
1487
+ const newRoom = new PluvRoom(room, {
1488
+ addons: options$1.addons,
1489
+ authEndpoint: this._authEndpoint,
1490
+ debug: options$1.debug,
1491
+ initialPresence: options$1.initialPresence,
1492
+ initialStorage: options$1.initialStorage ?? this._initialStorage,
1493
+ limits: this._limits,
1494
+ metadata: this.metadata,
1495
+ onAuthorizationFail: options$1.onAuthorizationFail,
1496
+ presence: this._presence,
1497
+ publicKey: this._publicKey ?? void 0,
1498
+ reconnectTimeoutMs: options$1.reconnectTimeoutMs,
1499
+ router: options$1.router,
1500
+ wsEndpoint: this._wsEndpoint
1501
+ });
1502
+ this._rooms.set(room, newRoom);
1503
+ this._logDebug(`New room was created: ${room}`);
1504
+ return newRoom;
1505
+ };
1506
+ this.enter = async (room, ...args) => {
1507
+ const toEnter = typeof room === "string" ? this.getRoom(room) : room;
1508
+ if (!toEnter) throw new Error(`Could not find room: ${room}.`);
1509
+ this._rooms.set(toEnter.id, toEnter);
1510
+ await toEnter.connect(...args);
1511
+ this._logDebug(`Entered room: ${room}`);
1512
+ return toEnter;
1513
+ };
1514
+ this.getRoom = (room) => {
1515
+ return this._rooms.get(room) ?? null;
1516
+ };
1517
+ this.getRooms = () => {
1518
+ return Array.from(this._rooms.values());
1519
+ };
1520
+ this.leave = async (room) => {
1521
+ const toLeave = typeof room === "string" ? this.getRoom(room) : room;
1522
+ if (!toLeave) return;
1523
+ await toLeave.disconnect();
1524
+ this._rooms.delete(toLeave.id);
1525
+ this._logDebug(`Left and deleted room: ${room}`);
1526
+ };
1527
+ const { authEndpoint, debug = false, initialStorage, limits, metadata, presence, publicKey, wsEndpoint } = options;
1528
+ this.metadata = metadata;
1529
+ this._authEndpoint = authEndpoint;
1530
+ this._debug = debug;
1531
+ this._initialStorage = initialStorage;
1532
+ this._limits = {
1533
+ presenceMaxSize: MAX_PRESENCE_SIZE_BYTES,
1534
+ ...limits
1535
+ };
1536
+ this._presence = presence;
1537
+ this._wsEndpoint = wsEndpoint;
1538
+ if (!!publicKey) this._publicKey = publicKey;
1539
+ }
1540
+ router(events) {
1541
+ const invalidName = Object.keys(events).find((name) => name.includes("$"));
1542
+ if (typeof invalidName === "string") throw new Error(`Invalid event name. Event names must not contain $: "${invalidName}"`);
1543
+ return new PluvRouter(events);
1544
+ }
1545
+ _logDebug(...data) {
1546
+ if (typeof process === "undefined") return;
1547
+ if (process.env?.NODE_ENV === "production") return;
1548
+ if (this._debug) console.log(...data);
1549
+ }
1638
1550
  };
1639
1551
 
1640
- // src/createClient.ts
1641
- var createClient = (options) => {
1642
- return new PluvClient(options);
1552
+ //#endregion
1553
+ //#region src/createClient.ts
1554
+ const createClient = (options) => {
1555
+ return new PluvClient(options);
1643
1556
  };
1644
1557
 
1645
- // src/infer.ts
1646
- var infer = (callback) => {
1647
- return callback;
1558
+ //#endregion
1559
+ //#region src/infer.ts
1560
+ const infer = (callback) => {
1561
+ return callback;
1648
1562
  };
1649
1563
 
1650
- // src/MockedRoom.ts
1651
- import { ConnectionState as ConnectionState2, StorageState as StorageState2 } from "@pluv/types";
1652
- var __storage2;
1564
+ //#endregion
1565
+ //#region src/MockedRoom.ts
1653
1566
  var MockedRoom = class {
1654
- constructor(room, options) {
1655
- this._crdtNotifier = new CrdtNotifier();
1656
- this._eventNotifier = new EventNotifier();
1657
- this._usersNotifier = new UsersNotifier();
1658
- this._state = {
1659
- authorization: {
1660
- token: null,
1661
- user: null
1662
- },
1663
- connection: {
1664
- attempts: 0,
1665
- id: null,
1666
- state: ConnectionState2.Untouched
1667
- },
1668
- storage: {
1669
- state: StorageState2.Unavailable
1670
- },
1671
- webSocket: null
1672
- };
1673
- this._stateNotifier = new StateNotifier();
1674
- this._subscriptions = {
1675
- observeCrdt: null
1676
- };
1677
- this.broadcast = new Proxy(
1678
- (event, data) => __async(this, null, function* () {
1679
- if (!this._state.webSocket) return;
1680
- if (this._state.connection.state !== ConnectionState2.Open) return;
1681
- const type = event.toString();
1682
- const procedure = this._router._defs.events[type];
1683
- if (!(procedure == null ? void 0 : procedure.config.broadcast)) {
1684
- this._simulateEvent(type, data);
1685
- return;
1686
- }
1687
- const myself = this._usersManager.myself;
1688
- if (!myself) return;
1689
- const parsed = procedure.config.input ? procedure.config.input.parse(data) : data;
1690
- const context = {
1691
- doc: this._crdtManager.doc,
1692
- others: this._usersManager.getOthers(),
1693
- room: this.id,
1694
- user: myself
1695
- };
1696
- const output = yield procedure.config.broadcast(parsed, context);
1697
- Object.entries(output).forEach(([_type, _data]) => {
1698
- this._simulateEvent(_type, _data);
1699
- });
1700
- }),
1701
- {
1702
- get(fn, prop) {
1703
- return (data) => __async(null, null, function* () {
1704
- return yield fn(prop, data);
1705
- });
1706
- }
1707
- }
1708
- );
1709
- this.canRedo = () => {
1710
- return !!this._crdtManager.doc.canRedo();
1711
- };
1712
- this.canUndo = () => {
1713
- return !!this._crdtManager.doc.canUndo();
1714
- };
1715
- this.getConnection = () => {
1716
- return Object.freeze(JSON.parse(JSON.stringify(this._state.connection)));
1717
- };
1718
- this.getMyPresence = () => {
1719
- return this._usersManager.myPresence;
1720
- };
1721
- this.getMyself = () => {
1722
- return this._usersManager.myself;
1723
- };
1724
- this.getOther = (connectionId) => {
1725
- return this._usersManager.getOther(connectionId);
1726
- };
1727
- this.getOthers = () => {
1728
- return this._usersManager.getOthers();
1729
- };
1730
- this.getStorage = (type) => {
1731
- const sharedType = this._crdtManager.get(type);
1732
- if (typeof sharedType === "undefined") return null;
1733
- return sharedType;
1734
- };
1735
- this.redo = () => {
1736
- this._crdtManager.doc.redo();
1737
- };
1738
- this.storageRoot = (fn) => {
1739
- return this._crdtNotifier.subcribeRoot(fn);
1740
- };
1741
- this.subscribe = new Proxy(
1742
- (name, callback) => {
1743
- return this._stateNotifier.subscribe(name, callback);
1744
- },
1745
- {
1746
- get: (fn, prop) => {
1747
- if (prop === "connection") {
1748
- return (callback) => {
1749
- return fn("connection", callback);
1750
- };
1751
- }
1752
- if (prop === "myPresence") {
1753
- return (callback) => {
1754
- return fn("my-presence", callback);
1755
- };
1756
- }
1757
- if (prop === "myself") {
1758
- return (callback) => {
1759
- return fn("myself", callback);
1760
- };
1761
- }
1762
- if (prop === "others") {
1763
- return (callback) => {
1764
- return this._usersNotifier.subscribeOthers(callback);
1765
- };
1766
- }
1767
- if (prop === "storageLoaded") {
1768
- return (callback) => {
1769
- return fn("storage-loaded", callback);
1770
- };
1771
- }
1772
- if (prop === "event") return this._event;
1773
- if (prop === "other") return this._other;
1774
- if (prop === "storage") return __privateGet(this, __storage2);
1775
- }
1776
- }
1777
- );
1778
- this.transact = (fn, origin) => {
1779
- const _origin = origin != null ? origin : this._state.connection.id;
1780
- const crdtManager = this._crdtManager;
1781
- if (typeof _origin !== "string") return;
1782
- if (!crdtManager) return;
1783
- crdtManager.doc.transact(() => {
1784
- const storage = crdtManager.doc.get();
1785
- fn(storage);
1786
- }, _origin);
1787
- };
1788
- this.undo = () => {
1789
- this._crdtManager.doc.undo();
1790
- };
1791
- this.updateMyPresence = (presence) => {
1792
- var _a;
1793
- const newPresence = typeof presence === "function" ? presence(this.getMyPresence()) : presence;
1794
- this._usersManager.updateMyPresence(newPresence);
1795
- const myPresence = this._usersManager.myPresence;
1796
- const myself = (_a = this._usersManager.myself) != null ? _a : null;
1797
- this._stateNotifier.subjects["my-presence"].next(myPresence);
1798
- if (!!myself) this._stateNotifier.subjects["myself"].next(myself);
1799
- };
1800
- this._event = new Proxy(
1801
- (event, callback) => this._eventNotifier.subscribe(event, callback),
1802
- {
1803
- get(fn, prop) {
1804
- return (callback) => fn(prop, callback);
1805
- }
1806
- }
1807
- );
1808
- this._other = (connectionId, callback) => {
1809
- const clientId = this._usersManager.getClientId(connectionId);
1810
- if (!clientId) return () => void 0;
1811
- return this._usersNotifier.subscribeOther(clientId, callback);
1812
- };
1813
- __privateAdd(this, __storage2, new Proxy(
1814
- (key, callback) => this._crdtNotifier.subscribe(key, callback),
1815
- {
1816
- get: (fn, prop) => {
1817
- if (!!prop) {
1818
- return (callback) => {
1819
- return this._crdtNotifier.subcribeRoot(callback);
1820
- };
1821
- }
1822
- return (callback) => {
1823
- return fn(prop, callback);
1824
- };
1825
- }
1826
- }
1827
- ));
1828
- const { events, initialPresence, initialStorage, limits, presence, router } = options;
1829
- this.id = room;
1830
- this._events = events;
1831
- this._limits = __spreadValues({
1832
- presenceMaxSize: MAX_PRESENCE_SIZE_BYTES
1833
- }, limits);
1834
- this._router = router != null ? router : new PluvRouter({});
1835
- this._usersManager = new UsersManager({
1836
- initialPresence,
1837
- limits: this._limits,
1838
- presence
1839
- });
1840
- this._crdtManager = new CrdtManager({
1841
- initialStorage
1842
- });
1843
- this._observeCrdt();
1844
- }
1845
- addEventListener(kind, handler) {
1846
- return () => void 0;
1847
- }
1848
- getDoc() {
1849
- return this._crdtManager.doc;
1850
- }
1851
- getStorageJson(type) {
1852
- if (this._state.connection.id === null) return null;
1853
- if (typeof type === "undefined") return this._crdtManager.doc.toJson();
1854
- return this._crdtManager.doc.toJson(type);
1855
- }
1856
- getStorageLoaded() {
1857
- return true;
1858
- }
1859
- _observeCrdt() {
1860
- var _a, _b;
1861
- (_b = (_a = this._subscriptions).observeCrdt) == null ? void 0 : _b.call(_a);
1862
- if (!this._crdtManager) return;
1863
- const unsubscribe = this._crdtManager.doc.subscribe((event) => {
1864
- var _a2;
1865
- const origin = (_a2 = event.origin) != null ? _a2 : null;
1866
- if (!this._crdtManager) return;
1867
- if (origin === "$storageUpdated") return;
1868
- const sharedTypes = this._crdtManager.doc.get();
1869
- const storageRoot = Object.keys(sharedTypes).reduce(
1870
- (acc, prop) => {
1871
- if (!this._crdtManager) return acc;
1872
- const serialized = this._crdtManager.doc.toJson(prop);
1873
- this._crdtNotifier.subject(prop).next(serialized);
1874
- return __spreadProps(__spreadValues({}, acc), { [prop]: serialized });
1875
- },
1876
- {}
1877
- );
1878
- this._crdtNotifier.rootSubject.next(storageRoot);
1879
- });
1880
- this._subscriptions.observeCrdt = unsubscribe;
1881
- }
1882
- _simulateEvent(event, data) {
1883
- if (!this._events) return;
1884
- const type = event.toString();
1885
- const resolver = this._events[type];
1886
- if (!resolver) return;
1887
- const result = resolver(data);
1888
- Object.keys(result).forEach((type2) => {
1889
- const _type = type2.toString();
1890
- const data2 = result[_type];
1891
- if (!data2) return;
1892
- this._eventNotifier.subject(_type).next(data2);
1893
- });
1894
- }
1567
+ constructor(room, options) {
1568
+ this._crdtNotifier = new CrdtNotifier();
1569
+ this._eventNotifier = new EventNotifier();
1570
+ this._usersNotifier = new UsersNotifier();
1571
+ this._state = {
1572
+ authorization: {
1573
+ token: null,
1574
+ user: null
1575
+ },
1576
+ connection: {
1577
+ attempts: 0,
1578
+ id: null,
1579
+ state: ConnectionState$1.Untouched
1580
+ },
1581
+ storage: { state: StorageState.Unavailable },
1582
+ webSocket: null
1583
+ };
1584
+ this._stateNotifier = new StateNotifier();
1585
+ this._subscriptions = { observeCrdt: null };
1586
+ this.broadcast = new Proxy(async (event, data) => {
1587
+ if (!this._state.webSocket) return;
1588
+ if (this._state.connection.state !== ConnectionState$1.Open) return;
1589
+ const type = event.toString();
1590
+ const procedure = this._router._defs.events[type];
1591
+ if (!procedure?.config.broadcast) {
1592
+ this._simulateEvent(type, data);
1593
+ return;
1594
+ }
1595
+ const myself = this._usersManager.myself;
1596
+ if (!myself) return;
1597
+ const parsed = procedure.config.input ? procedure.config.input.parse(data) : data;
1598
+ const context = {
1599
+ doc: this._crdtManager.doc,
1600
+ others: this._usersManager.getOthers(),
1601
+ room: this.id,
1602
+ user: myself
1603
+ };
1604
+ const output = await procedure.config.broadcast(parsed, context);
1605
+ Object.entries(output).forEach(([_type, _data]) => {
1606
+ this._simulateEvent(_type, _data);
1607
+ });
1608
+ }, { get(fn, prop) {
1609
+ return async (data) => {
1610
+ return await fn(prop, data);
1611
+ };
1612
+ } });
1613
+ this.canRedo = () => {
1614
+ return !!this._crdtManager.doc.canRedo();
1615
+ };
1616
+ this.canUndo = () => {
1617
+ return !!this._crdtManager.doc.canUndo();
1618
+ };
1619
+ this.getConnection = () => {
1620
+ return Object.freeze(JSON.parse(JSON.stringify(this._state.connection)));
1621
+ };
1622
+ this.getMyPresence = () => {
1623
+ return this._usersManager.myPresence;
1624
+ };
1625
+ this.getMyself = () => {
1626
+ return this._usersManager.myself;
1627
+ };
1628
+ this.getOther = (connectionId) => {
1629
+ return this._usersManager.getOther(connectionId);
1630
+ };
1631
+ this.getOthers = () => {
1632
+ return this._usersManager.getOthers();
1633
+ };
1634
+ this.getStorage = (type) => {
1635
+ const sharedType = this._crdtManager.get(type);
1636
+ if (typeof sharedType === "undefined") return null;
1637
+ return sharedType;
1638
+ };
1639
+ this.redo = () => {
1640
+ this._crdtManager.doc.redo();
1641
+ };
1642
+ this.storageRoot = (fn) => {
1643
+ return this._crdtNotifier.subcribeRoot(fn);
1644
+ };
1645
+ this.subscribe = new Proxy((name, callback) => {
1646
+ return this._stateNotifier.subscribe(name, callback);
1647
+ }, { get: (fn, prop) => {
1648
+ if (prop === "connection") return (callback) => {
1649
+ return fn("connection", callback);
1650
+ };
1651
+ if (prop === "myPresence") return (callback) => {
1652
+ return fn("my-presence", callback);
1653
+ };
1654
+ if (prop === "myself") return (callback) => {
1655
+ return fn("myself", callback);
1656
+ };
1657
+ if (prop === "others") return (callback) => {
1658
+ return this._usersNotifier.subscribeOthers(callback);
1659
+ };
1660
+ if (prop === "storageLoaded") return (callback) => {
1661
+ return fn("storage-loaded", callback);
1662
+ };
1663
+ if (prop === "event") return this._event;
1664
+ if (prop === "other") return this._other;
1665
+ if (prop === "storage") return this.#_storage;
1666
+ } });
1667
+ this.transact = (fn, origin) => {
1668
+ const _origin = origin ?? this._state.connection.id;
1669
+ const crdtManager = this._crdtManager;
1670
+ /**
1671
+ * !HACK
1672
+ * @description Don't transact anything if there is no origin, because
1673
+ * that means the user isn't connected yet. This will mean events are
1674
+ * lost unfortunately.
1675
+ * @date September 23, 2023
1676
+ */
1677
+ if (typeof _origin !== "string") return;
1678
+ if (!crdtManager) return;
1679
+ crdtManager.doc.transact(() => {
1680
+ fn(crdtManager.doc.get());
1681
+ }, _origin);
1682
+ };
1683
+ this.undo = () => {
1684
+ this._crdtManager.doc.undo();
1685
+ };
1686
+ this.updateMyPresence = (presence$1) => {
1687
+ const newPresence = typeof presence$1 === "function" ? presence$1(this.getMyPresence()) : presence$1;
1688
+ this._usersManager.updateMyPresence(newPresence);
1689
+ const myPresence = this._usersManager.myPresence;
1690
+ const myself = this._usersManager.myself ?? null;
1691
+ this._stateNotifier.subjects["my-presence"].next(myPresence);
1692
+ if (!!myself) this._stateNotifier.subjects["myself"].next(myself);
1693
+ };
1694
+ this._event = new Proxy((event, callback) => this._eventNotifier.subscribe(event, callback), { get(fn, prop) {
1695
+ return (callback) => fn(prop, callback);
1696
+ } });
1697
+ this._other = (connectionId, callback) => {
1698
+ const clientId = this._usersManager.getClientId(connectionId);
1699
+ if (!clientId) return () => void 0;
1700
+ return this._usersNotifier.subscribeOther(clientId, callback);
1701
+ };
1702
+ const { events, initialPresence, initialStorage, limits, presence, router } = options;
1703
+ this.id = room;
1704
+ this._events = events;
1705
+ this._limits = {
1706
+ presenceMaxSize: MAX_PRESENCE_SIZE_BYTES,
1707
+ ...limits
1708
+ };
1709
+ this._router = router ?? new PluvRouter({});
1710
+ this._usersManager = new UsersManager({
1711
+ initialPresence,
1712
+ limits: this._limits,
1713
+ presence
1714
+ });
1715
+ this._crdtManager = new CrdtManager({ initialStorage });
1716
+ this._observeCrdt();
1717
+ }
1718
+ addEventListener(kind, handler) {
1719
+ return () => void 0;
1720
+ }
1721
+ getDoc() {
1722
+ return this._crdtManager.doc;
1723
+ }
1724
+ getStorageJson(type) {
1725
+ if (this._state.connection.id === null) return null;
1726
+ if (typeof type === "undefined") return this._crdtManager.doc.toJson();
1727
+ return this._crdtManager.doc.toJson(type);
1728
+ }
1729
+ getStorageLoaded() {
1730
+ return true;
1731
+ }
1732
+ _observeCrdt() {
1733
+ this._subscriptions.observeCrdt?.();
1734
+ if (!this._crdtManager) return;
1735
+ const unsubscribe = this._crdtManager.doc.subscribe((event) => {
1736
+ const origin = event.origin ?? null;
1737
+ if (!this._crdtManager) return;
1738
+ if (origin === "$storageUpdated") return;
1739
+ const sharedTypes = this._crdtManager.doc.get();
1740
+ const storageRoot = Object.keys(sharedTypes).reduce((acc, prop) => {
1741
+ if (!this._crdtManager) return acc;
1742
+ const serialized = this._crdtManager.doc.toJson(prop);
1743
+ this._crdtNotifier.subject(prop).next(serialized);
1744
+ return {
1745
+ ...acc,
1746
+ [prop]: serialized
1747
+ };
1748
+ }, {});
1749
+ this._crdtNotifier.rootSubject.next(storageRoot);
1750
+ });
1751
+ this._subscriptions.observeCrdt = unsubscribe;
1752
+ }
1753
+ _simulateEvent(event, data) {
1754
+ if (!this._events) return;
1755
+ const type = event.toString();
1756
+ const resolver = this._events[type];
1757
+ if (!resolver) return;
1758
+ const result = resolver(data);
1759
+ Object.keys(result).forEach((type$1) => {
1760
+ const _type = type$1.toString();
1761
+ const data$1 = result[_type];
1762
+ if (!data$1) return;
1763
+ this._eventNotifier.subject(_type).next(data$1);
1764
+ });
1765
+ }
1766
+ #_storage = new Proxy((key, callback) => this._crdtNotifier.subscribe(key, callback), { get: (fn, prop) => {
1767
+ if (!!prop) return (callback) => {
1768
+ return this._crdtNotifier.subcribeRoot(callback);
1769
+ };
1770
+ return (callback) => {
1771
+ return fn(prop, callback);
1772
+ };
1773
+ } });
1895
1774
  };
1896
- __storage2 = new WeakMap();
1897
1775
 
1898
- // src/register.ts
1899
- var register = (params) => {
1900
- const { authEndpoint, publicKey } = params;
1901
- const wsEndpoint = ({ room }) => `wss://connect.pluv.io/api/room?room=${room}`;
1902
- return {
1903
- authEndpoint,
1904
- publicKey,
1905
- wsEndpoint
1906
- };
1907
- };
1908
- export {
1909
- AbstractStorageStore,
1910
- ConnectionState3 as ConnectionState,
1911
- MockedRoom,
1912
- PluvClient,
1913
- PluvRoom,
1914
- createClient,
1915
- infer,
1916
- register
1776
+ //#endregion
1777
+ //#region src/register.ts
1778
+ const register = (params) => {
1779
+ const { authEndpoint, publicKey } = params;
1780
+ const wsEndpoint = ({ room }) => `wss://connect.pluv.io/api/room?room=${room}`;
1781
+ return {
1782
+ authEndpoint,
1783
+ publicKey,
1784
+ wsEndpoint
1785
+ };
1917
1786
  };
1787
+
1788
+ //#endregion
1789
+ export { AbstractStorageStore, ConnectionState, MockedRoom, PluvClient, PluvRoom, createClient, infer, register };
1790
+ //# sourceMappingURL=index.mjs.map