@signe/room 0.0.1 → 0.0.2

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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/decorators.ts
2
- function action(name, bodyValidation) {
2
+ function Action(name, bodyValidation) {
3
3
  return function(target, propertyKey) {
4
4
  if (!target.constructor._actionMetadata) {
5
5
  target.constructor._actionMetadata = /* @__PURE__ */ new Map();
@@ -15,23 +15,261 @@ function Room(options) {
15
15
  target.path = options.path;
16
16
  target.maxUsers = options.maxUsers;
17
17
  target.throttleStorage = options.throttleStorage;
18
+ target.throttleSync = options.throttleSync;
18
19
  };
19
20
  }
20
21
 
22
+ // ../sync/src/utils.ts
23
+ function isClass(obj) {
24
+ return typeof obj === "function" && obj.prototype && obj.prototype.constructor === obj;
25
+ }
26
+ var isObject = (item) => item && typeof item === "object" && !Array.isArray(item) && item !== null;
27
+ function generateShortUUID() {
28
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
29
+ let uuid = "";
30
+ for (let i = 0; i < 8; i++) {
31
+ const randomIndex = Math.floor(Math.random() * chars.length);
32
+ uuid += chars[randomIndex];
33
+ }
34
+ return uuid;
35
+ }
36
+
37
+ // src/mock.ts
38
+ var MockPartySocket = class {
39
+ constructor() {
40
+ this.events = /* @__PURE__ */ new Map();
41
+ this.id = generateShortUUID();
42
+ }
43
+ addEventListener(event, cb) {
44
+ this.events.set(event, cb);
45
+ }
46
+ removeEventListener(event, cb) {
47
+ this.events.delete(event);
48
+ }
49
+ _trigger(event, data) {
50
+ this.events.get(event)?.(data);
51
+ }
52
+ };
53
+ var MockStorage = class {
54
+ constructor() {
55
+ this.storage = /* @__PURE__ */ new Map();
56
+ }
57
+ async get(key) {
58
+ return this.storage.get(key);
59
+ }
60
+ async put(key, value) {
61
+ this.storage.set(key, value);
62
+ }
63
+ async list() {
64
+ return this.storage;
65
+ }
66
+ };
67
+ var MockPartyRoom = class {
68
+ constructor(id) {
69
+ this.id = id;
70
+ this.clients = /* @__PURE__ */ new Map();
71
+ this.storage = new MockStorage();
72
+ this.id = id || generateShortUUID();
73
+ }
74
+ connection(client) {
75
+ const socket = new MockPartySocket();
76
+ this.clients.set(socket.id, client);
77
+ client.id = socket.id;
78
+ }
79
+ broadcast(data) {
80
+ this.clients.forEach((client) => {
81
+ client._trigger("message", data);
82
+ });
83
+ }
84
+ clear() {
85
+ this.clients.clear();
86
+ }
87
+ };
88
+ var MockConnection = class {
89
+ constructor() {
90
+ this.state = {};
91
+ }
92
+ setState(value) {
93
+ this.state = value;
94
+ }
95
+ };
96
+ var ServerIo = MockPartyRoom;
97
+ var ClientIo = MockPartySocket;
98
+
21
99
  // src/server.ts
22
- import { createStatesSnapshot, getByPath, load, syncClass } from "@signe/sync";
23
100
  import { dset as dset2 } from "dset";
24
101
  import z from "zod";
25
102
 
103
+ // ../sync/src/core.ts
104
+ import {
105
+ ArraySubject,
106
+ ObjectSubject,
107
+ isSignal
108
+ } from "@signe/reactive";
109
+ var syncClass = (instance, options = {}) => {
110
+ const cacheSync = /* @__PURE__ */ new Map();
111
+ const cachePersist = /* @__PURE__ */ new Set();
112
+ instance.$valuesChanges = {
113
+ set: (path, value) => {
114
+ cacheSync.set(path, value);
115
+ options.onSync?.(cacheSync);
116
+ },
117
+ setPersist: (path) => {
118
+ if (path == "") path = ".";
119
+ cachePersist.add(path);
120
+ options.onPersist?.(cachePersist);
121
+ },
122
+ has: (path) => {
123
+ return cacheSync.has(path);
124
+ },
125
+ get: (path) => {
126
+ return cacheSync.get(path);
127
+ }
128
+ };
129
+ createSyncClass(instance);
130
+ };
131
+ function createStatesSnapshot(instance) {
132
+ let persistObject = {};
133
+ if (instance.$snapshot) {
134
+ for (const key of instance.$snapshot.keys()) {
135
+ const signal = instance.$snapshot.get(key);
136
+ const persist = signal.options.persist ?? true;
137
+ let value = signal();
138
+ if (isObject(value) || Array.isArray(value)) {
139
+ break;
140
+ }
141
+ if (persist) {
142
+ persistObject[key] = value;
143
+ }
144
+ }
145
+ }
146
+ return persistObject;
147
+ }
148
+ function setMetadata(target, key, value) {
149
+ const meta = target.constructor._propertyMetadata;
150
+ const propId = meta?.get(key);
151
+ if (propId) {
152
+ if (isSignal(target[propId])) {
153
+ target[propId].set(value);
154
+ } else {
155
+ target[propId] = value;
156
+ }
157
+ }
158
+ }
159
+ var createSyncClass = (currentClass, parentKey = null, parentClass = null, path = "") => {
160
+ currentClass.$path = path;
161
+ if (parentClass) {
162
+ currentClass.$valuesChanges = parentClass.$valuesChanges;
163
+ }
164
+ if (parentKey) {
165
+ setMetadata(currentClass, "id", parentKey);
166
+ }
167
+ if (currentClass.$snapshot) {
168
+ for (const key of currentClass.$snapshot.keys()) {
169
+ const signal = currentClass.$snapshot.get(key);
170
+ const syncToClient = signal.options.syncToClient ?? true;
171
+ const persist = signal.options.persist ?? true;
172
+ let value = signal();
173
+ if (isObject(value) || Array.isArray(value)) {
174
+ value = { ...value };
175
+ }
176
+ const newPath = (path ? path + "." : "") + key;
177
+ if (syncToClient) {
178
+ currentClass.$valuesChanges.set(newPath, value);
179
+ }
180
+ if (persist) {
181
+ if (parentClass) currentClass.$valuesChanges.setPersist(path);
182
+ }
183
+ }
184
+ }
185
+ };
186
+
187
+ // ../sync/src/load.ts
188
+ import { isSignal as isSignal2 } from "@signe/reactive";
189
+ function load(rootInstance, values, valueIsObject) {
190
+ if (valueIsObject) {
191
+ loadFromObject(rootInstance, values);
192
+ } else {
193
+ loadFromPaths(rootInstance, values);
194
+ }
195
+ }
196
+ function loadFromPaths(rootInstance, values) {
197
+ for (const [path, value] of Object.entries(values)) {
198
+ const parts = path.split(".");
199
+ loadValue(rootInstance, parts, value);
200
+ }
201
+ }
202
+ function loadFromObject(rootInstance, values, currentPath = "") {
203
+ for (const [key, value] of Object.entries(values)) {
204
+ const newPath = currentPath ? `${currentPath}.${key}` : key;
205
+ if (typeof value === "object" && !Array.isArray(value)) {
206
+ loadFromObject(rootInstance, value, newPath);
207
+ } else {
208
+ const parts = newPath.split(".");
209
+ loadValue(rootInstance, parts, value);
210
+ }
211
+ }
212
+ }
213
+ function loadValue(rootInstance, parts, value) {
214
+ let current = rootInstance;
215
+ for (let i = 0; i < parts.length; i++) {
216
+ const part = parts[i];
217
+ if (i === parts.length - 1) {
218
+ if (value == "$delete") {
219
+ if (isSignal2(current)) {
220
+ current = current();
221
+ }
222
+ Reflect.deleteProperty(current, part);
223
+ } else if (current[part]?._subject) {
224
+ current[part].set(value);
225
+ }
226
+ } else {
227
+ if (isSignal2(current)) {
228
+ current = current();
229
+ }
230
+ const currentValue = current[part];
231
+ if (currentValue === void 0) {
232
+ const parentInstance = getByPath(
233
+ rootInstance,
234
+ parts.slice(0, i).join(".")
235
+ );
236
+ const classType = parentInstance?.options?.classType;
237
+ if (classType) {
238
+ current[part] = !isClass(classType) ? classType(part) : new classType();
239
+ setMetadata(current[part], "id", part);
240
+ } else {
241
+ current[part] = {};
242
+ }
243
+ }
244
+ current = current[part];
245
+ }
246
+ }
247
+ }
248
+ function getByPath(root, path) {
249
+ const parts = path.split(".");
250
+ let current = root;
251
+ for (const part of parts) {
252
+ if (isSignal2(current)) {
253
+ current = current();
254
+ }
255
+ if (current[part]) {
256
+ current = current[part];
257
+ } else {
258
+ return void 0;
259
+ }
260
+ }
261
+ return current;
262
+ }
263
+
26
264
  // src/utils.ts
27
265
  import { dset } from "dset";
28
266
  function isPromise(value) {
29
- return value && value instanceof Promise;
267
+ return value instanceof Promise;
30
268
  }
31
269
  async function awaitReturn(val) {
32
270
  return isPromise(val) ? await val : val;
33
271
  }
34
- function isClass(obj) {
272
+ function isClass2(obj) {
35
273
  return typeof obj === "function" && obj.prototype && obj.prototype.constructor === obj;
36
274
  }
37
275
  function throttle(func, wait) {
@@ -58,18 +296,25 @@ function extractParams(pattern, str) {
58
296
  const match = regex.exec(str);
59
297
  if (match && match.groups) {
60
298
  return match.groups;
299
+ } else if (pattern === str) {
300
+ return {};
61
301
  } else {
62
302
  return null;
63
303
  }
64
304
  }
65
305
  function dremove(obj, keys) {
66
- keys.split && (keys = keys.split("."));
67
- var i = 0, l = keys.length, t = { ...obj }, k;
306
+ if (typeof keys === "string") {
307
+ keys = keys.split(".");
308
+ }
309
+ let i = 0;
310
+ const l = keys.length;
311
+ let t = obj;
312
+ let k;
68
313
  while (i < l - 1) {
69
314
  k = keys[i++];
70
315
  if (k === "__proto__" || k === "constructor" || k === "prototype") return;
316
+ if (typeof t[k] !== "object" || t[k] === null) return;
71
317
  t = t[k];
72
- if (typeof t !== "object" || t === null) return;
73
318
  }
74
319
  k = keys[i];
75
320
  if (t && typeof t === "object" && !(k === "__proto__" || k === "constructor" || k === "prototype")) {
@@ -81,8 +326,8 @@ function buildObject(valuesMap, allMemory) {
81
326
  for (let path of valuesMap.keys()) {
82
327
  const value = valuesMap.get(path);
83
328
  dset(memoryObj, path, value);
84
- if (path == "$delete") {
85
- dremove(allMemory, value);
329
+ if (value === "$delete") {
330
+ dremove(allMemory, path);
86
331
  } else {
87
332
  dset(allMemory, path, value);
88
333
  }
@@ -96,19 +341,81 @@ var Message = z.object({
96
341
  value: z.any()
97
342
  });
98
343
  var Server = class {
344
+ /**
345
+ * @constructor
346
+ * @param {Party.Room} room - The room object representing the current game or application instance.
347
+ *
348
+ * @example
349
+ * ```typescript
350
+ * const server = new MyServer(new ServerIo("game"));
351
+ * ```
352
+ */
99
353
  constructor(room) {
100
354
  this.room = room;
101
- this.memoryAll = {};
102
- this.subRoom = {};
355
+ this.subRoom = null;
103
356
  this.rooms = [];
104
- for (let room2 of this.rooms) {
105
- const params = extractParams(room2.path, this.room.id);
357
+ }
358
+ /**
359
+ * @readonly
360
+ * @property {boolean} isHibernate - Indicates whether the server is in hibernate mode.
361
+ *
362
+ * @example
363
+ * ```typescript
364
+ * if (!server.isHibernate) {
365
+ * console.log("Server is active");
366
+ * }
367
+ * ```
368
+ */
369
+ get isHibernate() {
370
+ return !!this["options"]?.hibernate;
371
+ }
372
+ /**
373
+ * @method onStart
374
+ * @async
375
+ * @description Initializes the server and creates the initial room if not in hibernate mode.
376
+ * @returns {Promise<void>}
377
+ *
378
+ * @example
379
+ * ```typescript
380
+ * async function initServer() {
381
+ * await server.onStart();
382
+ * console.log("Server started");
383
+ * }
384
+ * ```
385
+ */
386
+ async onStart() {
387
+ if (!this.isHibernate) {
388
+ this.subRoom = await this.createRoom();
389
+ }
390
+ }
391
+ /**
392
+ * @method createRoom
393
+ * @private
394
+ * @async
395
+ * @param {CreateRoomOptions} [options={}] - Options for creating the room.
396
+ * @returns {Promise<Object>} The created room instance.
397
+ * @throws {Error} If no matching room is found.
398
+ *
399
+ * @example
400
+ * ```typescript
401
+ * // This method is private and called internally
402
+ * async function internalCreateRoom() {
403
+ * const room = await this.createRoom({ getMemoryAll: true });
404
+ * console.log("Room created:", room);
405
+ * }
406
+ * ```
407
+ */
408
+ async createRoom(options = {}) {
409
+ let instance;
410
+ let init = true;
411
+ for (let room of this.rooms) {
412
+ const params = extractParams(room.path, this.room.id);
106
413
  if (params) {
107
- this.subRoom = new room2(this.room, params);
414
+ instance = new room(this.room, params);
108
415
  break;
109
416
  }
110
417
  }
111
- if (!this.subRoom) {
418
+ if (!instance) {
112
419
  throw new Error("Room not found");
113
420
  }
114
421
  const loadMemory = async () => {
@@ -121,75 +428,160 @@ var Server = class {
121
428
  }
122
429
  dset2(tmpObject, key, value);
123
430
  }
124
- load(this, tmpObject);
431
+ load(instance, tmpObject);
125
432
  };
126
- loadMemory();
127
- syncClass(this.subRoom, {
128
- onSync: throttle((values) => {
129
- const packet = buildObject(values, this.memoryAll);
130
- this.room.broadcast(
131
- JSON.stringify({
132
- type: "sync",
133
- value: packet
134
- })
135
- );
136
- values.clear();
137
- }, 500),
138
- onPersist: throttle(async (values) => {
139
- for (let path of values) {
140
- const instance = path == "." ? this.subRoom : getByPath(this.subRoom, path);
141
- const itemValue = createStatesSnapshot(instance);
142
- await this.room.storage.put(path, itemValue);
143
- }
144
- values.clear();
145
- }, this.subRoom["throttleStorage"] ?? 2e3)
433
+ await loadMemory();
434
+ instance.$memoryAll = {};
435
+ const syncCb = (values) => {
436
+ if (options.getMemoryAll) {
437
+ buildObject(values, instance.$memoryAll);
438
+ }
439
+ if (init && this.isHibernate) {
440
+ init = false;
441
+ return;
442
+ }
443
+ const packet = buildObject(values, instance.$memoryAll);
444
+ this.room.broadcast(
445
+ JSON.stringify({
446
+ type: "sync",
447
+ value: packet
448
+ })
449
+ );
450
+ values.clear();
451
+ };
452
+ const persistCb = async (values) => {
453
+ for (let path of values) {
454
+ const _instance = path == "." ? instance : getByPath(instance, path);
455
+ const itemValue = createStatesSnapshot(_instance);
456
+ await this.room.storage.put(path, itemValue);
457
+ }
458
+ values.clear();
459
+ };
460
+ syncClass(instance, {
461
+ onSync: throttle(syncCb, instance["throttleSync"] ?? 500),
462
+ onPersist: throttle(persistCb, instance["throttleStorage"] ?? 2e3)
146
463
  });
464
+ return instance;
147
465
  }
148
- static async onBeforeConnect(request) {
149
- try {
150
- request.headers.set("X-User-ID", "" + Math.random());
151
- return request;
152
- } catch (e) {
153
- return new Response("Unauthorized", { status: 401 });
466
+ /**
467
+ * @method getSubRoom
468
+ * @private
469
+ * @async
470
+ * @param {Object} [options={}] - Options for getting the sub-room.
471
+ * @returns {Promise<Object>} The sub-room instance.
472
+ *
473
+ * @example
474
+ * ```typescript
475
+ * // This method is private and called internally
476
+ * async function internalGetSubRoom() {
477
+ * const subRoom = await this.getSubRoom();
478
+ * console.log("Sub-room retrieved:", subRoom);
479
+ * }
480
+ * ```
481
+ */
482
+ async getSubRoom(options = {}) {
483
+ let subRoom;
484
+ if (this.isHibernate) {
485
+ subRoom = await this.createRoom(options);
486
+ } else {
487
+ subRoom = this.subRoom;
154
488
  }
489
+ return subRoom;
155
490
  }
156
- getUsersProperty() {
157
- const meta = this.subRoom.constructor["_propertyMetadata"];
491
+ /**
492
+ * @method getUsersProperty
493
+ * @private
494
+ * @param {Object} subRoom - The sub-room instance.
495
+ * @returns {Object|null} The users property of the sub-room, or null if not found.
496
+ *
497
+ * @example
498
+ * ```typescript
499
+ * // This method is private and called internally
500
+ * function internalGetUsers(subRoom) {
501
+ * const users = this.getUsersProperty(subRoom);
502
+ * console.log("Users:", users);
503
+ * }
504
+ * ```
505
+ */
506
+ getUsersProperty(subRoom) {
507
+ const meta = subRoom.constructor["_propertyMetadata"];
158
508
  const propId = meta?.get("users");
159
509
  if (propId) {
160
- return this.subRoom[propId];
510
+ return subRoom[propId];
161
511
  }
162
512
  return null;
163
513
  }
514
+ /**
515
+ * @method onConnect
516
+ * @async
517
+ * @param {Party.Connection} conn - The connection object for the new user.
518
+ * @param {Party.ConnectionContext} ctx - The context of the connection.
519
+ * @description Handles a new user connection, creates a user object, and sends initial sync data.
520
+ * @returns {Promise<void>}
521
+ *
522
+ * @example
523
+ * ```typescript
524
+ * server.onConnect = async (conn, ctx) => {
525
+ * await server.onConnect(conn, ctx);
526
+ * console.log("New user connected:", conn.id);
527
+ * };
528
+ * ```
529
+ */
164
530
  async onConnect(conn, ctx) {
165
- const publicId = "a" + ("" + Math.random()).split(".")[1];
531
+ const subRoom = await this.getSubRoom({
532
+ getMemoryAll: true
533
+ });
534
+ const publicId = generateShortUUID();
166
535
  let user = null;
167
- const signal = this.getUsersProperty();
536
+ const signal = this.getUsersProperty(subRoom);
168
537
  if (signal) {
169
538
  const { classType } = signal.options;
170
- user = isClass(classType) ? new classType() : classType(conn, ctx);
539
+ user = isClass2(classType) ? new classType() : classType(conn, ctx);
171
540
  signal()[publicId] = user;
172
541
  }
173
- await awaitReturn(this.subRoom["onJoin"]?.(user, conn, ctx));
542
+ await awaitReturn(subRoom["onJoin"]?.(user, conn, ctx));
174
543
  conn.setState({ publicId });
175
544
  conn.send(
176
545
  JSON.stringify({
177
546
  type: "sync",
178
547
  value: {
179
548
  pId: publicId,
180
- ...this.memoryAll
549
+ ...subRoom.$memoryAll
181
550
  }
182
551
  })
183
552
  );
184
553
  }
554
+ /**
555
+ * @method onMessage
556
+ * @async
557
+ * @param {string} message - The message received from a user.
558
+ * @param {Party.Connection} sender - The connection object of the sender.
559
+ * @description Processes incoming messages and triggers corresponding actions in the sub-room.
560
+ * @returns {Promise<void>}
561
+ *
562
+ * @example
563
+ * ```typescript
564
+ * server.onMessage = async (message, sender) => {
565
+ * await server.onMessage(message, sender);
566
+ * console.log("Message processed from:", sender.id);
567
+ * };
568
+ * ```
569
+ */
185
570
  async onMessage(message, sender) {
186
- const actions = this.subRoom.constructor["_actionMetadata"];
187
- const result = Message.safeParse(JSON.parse(message));
571
+ let json;
572
+ try {
573
+ json = JSON.parse(message);
574
+ } catch (e) {
575
+ return;
576
+ }
577
+ const result = Message.safeParse(json);
188
578
  if (!result.success) {
189
579
  return;
190
580
  }
581
+ const subRoom = await this.getSubRoom();
582
+ const actions = subRoom.constructor["_actionMetadata"];
191
583
  if (actions) {
192
- const signal = this.getUsersProperty();
584
+ const signal = this.getUsersProperty(subRoom);
193
585
  const { publicId } = sender.state;
194
586
  const user = signal?.()[publicId];
195
587
  const actionName = actions.get(result.data.action);
@@ -203,24 +595,43 @@ var Server = class {
203
595
  }
204
596
  }
205
597
  await awaitReturn(
206
- this.subRoom[actionName.key](user, result.data.value, sender)
598
+ subRoom[actionName.key](user, result.data.value, sender)
207
599
  );
208
600
  }
209
601
  }
210
602
  }
603
+ /**
604
+ * @method onClose
605
+ * @async
606
+ * @param {Party.Connection} conn - The connection object of the disconnecting user.
607
+ * @description Handles user disconnection, removing them from the room and triggering the onLeave event.
608
+ * @returns {Promise<void>}
609
+ *
610
+ * @example
611
+ * ```typescript
612
+ * server.onClose = async (conn) => {
613
+ * await server.onClose(conn);
614
+ * console.log("User disconnected:", conn.id);
615
+ * };
616
+ * ```
617
+ */
211
618
  async onClose(conn) {
212
- const signal = this.getUsersProperty();
619
+ const subRoom = await this.getSubRoom();
620
+ const signal = this.getUsersProperty(subRoom);
213
621
  const { publicId } = conn.state;
214
622
  const user = signal?.()[publicId];
215
- await awaitReturn(this.subRoom["onLeave"]?.(user, conn));
623
+ await awaitReturn(subRoom["onLeave"]?.(user, conn));
216
624
  if (signal) {
217
625
  delete signal()[publicId];
218
626
  }
219
627
  }
220
628
  };
221
629
  export {
630
+ Action,
631
+ ClientIo,
632
+ MockConnection,
222
633
  Room,
223
634
  Server,
224
- action
635
+ ServerIo
225
636
  };
226
637
  //# sourceMappingURL=index.js.map