@pluv/io 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @pluv/io@0.1.0 build /home/runner/work/pluv/pluv/packages/io
2
+ > @pluv/io@0.2.1 build /home/runner/work/pluv/pluv/packages/io
3
3
  > tsup src/index.ts --format esm,cjs --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -8,10 +8,10 @@
8
8
  CLI Target: es6
9
9
  ESM Build start
10
10
  CJS Build start
11
- ESM dist/index.mjs 25.31 KB
12
- ESM ⚡️ Build success in 107ms
13
- CJS dist/index.js 27.11 KB
14
- CJS ⚡️ Build success in 119ms
11
+ CJS dist/index.js 28.59 KB
12
+ CJS ⚡️ Build success in 95ms
13
+ ESM dist/index.mjs 26.79 KB
14
+ ESM ⚡️ Build success in 96ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 3107ms
17
- DTS dist/index.d.ts 12.29 KB
16
+ DTS ⚡️ Build success in 2778ms
17
+ DTS dist/index.d.ts 12.86 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @pluv/io
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - b45d642: Fixed links on README
8
+ - 203dfee: Updated README
9
+ - Updated dependencies [b45d642]
10
+ - @pluv/crdt-yjs@0.1.2
11
+
12
+ ## 0.2.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 23a7382: Added `initialStorage` param to `createIO` to set initialStorage state of room.
17
+ - 39271d4: Added support for IORoom listeners to get current storage state.
18
+
19
+ ### Patch Changes
20
+
21
+ - 24016e6: Updated dependencies
22
+ - Updated dependencies [24016e6]
23
+ - @pluv/crdt-yjs@0.1.1
24
+ - @pluv/types@0.1.1
25
+
3
26
  ## 0.1.0
4
27
 
5
28
  ### Minor Changes
package/README.md CHANGED
@@ -39,37 +39,104 @@
39
39
 
40
40
  Pluv.IO allows you to build real-time collaborate features with a fully end-to-end type-safe api.
41
41
 
42
+ ### Why
43
+
44
+ So you can do this:
45
+
46
+ ```tsx
47
+ usePluvEvent("RECEIVE_MESSAGE", ({ data }) => {
48
+ setMessages([...messages, data.message]);
49
+ });
50
+
51
+ usePluvBroadcast({
52
+ type: "SEND_MESSAGE",
53
+ data: { message: "Hello world!" },
54
+ });
55
+ ```
56
+
57
+ And this:
58
+
59
+ ```tsx
60
+ const [myCursor, updatePresence] = usePluvMyPresence((me) => me.presence.cursor);
61
+ const cursors = usePluvOthers((others) => {
62
+ return others.map((other) => other.presence.cursor);
63
+ });
64
+
65
+ useEffect(() => {
66
+ const updateCursor = (e: MouseEvent) => {
67
+ updatePresence({ cursor: { x: e.pageX, y: e.pageY } });
68
+ };
69
+
70
+ window.addEventListener("mousemove", updateCursor);
71
+
72
+ return () => {
73
+ window.removeEventListener("mousemove", updateCursor);
74
+ };
75
+ }, [updatePresence]);
76
+
77
+ return (
78
+ <div>
79
+ <div style={{ top: myCursor?.y, left: myCursor?.x }} />
80
+ {cursors.map((cursor, i) => (
81
+ <div key={i} style={{ top: cursor?.y, left: cursor?.x }} />
82
+ ))}
83
+ </div>
84
+ );
85
+ ```
86
+
87
+ Also this:
88
+
89
+
90
+ ```tsx
91
+ const [messages, sharedType] = usePluvStorage("messages");
92
+
93
+ sharedType?.push(["Hello world!"]);
94
+
95
+ return (
96
+ <div>
97
+ {messages?.map((message, i) => (
98
+ <div key={i}>{message}</div>
99
+ ))}
100
+ </div>
101
+ );
102
+ ```
103
+
104
+ And more. With great **E2E type-safety**, **intellisense** and the **[yjs](https://docs.yjs.dev/) ecosystem**.
105
+
42
106
  ### Features
43
- - [x] Automatic type safety
44
- - [x] Basic events
45
- - [x] Rooms
46
- - [x] Authentication
47
- - [x] Awareness + Presence
48
- - [x] [CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) (with [Yjs](https://docs.yjs.dev/))
49
- - [x] **Shared Types**
50
- - [x] [Map](https://docs.yjs.dev/api/shared-types/y.map)
51
- - [x] [Array](https://docs.yjs.dev/api/shared-types/y.array)
52
- - [x] [Text](https://docs.yjs.dev/api/shared-types/y.text)
53
- - [ ] [XmlFragment](https://docs.yjs.dev/api/shared-types/y.xmlfragment)
54
- - [ ] [XmlElement](https://docs.yjs.dev/api/shared-types/y.xmlelement)
55
- - [ ] [XmlText](https://docs.yjs.dev/api/shared-types/y.xmltext)
56
- - [ ] Studio (admin & developer panel)
107
+
108
+ - ✅ Automatic type safety
109
+ - ✅ Basic events
110
+ - ✅ Rooms
111
+ - ✅ Authentication
112
+ - ✅ Awareness + Presence
113
+ - ✅ [CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) (with [Yjs](https://docs.yjs.dev/))
114
+ - ✅ **Shared Types**
115
+ - ✅ [Map](https://docs.yjs.dev/api/shared-types/y.map)
116
+ - ✅ [Array](https://docs.yjs.dev/api/shared-types/y.array)
117
+ - ✅ [Text](https://docs.yjs.dev/api/shared-types/y.text)
118
+ - ⬜ [XmlFragment](https://docs.yjs.dev/api/shared-types/y.xmlfragment)
119
+ - ⬜ [XmlElement](https://docs.yjs.dev/api/shared-types/y.xmlelement)
120
+ - ⬜ [XmlText](https://docs.yjs.dev/api/shared-types/y.xmltext)
121
+ - ⬜ Studio (admin & developer panel)
57
122
 
58
123
  ### Runtimes
59
- - [x] [Cloudflare Workers](https://workers.cloudflare.com/)
60
- - [x] [Node.js](https://nodejs.org/)
61
- - [x] **PubSubs**
62
- - [x] [Redis](https://redis.io/)
63
- - [ ] [RabbitMQ](https://www.rabbitmq.com/)
64
- - [ ] [Kafka](https://kafka.apache.org/)
65
- - [x] **CRDT State Persistance**
66
- - [x] [Redis](https://redis.io/)
67
- - [ ] [Prisma](https://www.prisma.io/)
124
+
125
+ - ✅ [Cloudflare Workers](https://workers.cloudflare.com/)
126
+ - ✅ [Node.js](https://nodejs.org/)
127
+ - ✅ **PubSubs**
128
+ - ✅ [Redis](https://redis.io/)
129
+ - ⬜ [RabbitMQ](https://www.rabbitmq.com/)
130
+ - ⬜ [Kafka](https://kafka.apache.org/)
131
+ - ✅ **CRDT State Persistance**
132
+ - ✅ [Redis](https://redis.io/)
133
+ - ⬜ [Prisma](https://www.prisma.io/)
68
134
 
69
135
  ### Frontends
70
- - [x] [React.js](https://beta.reactjs.org/)
71
- - [ ] [Vue.js](https://vuejs.org/)
72
- - [ ] [Svelte](https://svelte.dev/)
136
+
137
+ - ✅ [React.js](https://beta.reactjs.org/)
138
+ - ⬜ [Vue.js](https://vuejs.org/)
139
+ - ⬜ [Svelte](https://svelte.dev/)
73
140
 
74
141
  ## Usage
75
142
 
@@ -88,6 +155,7 @@ Before diving into documentation, check out usage instructions for your selected
88
155
  * [Authentication](https://github.com/pluv-io/pluv/blob/master/docs/authentication.md)
89
156
  * [CRDT Storage](https://github.com/pluv-io/pluv/blob/master/docs/crdt-storage.md)
90
157
  * [Creating Rooms](https://github.com/pluv-io/pluv/blob/master/docs/creating-rooms.md)
158
+ * [`createIO` API](https://github.com/pluv-io/pluv/blob/master/docs/create-io-api.md)
91
159
  * [Custom Events](https://github.com/pluv-io/pluv/blob/master/docs/custom-events.md)
92
160
  * [Presence](https://github.com/pluv-io/pluv/blob/master/docs/presence.md)
93
161
  * [PubSubs and Persistance](https://github.com/pluv-io/pluv/blob/master/docs/pubsubs-and-persistance.md)
@@ -95,13 +163,13 @@ Before diving into documentation, check out usage instructions for your selected
95
163
 
96
164
  ## Related
97
165
 
98
- - [@pluv/client](https://github.com/pluv-io/pluv/tree/master/packages/client) - Framework agnostic client
99
- - [@pluv/crdt-yjs](https://github.com/pluv-io/pluv/tree/master/packages/crdt-yjs) - Yjs for Pluv.IO
100
- - [@pluv/persistance-redis](https://github.com/pluv-io/pluv/tree/master/packages/persistance-redis) - Persistance for storage on distributed systems (Node.js only)
101
- - [@pluv/platform-cloudflare](https://github.com/pluv-io/pluv/tree/master/packages/platform-cloudflare) - Adapter to run @pluv/io on Cloudflare Workers
102
- - [@pluv/platform-node](https://github.com/pluv-io/pluv/tree/master/packages/platform-node) - Adapter to run @pluv/io on Node.js
103
- - [@pluv/pubsub-redis](https://github.com/pluv-io/pluv/tree/master/packages/pubsub-redis) - PubSub for rooms across distributed systems
104
- - [@pluv/react](https://github.com/pluv-io/pluv/tree/master/packages/react) - Integrate @pluv/client with React.js
166
+ - [@pluv/client](https://www.npmjs.com/package/@pluv/client) - Framework agnostic client
167
+ - [@pluv/crdt-yjs](https://www.npmjs.com/package/@pluv/crdt-yjs) - Yjs for Pluv.IO
168
+ - [@pluv/persistance-redis](https://www.npmjs.com/package/@pluv/persistance-redis) - Persistance for storage on distributed systems (Node.js only)
169
+ - [@pluv/platform-cloudflare](https://www.npmjs.com/package/@pluv/platform-cloudflare) - Adapter to run @pluv/io on Cloudflare Workers
170
+ - [@pluv/platform-node](https://www.npmjs.com/package/@pluv/platform-node) - Adapter to run @pluv/io on Node.js
171
+ - [@pluv/pubsub-redis](https://www.npmjs.com/package/@pluv/pubsub-redis) - PubSub for rooms across distributed systems
172
+ - [@pluv/react](https://www.npmjs.com/package/@pluv/react) - Integrate @pluv/client with React.js
105
173
 
106
174
  ## Credits
107
175
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { JsonObject, MaybePromise, IOEventMessage, EventRecord, InputZodLike, Id, IOLike, InferIOOutput, BaseUser, IOAuthorize, BaseIOAuthorize, InferEventMessage, InferIOInput, BaseIOEventRecord, InferIOAuthorize, InferIOAuthorizeUser, Spread, BaseClientEventRecord } from '@pluv/types';
1
+ import { JsonObject, MaybePromise, IOEventMessage, EventRecord, InputZodLike, Id, IOLike, InferIOOutput, BaseUser, IOAuthorize, BaseIOAuthorize, InferEventMessage, InferIOInput, BaseIOEventRecord, InferIOAuthorize, Maybe, InferIOAuthorizeUser, Spread, BaseClientEventRecord } from '@pluv/types';
2
2
  export { BaseUser, EventMessage, EventRecord, IOEventMessage } from '@pluv/types';
3
3
  import { YjsDoc } from '@pluv/crdt-yjs';
4
4
 
@@ -7,6 +7,7 @@ declare abstract class AbstractPersistance {
7
7
  abstract deleteStorageState(room: string): Promise<void>;
8
8
  abstract deleteUser(room: string, connectionId: string): Promise<void>;
9
9
  abstract deleteUsers(room: string): Promise<void>;
10
+ abstract getSize(room: string): Promise<number>;
10
11
  abstract getStorageState(room: string): Promise<string | null>;
11
12
  abstract getUser(room: string, connectionId: string): Promise<JsonObject | null>;
12
13
  abstract getUsers(room: string): Promise<readonly (JsonObject | null)[]>;
@@ -176,14 +177,17 @@ interface AuthorizeModule {
176
177
  }
177
178
 
178
179
  type BroadcastMessage<TIO extends IORoom<any, any, any, any, any>> = InferEventMessage<InferIOInput<TIO>> | InferEventMessage<BaseIOEventRecord<InferIOAuthorize<TIO>>>;
179
- interface IORoomConfig<TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>, TAuthorize extends IOAuthorize<any, any> = BaseIOAuthorize, TContext extends JsonObject = {}, TInput extends EventRecord<string, any> = {}, TOutput extends EventRecord<string, any> = {}> {
180
+ interface IORoomListeners {
181
+ onDestroy: (encodedState: string) => void;
182
+ }
183
+ type IORoomConfig<TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>, TAuthorize extends IOAuthorize<any, any> = BaseIOAuthorize, TContext extends JsonObject = {}, TInput extends EventRecord<string, any> = {}, TOutput extends EventRecord<string, any> = {}> = Partial<IORoomListeners> & {
180
184
  authorize?: TAuthorize;
181
185
  context: TContext;
182
186
  debug: boolean;
183
187
  events: InferEventConfig<TContext, TInput, TOutput>;
184
- onDestroy?: () => void;
188
+ initialStorage?: () => MaybePromise<Maybe<string>>;
185
189
  platform: TPlatform;
186
- }
190
+ };
187
191
  interface WebsocketRegisterOptions {
188
192
  token?: string;
189
193
  }
@@ -192,6 +196,7 @@ declare class IORoom<TPlatform extends AbstractPlatform<any> = AbstractPlatform<
192
196
  private readonly _debug;
193
197
  private readonly _platform;
194
198
  private _doc;
199
+ private _initialStorage;
195
200
  private _listeners;
196
201
  private _sessions;
197
202
  private _uninitialize;
@@ -222,22 +227,27 @@ declare class IORoom<TPlatform extends AbstractPlatform<any> = AbstractPlatform<
222
227
  type InferIOContext<TIO extends PluvIO<any, any, any, any, any>> = TIO extends PluvIO<any, any, infer IContext, any, any> ? IContext : never;
223
228
  type InferIOPlatform<TIO extends PluvIO<any, any, any, any>> = TIO extends PluvIO<infer IPlatform, any, any, any, any> ? IPlatform : never;
224
229
  type InferIORoom<TIO extends PluvIO<any, any, any, any>> = TIO extends PluvIO<infer IPlatform, infer IAuthorize, infer IContext, infer IInput, infer IOutput> ? IORoom<IPlatform, IAuthorize, IContext, IInput, IOutput> : never;
225
- interface PluvIOConfig<TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>, TAuthorize extends IOAuthorize<any, any> | null = null, TContext extends JsonObject = {}, TInput extends EventRecord<string, any> = {}, TOutput extends EventRecord<string, any> = {}> {
230
+ interface PluvIOListeners {
231
+ onRoomDeleted: (room: string, encodedState: string) => void;
232
+ onStorageUpdated: (room: string, encodedState: string) => void;
233
+ }
234
+ type PluvIOConfig<TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>, TAuthorize extends IOAuthorize<any, any> | null = null, TContext extends JsonObject = {}, TInput extends EventRecord<string, any> = {}, TOutput extends EventRecord<string, any> = {}> = Partial<PluvIOListeners> & {
226
235
  authorize?: TAuthorize;
227
236
  context?: TContext;
228
237
  debug?: boolean;
229
238
  events?: InferEventConfig<TContext, TInput, TOutput>;
239
+ initialStorage?: (room: string) => MaybePromise<Maybe<string>>;
230
240
  platform: TPlatform;
231
- room?: string;
232
- }
241
+ };
233
242
  declare class PluvIO<TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>, TAuthorize extends IOAuthorize<any, any> = BaseIOAuthorize, TContext extends JsonObject = {}, TInput extends EventRecord<string, any> = {}, TOutput extends EventRecord<string, any> = {}> implements IOLike<TAuthorize, TInput, TOutput> {
243
+ private _context;
244
+ private _initialStorage;
245
+ private _listeners;
246
+ private _rooms;
234
247
  readonly _authorize: TAuthorize | null;
235
248
  readonly _debug: boolean;
236
249
  readonly _events: InferEventConfig<TContext, TInput, TOutput>;
237
250
  readonly _platform: TPlatform;
238
- readonly room: string | null;
239
- private _context;
240
- private _rooms;
241
251
  constructor(options: PluvIOConfig<TPlatform, TAuthorize, TContext, TInput, TOutput>);
242
252
  createToken(params: JWTEncodeParams<InferIOAuthorizeUser<InferIOAuthorize<this>>>): Promise<string>;
243
253
  event<TEvent extends string, TData extends JsonObject = {}, TResult extends EventRecord<string, any> = {}>(event: TEvent, config: EventConfig<TContext, TData, TResult>): PluvIO<TPlatform, TAuthorize, TContext, Spread<[TInput, EventRecord<TEvent, TData>]>, Spread<[TOutput, TResult]>>;
@@ -246,12 +256,13 @@ declare class PluvIO<TPlatform extends AbstractPlatform<any> = AbstractPlatform<
246
256
  private _logDebug;
247
257
  }
248
258
 
249
- interface CreateIOParams<TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>, TContext extends JsonObject = {}, TAuthorizeUser extends BaseUser = BaseUser, TAuthorizeRequired extends boolean = false> {
259
+ type CreateIOParams<TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>, TContext extends JsonObject = {}, TAuthorizeUser extends BaseUser = BaseUser, TAuthorizeRequired extends boolean = false> = Partial<PluvIOListeners> & {
250
260
  authorize?: IOAuthorize<TAuthorizeUser, TAuthorizeRequired>;
251
261
  context?: TContext;
252
262
  debug?: boolean;
263
+ initialStorage?: (room: string) => MaybePromise<Maybe<string>>;
253
264
  platform: TPlatform;
254
- }
265
+ };
255
266
  declare const createIO: <TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>, TContext extends JsonObject = {}, TAuthorizeUser extends BaseUser = BaseUser, TAuthorizeRequired extends boolean = false>(params: CreateIOParams<TPlatform, TContext, TAuthorizeUser, TAuthorizeRequired>) => PluvIO<TPlatform, IOAuthorize<TAuthorizeUser, TAuthorizeRequired>, TContext, BaseClientEventRecord, BaseIOEventRecord<IOAuthorize<TAuthorizeUser, TAuthorizeRequired>>>;
256
267
 
257
- export { AbstractCloseEvent, AbstractErrorEvent, AbstractEvent, AbstractEventMap, AbstractListener, AbstractMessageEvent, AbstractPersistance, AbstractPlatform, AbstractPubSub, AbstractWebSocket, AbstractWebSocketConfig, AbstractWebSocketHandleErrorParams, AuthorizeModule, AuthorizeParams, ConvertWebSocketConfig, IOPubSubEventMessage, IORoom, InferIORoom, InferWebSocketType, JWT, JWTEncodeParams, PluvIO, createIO };
268
+ export { AbstractCloseEvent, AbstractErrorEvent, AbstractEvent, AbstractEventMap, AbstractListener, AbstractMessageEvent, AbstractPersistance, AbstractPlatform, AbstractPubSub, AbstractWebSocket, AbstractWebSocketConfig, AbstractWebSocketHandleErrorParams, AuthorizeModule, AuthorizeParams, ConvertWebSocketConfig, CreateIOParams, IOPubSubEventMessage, IORoom, InferIORoom, InferWebSocketType, JWT, JWTEncodeParams, PluvIO, createIO };
package/dist/index.js CHANGED
@@ -122,6 +122,10 @@ var Persistance = class extends AbstractPersistance {
122
122
  this._users.delete(room);
123
123
  return Promise.resolve();
124
124
  }
125
+ getSize(room) {
126
+ var _a, _b;
127
+ return Promise.resolve((_b = (_a = this._users.get(room)) == null ? void 0 : _a.size) != null ? _b : 0);
128
+ }
125
129
  getStorageState(room) {
126
130
  var _a;
127
131
  const storage = (_a = this._storages.get(room)) != null ? _a : null;
@@ -289,20 +293,31 @@ var PING_TIMEOUT_MS = 3e4;
289
293
  var IORoom = class {
290
294
  constructor(room, config) {
291
295
  this._doc = (0, import_crdt_yjs.doc)();
296
+ this._initialStorage = null;
292
297
  this._sessions = /* @__PURE__ */ new Map();
293
298
  this._uninitialize = null;
294
299
  this._authorize = null;
295
- const { authorize: authorize2, context, debug, events, onDestroy, platform } = config;
300
+ const {
301
+ authorize: authorize2,
302
+ context,
303
+ debug,
304
+ events,
305
+ initialStorage,
306
+ onDestroy,
307
+ platform
308
+ } = config;
296
309
  this._context = context;
297
310
  this._debug = debug;
298
311
  this._events = events;
299
312
  this._platform = platform;
300
313
  this.room = room;
301
314
  this._listeners = {
302
- onDestroy: () => onDestroy == null ? void 0 : onDestroy()
315
+ onDestroy: (encodedState) => onDestroy == null ? void 0 : onDestroy(encodedState)
303
316
  };
304
317
  if (authorize2)
305
318
  this._authorize = authorize2;
319
+ if (initialStorage)
320
+ this._initialStorage = initialStorage;
306
321
  }
307
322
  broadcast(message) {
308
323
  return Promise.resolve(this._broadcast({ message }));
@@ -428,25 +443,29 @@ var IORoom = class {
428
443
  )} ${sessionId}`
429
444
  );
430
445
  this._sessions.delete(session.id);
431
- this._platform.persistance.deleteUser(this.room, session.id);
432
- this._broadcast({
433
- message: {
434
- type: "$EXIT",
435
- data: { sessionId: session.id }
436
- },
437
- senderId: session.id
446
+ this._platform.persistance.deleteUser(this.room, session.id).finally(() => {
447
+ var _a2;
448
+ this._broadcast({
449
+ message: {
450
+ type: "$EXIT",
451
+ data: { sessionId: session.id }
452
+ },
453
+ senderId: session.id
454
+ });
455
+ uninitializeWs();
456
+ const size2 = this._getSessionsSize();
457
+ this._logDebug(
458
+ `${import_chalk.default.blue(
459
+ `Unregistered connection for room ${this.room}:`
460
+ )} ${sessionId}`
461
+ );
462
+ this._logDebug(
463
+ `${import_chalk.default.blue(`Room ${this.room} size:`)} ${size2}`
464
+ );
465
+ if (size2)
466
+ return;
467
+ (_a2 = this._uninitialize) == null ? void 0 : _a2.call(this);
438
468
  });
439
- uninitializeWs();
440
- const size2 = this._getSessionsSize();
441
- this._logDebug(
442
- `${import_chalk.default.blue(
443
- `Unregistered connection for room ${this.room}:`
444
- )} ${sessionId}`
445
- );
446
- this._logDebug(`${import_chalk.default.blue(`Room ${this.room} size:`)} ${size2}`);
447
- if (size2)
448
- return;
449
- this._uninitialize();
450
469
  };
451
470
  pluvWs.addEventListener("close", closeHandler);
452
471
  pluvWs.addEventListener("error", closeHandler);
@@ -498,8 +517,9 @@ var IORoom = class {
498
517
  );
499
518
  }
500
519
  _emitStorageReceived(session) {
501
- this._platform.persistance.getStorageState(this.room).then((_state) => {
502
- const state = _state != null ? _state : this._doc.encodeStateAsUpdate();
520
+ this._platform.persistance.getStorageState(this.room).then((_state) => __async(this, null, function* () {
521
+ var _a, _b;
522
+ const state = (_b = _state != null ? _state : yield (_a = this._initialStorage) == null ? void 0 : _a.call(this)) != null ? _b : this._doc.encodeStateAsUpdate();
503
523
  this._sendSelfMessage(
504
524
  {
505
525
  type: "$STORAGE_RECEIVED",
@@ -507,7 +527,7 @@ var IORoom = class {
507
527
  },
508
528
  session
509
529
  );
510
- });
530
+ }));
511
531
  }
512
532
  _emitUserJoined(session) {
513
533
  const user = session.user;
@@ -588,9 +608,10 @@ var IORoom = class {
588
608
  );
589
609
  this._uninitialize = () => __async(this, null, function* () {
590
610
  this._platform.pubSub.unsubscribe(pubSubId);
611
+ const encodedState = this._doc.encodeStateAsUpdate();
591
612
  this._doc.destroy();
592
613
  this._doc = (0, import_crdt_yjs.doc)();
593
- this._listeners.onDestroy();
614
+ this._listeners.onDestroy(encodedState);
594
615
  });
595
616
  });
596
617
  }
@@ -711,6 +732,9 @@ var IORoom = class {
711
732
  // src/PluvIO.ts
712
733
  var PluvIO = class {
713
734
  constructor(options) {
735
+ this._context = {};
736
+ this._initialStorage = null;
737
+ this._rooms = /* @__PURE__ */ new Map();
714
738
  this._authorize = null;
715
739
  this._events = {
716
740
  $PING: {
@@ -724,13 +748,13 @@ var PluvIO = class {
724
748
  options: { type: "self" }
725
749
  },
726
750
  $GET_OTHERS: {
727
- resolver: (data, { session, sessions }) => {
751
+ resolver: (data, { room, session, sessions }) => {
728
752
  const others = Array.from(sessions.entries()).filter(([, wsSession]) => wsSession.id !== (session == null ? void 0 : session.id)).reduce(
729
753
  (acc, [connectionId, { presence, user }]) => __spreadProps(__spreadValues({}, acc), {
730
754
  [connectionId]: {
731
755
  connectionId,
732
756
  presence,
733
- room: this.room,
757
+ room,
734
758
  user
735
759
  }
736
760
  }),
@@ -759,21 +783,22 @@ var PluvIO = class {
759
783
  }
760
784
  const updated = doc2.applyUpdate(update);
761
785
  const state = updated.encodeStateAsUpdate();
762
- this._platform.persistance.setStorageState(room, state);
786
+ this._platform.persistance.setStorageState(room, state).then(() => {
787
+ this._listeners.onStorageUpdated(room, state);
788
+ });
763
789
  return { $STORAGE_UPDATED: { state } };
764
790
  }
765
791
  }
766
792
  };
767
- this.room = null;
768
- this._context = {};
769
- this._rooms = /* @__PURE__ */ new Map();
770
793
  const {
771
794
  authorize: authorize2,
772
795
  context,
773
796
  debug = false,
774
797
  events,
775
- platform,
776
- room
798
+ initialStorage,
799
+ onRoomDeleted,
800
+ onStorageUpdated,
801
+ platform
777
802
  } = options;
778
803
  this._debug = debug;
779
804
  this._platform = platform;
@@ -783,8 +808,16 @@ var PluvIO = class {
783
808
  this._context = context;
784
809
  if (events)
785
810
  this._events = events;
786
- if (room)
787
- this.room = room;
811
+ if (initialStorage)
812
+ this._initialStorage = initialStorage;
813
+ this._listeners = {
814
+ onRoomDeleted: (room, encodedState) => {
815
+ return onRoomDeleted == null ? void 0 : onRoomDeleted(room, encodedState);
816
+ },
817
+ onStorageUpdated: (room, encodedState) => {
818
+ return onStorageUpdated == null ? void 0 : onStorageUpdated(room, encodedState);
819
+ }
820
+ };
788
821
  }
789
822
  createToken(params) {
790
823
  return __async(this, null, function* () {
@@ -816,9 +849,6 @@ var PluvIO = class {
816
849
  }
817
850
  getRoom(room) {
818
851
  var _a;
819
- if (this.room) {
820
- throw new Error("IO cannot getRoom from within a room.");
821
- }
822
852
  if (!/^[a-z0-9]+[a-z0-9\-_]+[a-z0-9]+$/i.test(room)) {
823
853
  throw new Error("Unsupported room name");
824
854
  }
@@ -831,7 +861,11 @@ var PluvIO = class {
831
861
  context: this._context,
832
862
  debug: this._debug,
833
863
  events: this._events,
834
- onDestroy: () => {
864
+ initialStorage: () => {
865
+ var _a2;
866
+ return (_a2 = this._initialStorage) == null ? void 0 : _a2.call(this, room);
867
+ },
868
+ onDestroy: (encodedState) => {
835
869
  this._logDebug(`${import_chalk2.default.blue("Deleting empty room:")} ${room}`);
836
870
  this._rooms.delete(room);
837
871
  if (this._debug) {
@@ -841,6 +875,11 @@ var PluvIO = class {
841
875
  `${import_chalk2.default.blue("Rooms available:")} ${rooms.join(", ")}`
842
876
  );
843
877
  }
878
+ this._platform.persistance.getSize(room).then((size) => {
879
+ if (size)
880
+ return;
881
+ this._listeners.onRoomDeleted(room, encodedState);
882
+ });
844
883
  },
845
884
  platform
846
885
  });
@@ -855,11 +894,10 @@ var PluvIO = class {
855
894
  return newRoom;
856
895
  }
857
896
  static merge(first, second) {
858
- var _a, _b;
897
+ var _a;
859
898
  const authorize2 = (_a = first._authorize) != null ? _a : void 0;
860
899
  const debug = first._debug;
861
900
  const platform = first._platform;
862
- const room = (_b = first.room) != null ? _b : void 0;
863
901
  const events1 = first._events;
864
902
  const events2 = second._events;
865
903
  const events = Object.assign(/* @__PURE__ */ Object.create(null), events1, events2);
@@ -867,8 +905,7 @@ var PluvIO = class {
867
905
  authorize: authorize2,
868
906
  debug,
869
907
  events,
870
- platform,
871
- room
908
+ platform
872
909
  });
873
910
  }
874
911
  _logDebug(...data) {
@@ -878,8 +915,24 @@ var PluvIO = class {
878
915
 
879
916
  // src/createIO.ts
880
917
  var createIO = (params) => {
881
- const { authorize: authorize2, context, debug, platform } = params;
882
- return new PluvIO({ authorize: authorize2, context, debug, platform });
918
+ const {
919
+ authorize: authorize2,
920
+ context,
921
+ debug,
922
+ initialStorage,
923
+ onRoomDeleted,
924
+ onStorageUpdated,
925
+ platform
926
+ } = params;
927
+ return new PluvIO({
928
+ authorize: authorize2,
929
+ context,
930
+ debug,
931
+ initialStorage,
932
+ onRoomDeleted,
933
+ onStorageUpdated,
934
+ platform
935
+ });
883
936
  };
884
937
  // Annotate the CommonJS export names for ESM import in node:
885
938
  0 && (module.exports = {
package/dist/index.mjs CHANGED
@@ -89,6 +89,10 @@ var Persistance = class extends AbstractPersistance {
89
89
  this._users.delete(room);
90
90
  return Promise.resolve();
91
91
  }
92
+ getSize(room) {
93
+ var _a, _b;
94
+ return Promise.resolve((_b = (_a = this._users.get(room)) == null ? void 0 : _a.size) != null ? _b : 0);
95
+ }
92
96
  getStorageState(room) {
93
97
  var _a;
94
98
  const storage = (_a = this._storages.get(room)) != null ? _a : null;
@@ -256,20 +260,31 @@ var PING_TIMEOUT_MS = 3e4;
256
260
  var IORoom = class {
257
261
  constructor(room, config) {
258
262
  this._doc = doc();
263
+ this._initialStorage = null;
259
264
  this._sessions = /* @__PURE__ */ new Map();
260
265
  this._uninitialize = null;
261
266
  this._authorize = null;
262
- const { authorize: authorize2, context, debug, events, onDestroy, platform } = config;
267
+ const {
268
+ authorize: authorize2,
269
+ context,
270
+ debug,
271
+ events,
272
+ initialStorage,
273
+ onDestroy,
274
+ platform
275
+ } = config;
263
276
  this._context = context;
264
277
  this._debug = debug;
265
278
  this._events = events;
266
279
  this._platform = platform;
267
280
  this.room = room;
268
281
  this._listeners = {
269
- onDestroy: () => onDestroy == null ? void 0 : onDestroy()
282
+ onDestroy: (encodedState) => onDestroy == null ? void 0 : onDestroy(encodedState)
270
283
  };
271
284
  if (authorize2)
272
285
  this._authorize = authorize2;
286
+ if (initialStorage)
287
+ this._initialStorage = initialStorage;
273
288
  }
274
289
  broadcast(message) {
275
290
  return Promise.resolve(this._broadcast({ message }));
@@ -395,25 +410,29 @@ var IORoom = class {
395
410
  )} ${sessionId}`
396
411
  );
397
412
  this._sessions.delete(session.id);
398
- this._platform.persistance.deleteUser(this.room, session.id);
399
- this._broadcast({
400
- message: {
401
- type: "$EXIT",
402
- data: { sessionId: session.id }
403
- },
404
- senderId: session.id
413
+ this._platform.persistance.deleteUser(this.room, session.id).finally(() => {
414
+ var _a2;
415
+ this._broadcast({
416
+ message: {
417
+ type: "$EXIT",
418
+ data: { sessionId: session.id }
419
+ },
420
+ senderId: session.id
421
+ });
422
+ uninitializeWs();
423
+ const size2 = this._getSessionsSize();
424
+ this._logDebug(
425
+ `${chalk.blue(
426
+ `Unregistered connection for room ${this.room}:`
427
+ )} ${sessionId}`
428
+ );
429
+ this._logDebug(
430
+ `${chalk.blue(`Room ${this.room} size:`)} ${size2}`
431
+ );
432
+ if (size2)
433
+ return;
434
+ (_a2 = this._uninitialize) == null ? void 0 : _a2.call(this);
405
435
  });
406
- uninitializeWs();
407
- const size2 = this._getSessionsSize();
408
- this._logDebug(
409
- `${chalk.blue(
410
- `Unregistered connection for room ${this.room}:`
411
- )} ${sessionId}`
412
- );
413
- this._logDebug(`${chalk.blue(`Room ${this.room} size:`)} ${size2}`);
414
- if (size2)
415
- return;
416
- this._uninitialize();
417
436
  };
418
437
  pluvWs.addEventListener("close", closeHandler);
419
438
  pluvWs.addEventListener("error", closeHandler);
@@ -465,8 +484,9 @@ var IORoom = class {
465
484
  );
466
485
  }
467
486
  _emitStorageReceived(session) {
468
- this._platform.persistance.getStorageState(this.room).then((_state) => {
469
- const state = _state != null ? _state : this._doc.encodeStateAsUpdate();
487
+ this._platform.persistance.getStorageState(this.room).then((_state) => __async(this, null, function* () {
488
+ var _a, _b;
489
+ const state = (_b = _state != null ? _state : yield (_a = this._initialStorage) == null ? void 0 : _a.call(this)) != null ? _b : this._doc.encodeStateAsUpdate();
470
490
  this._sendSelfMessage(
471
491
  {
472
492
  type: "$STORAGE_RECEIVED",
@@ -474,7 +494,7 @@ var IORoom = class {
474
494
  },
475
495
  session
476
496
  );
477
- });
497
+ }));
478
498
  }
479
499
  _emitUserJoined(session) {
480
500
  const user = session.user;
@@ -555,9 +575,10 @@ var IORoom = class {
555
575
  );
556
576
  this._uninitialize = () => __async(this, null, function* () {
557
577
  this._platform.pubSub.unsubscribe(pubSubId);
578
+ const encodedState = this._doc.encodeStateAsUpdate();
558
579
  this._doc.destroy();
559
580
  this._doc = doc();
560
- this._listeners.onDestroy();
581
+ this._listeners.onDestroy(encodedState);
561
582
  });
562
583
  });
563
584
  }
@@ -678,6 +699,9 @@ var IORoom = class {
678
699
  // src/PluvIO.ts
679
700
  var PluvIO = class {
680
701
  constructor(options) {
702
+ this._context = {};
703
+ this._initialStorage = null;
704
+ this._rooms = /* @__PURE__ */ new Map();
681
705
  this._authorize = null;
682
706
  this._events = {
683
707
  $PING: {
@@ -691,13 +715,13 @@ var PluvIO = class {
691
715
  options: { type: "self" }
692
716
  },
693
717
  $GET_OTHERS: {
694
- resolver: (data, { session, sessions }) => {
718
+ resolver: (data, { room, session, sessions }) => {
695
719
  const others = Array.from(sessions.entries()).filter(([, wsSession]) => wsSession.id !== (session == null ? void 0 : session.id)).reduce(
696
720
  (acc, [connectionId, { presence, user }]) => __spreadProps(__spreadValues({}, acc), {
697
721
  [connectionId]: {
698
722
  connectionId,
699
723
  presence,
700
- room: this.room,
724
+ room,
701
725
  user
702
726
  }
703
727
  }),
@@ -726,21 +750,22 @@ var PluvIO = class {
726
750
  }
727
751
  const updated = doc2.applyUpdate(update);
728
752
  const state = updated.encodeStateAsUpdate();
729
- this._platform.persistance.setStorageState(room, state);
753
+ this._platform.persistance.setStorageState(room, state).then(() => {
754
+ this._listeners.onStorageUpdated(room, state);
755
+ });
730
756
  return { $STORAGE_UPDATED: { state } };
731
757
  }
732
758
  }
733
759
  };
734
- this.room = null;
735
- this._context = {};
736
- this._rooms = /* @__PURE__ */ new Map();
737
760
  const {
738
761
  authorize: authorize2,
739
762
  context,
740
763
  debug = false,
741
764
  events,
742
- platform,
743
- room
765
+ initialStorage,
766
+ onRoomDeleted,
767
+ onStorageUpdated,
768
+ platform
744
769
  } = options;
745
770
  this._debug = debug;
746
771
  this._platform = platform;
@@ -750,8 +775,16 @@ var PluvIO = class {
750
775
  this._context = context;
751
776
  if (events)
752
777
  this._events = events;
753
- if (room)
754
- this.room = room;
778
+ if (initialStorage)
779
+ this._initialStorage = initialStorage;
780
+ this._listeners = {
781
+ onRoomDeleted: (room, encodedState) => {
782
+ return onRoomDeleted == null ? void 0 : onRoomDeleted(room, encodedState);
783
+ },
784
+ onStorageUpdated: (room, encodedState) => {
785
+ return onStorageUpdated == null ? void 0 : onStorageUpdated(room, encodedState);
786
+ }
787
+ };
755
788
  }
756
789
  createToken(params) {
757
790
  return __async(this, null, function* () {
@@ -783,9 +816,6 @@ var PluvIO = class {
783
816
  }
784
817
  getRoom(room) {
785
818
  var _a;
786
- if (this.room) {
787
- throw new Error("IO cannot getRoom from within a room.");
788
- }
789
819
  if (!/^[a-z0-9]+[a-z0-9\-_]+[a-z0-9]+$/i.test(room)) {
790
820
  throw new Error("Unsupported room name");
791
821
  }
@@ -798,7 +828,11 @@ var PluvIO = class {
798
828
  context: this._context,
799
829
  debug: this._debug,
800
830
  events: this._events,
801
- onDestroy: () => {
831
+ initialStorage: () => {
832
+ var _a2;
833
+ return (_a2 = this._initialStorage) == null ? void 0 : _a2.call(this, room);
834
+ },
835
+ onDestroy: (encodedState) => {
802
836
  this._logDebug(`${chalk2.blue("Deleting empty room:")} ${room}`);
803
837
  this._rooms.delete(room);
804
838
  if (this._debug) {
@@ -808,6 +842,11 @@ var PluvIO = class {
808
842
  `${chalk2.blue("Rooms available:")} ${rooms.join(", ")}`
809
843
  );
810
844
  }
845
+ this._platform.persistance.getSize(room).then((size) => {
846
+ if (size)
847
+ return;
848
+ this._listeners.onRoomDeleted(room, encodedState);
849
+ });
811
850
  },
812
851
  platform
813
852
  });
@@ -822,11 +861,10 @@ var PluvIO = class {
822
861
  return newRoom;
823
862
  }
824
863
  static merge(first, second) {
825
- var _a, _b;
864
+ var _a;
826
865
  const authorize2 = (_a = first._authorize) != null ? _a : void 0;
827
866
  const debug = first._debug;
828
867
  const platform = first._platform;
829
- const room = (_b = first.room) != null ? _b : void 0;
830
868
  const events1 = first._events;
831
869
  const events2 = second._events;
832
870
  const events = Object.assign(/* @__PURE__ */ Object.create(null), events1, events2);
@@ -834,8 +872,7 @@ var PluvIO = class {
834
872
  authorize: authorize2,
835
873
  debug,
836
874
  events,
837
- platform,
838
- room
875
+ platform
839
876
  });
840
877
  }
841
878
  _logDebug(...data) {
@@ -845,8 +882,24 @@ var PluvIO = class {
845
882
 
846
883
  // src/createIO.ts
847
884
  var createIO = (params) => {
848
- const { authorize: authorize2, context, debug, platform } = params;
849
- return new PluvIO({ authorize: authorize2, context, debug, platform });
885
+ const {
886
+ authorize: authorize2,
887
+ context,
888
+ debug,
889
+ initialStorage,
890
+ onRoomDeleted,
891
+ onStorageUpdated,
892
+ platform
893
+ } = params;
894
+ return new PluvIO({
895
+ authorize: authorize2,
896
+ context,
897
+ debug,
898
+ initialStorage,
899
+ onRoomDeleted,
900
+ onStorageUpdated,
901
+ platform
902
+ });
850
903
  };
851
904
  export {
852
905
  AbstractPersistance,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pluv/io",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "@pluv/io server-side websocket manager",
5
5
  "author": "leedavidcs",
6
6
  "license": "MIT",
@@ -18,8 +18,8 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@panva/hkdf": "^1.0.2",
21
- "@pluv/crdt-yjs": "^0.1.0",
22
- "@pluv/types": "^0.1.0",
21
+ "@pluv/crdt-yjs": "^0.1.2",
22
+ "@pluv/types": "^0.1.1",
23
23
  "chalk": "^4.1.2",
24
24
  "jose": "^4.11.2",
25
25
  "wonka": "^6.1.2"
@@ -28,8 +28,8 @@
28
28
  "yjs": "^13.0.0"
29
29
  },
30
30
  "devDependencies": {
31
- "@pluv/tsconfig": "^0.1.0",
32
- "eslint-config-pluv": "^0.1.0",
31
+ "@pluv/tsconfig": "^0.1.1",
32
+ "eslint-config-pluv": "^0.1.1",
33
33
  "tsup": "^6.5.0",
34
34
  "typescript": "^4.9.4",
35
35
  "yjs": "^13.5.44"
@@ -16,6 +16,8 @@ export abstract class AbstractPersistance {
16
16
 
17
17
  public abstract deleteUsers(room: string): Promise<void>;
18
18
 
19
+ public abstract getSize(room: string): Promise<number>;
20
+
19
21
  public abstract getStorageState(room: string): Promise<string | null>;
20
22
 
21
23
  public abstract getUser(
package/src/IORoom.ts CHANGED
@@ -12,6 +12,8 @@ import type {
12
12
  IOAuthorize,
13
13
  IOLike,
14
14
  JsonObject,
15
+ Maybe,
16
+ MaybePromise,
15
17
  } from "@pluv/types";
16
18
  import chalk from "chalk";
17
19
  import type { AbstractPlatform, InferWebSocketType } from "./AbstractPlatform";
@@ -35,23 +37,23 @@ interface BroadcastParams<TIO extends IORoom<any, any, any, any, any>> {
35
37
  }
36
38
 
37
39
  interface IORoomListeners {
38
- onDestroy: () => void;
40
+ onDestroy: (encodedState: string) => void;
39
41
  }
40
42
 
41
- export interface IORoomConfig<
43
+ export type IORoomConfig<
42
44
  TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
43
45
  TAuthorize extends IOAuthorize<any, any> = BaseIOAuthorize,
44
46
  TContext extends JsonObject = {},
45
47
  TInput extends EventRecord<string, any> = {},
46
48
  TOutput extends EventRecord<string, any> = {}
47
- > {
49
+ > = Partial<IORoomListeners> & {
48
50
  authorize?: TAuthorize;
49
51
  context: TContext;
50
52
  debug: boolean;
51
53
  events: InferEventConfig<TContext, TInput, TOutput>;
52
- onDestroy?: () => void;
54
+ initialStorage?: () => MaybePromise<Maybe<string>>;
53
55
  platform: TPlatform;
54
- }
56
+ };
55
57
 
56
58
  interface SendMessageSender {
57
59
  id: string | null;
@@ -75,6 +77,7 @@ export class IORoom<
75
77
  private readonly _platform: TPlatform;
76
78
 
77
79
  private _doc: YjsDoc<any> = doc();
80
+ private _initialStorage: (() => MaybePromise<Maybe<string>>) | null = null;
78
81
  private _listeners: IORoomListeners;
79
82
  private _sessions = new Map<string, WebSocketSession>();
80
83
  private _uninitialize: (() => Promise<void>) | null = null;
@@ -88,8 +91,15 @@ export class IORoom<
88
91
  room: string,
89
92
  config: IORoomConfig<TPlatform, TAuthorize, TContext, TInput, TOutput>
90
93
  ) {
91
- const { authorize, context, debug, events, onDestroy, platform } =
92
- config;
94
+ const {
95
+ authorize,
96
+ context,
97
+ debug,
98
+ events,
99
+ initialStorage,
100
+ onDestroy,
101
+ platform,
102
+ } = config;
93
103
 
94
104
  this._context = context;
95
105
  this._debug = debug;
@@ -99,10 +109,11 @@ export class IORoom<
99
109
  this.room = room;
100
110
 
101
111
  this._listeners = {
102
- onDestroy: () => onDestroy?.(),
112
+ onDestroy: (encodedState) => onDestroy?.(encodedState),
103
113
  };
104
114
 
105
115
  if (authorize) this._authorize = authorize;
116
+ if (initialStorage) this._initialStorage = initialStorage;
106
117
  }
107
118
 
108
119
  public broadcast(message: BroadcastMessage<this>): Promise<void> {
@@ -255,30 +266,34 @@ export class IORoom<
255
266
  );
256
267
  this._sessions.delete(session.id);
257
268
 
258
- this._platform.persistance.deleteUser(this.room, session.id);
259
-
260
- this._broadcast({
261
- message: {
262
- type: "$EXIT",
263
- data: { sessionId: session.id },
264
- },
265
- senderId: session.id,
266
- });
267
-
268
- uninitializeWs();
269
-
270
- const size = this._getSessionsSize();
271
-
272
- this._logDebug(
273
- `${chalk.blue(
274
- `Unregistered connection for room ${this.room}:`
275
- )} ${sessionId}`
276
- );
277
- this._logDebug(`${chalk.blue(`Room ${this.room} size:`)} ${size}`);
278
-
279
- if (size) return;
280
-
281
- this._uninitialize();
269
+ this._platform.persistance
270
+ .deleteUser(this.room, session.id)
271
+ .finally(() => {
272
+ this._broadcast({
273
+ message: {
274
+ type: "$EXIT",
275
+ data: { sessionId: session.id },
276
+ },
277
+ senderId: session.id,
278
+ });
279
+
280
+ uninitializeWs();
281
+
282
+ const size = this._getSessionsSize();
283
+
284
+ this._logDebug(
285
+ `${chalk.blue(
286
+ `Unregistered connection for room ${this.room}:`
287
+ )} ${sessionId}`
288
+ );
289
+ this._logDebug(
290
+ `${chalk.blue(`Room ${this.room} size:`)} ${size}`
291
+ );
292
+
293
+ if (size) return;
294
+
295
+ this._uninitialize?.();
296
+ });
282
297
  };
283
298
 
284
299
  pluvWs.addEventListener("close", closeHandler);
@@ -344,17 +359,22 @@ export class IORoom<
344
359
  }
345
360
 
346
361
  private _emitStorageReceived(session: WebSocketSession): void {
347
- this._platform.persistance.getStorageState(this.room).then((_state) => {
348
- const state = _state ?? this._doc.encodeStateAsUpdate();
349
-
350
- this._sendSelfMessage(
351
- {
352
- type: "$STORAGE_RECEIVED",
353
- data: { state },
354
- },
355
- session
356
- );
357
- });
362
+ this._platform.persistance
363
+ .getStorageState(this.room)
364
+ .then(async (_state) => {
365
+ const state =
366
+ _state ??
367
+ (await this._initialStorage?.()) ??
368
+ this._doc.encodeStateAsUpdate();
369
+
370
+ this._sendSelfMessage(
371
+ {
372
+ type: "$STORAGE_RECEIVED",
373
+ data: { state },
374
+ },
375
+ session
376
+ );
377
+ });
358
378
  }
359
379
 
360
380
  private _emitUserJoined(session: WebSocketSession): void {
@@ -459,10 +479,12 @@ export class IORoom<
459
479
  this._uninitialize = async () => {
460
480
  this._platform.pubSub.unsubscribe(pubSubId);
461
481
 
482
+ const encodedState = this._doc.encodeStateAsUpdate();
483
+
462
484
  this._doc.destroy();
463
485
  this._doc = doc();
464
486
 
465
- this._listeners.onDestroy();
487
+ this._listeners.onDestroy(encodedState);
466
488
  };
467
489
  }
468
490
 
@@ -46,6 +46,10 @@ export class Persistance extends AbstractPersistance {
46
46
  return Promise.resolve();
47
47
  }
48
48
 
49
+ public getSize(room: string): Promise<number> {
50
+ return Promise.resolve(this._users.get(room)?.size ?? 0);
51
+ }
52
+
49
53
  public getStorageState(room: string): Promise<string | null> {
50
54
  const storage = this._storages.get(room) ?? null;
51
55
 
package/src/PluvIO.ts CHANGED
@@ -9,6 +9,8 @@ import type {
9
9
  JsonObject,
10
10
  Spread,
11
11
  BaseIOAuthorize,
12
+ MaybePromise,
13
+ Maybe,
12
14
  } from "@pluv/types";
13
15
  import chalk from "chalk";
14
16
  import type { AbstractPlatform } from "./AbstractPlatform";
@@ -38,20 +40,25 @@ export interface PluvIORegisterOptions {
38
40
  token?: string;
39
41
  }
40
42
 
41
- export interface PluvIOConfig<
43
+ export interface PluvIOListeners {
44
+ onRoomDeleted: (room: string, encodedState: string) => void;
45
+ onStorageUpdated: (room: string, encodedState: string) => void;
46
+ }
47
+
48
+ export type PluvIOConfig<
42
49
  TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
43
50
  TAuthorize extends IOAuthorize<any, any> | null = null,
44
51
  TContext extends JsonObject = {},
45
52
  TInput extends EventRecord<string, any> = {},
46
53
  TOutput extends EventRecord<string, any> = {}
47
- > {
54
+ > = Partial<PluvIOListeners> & {
48
55
  authorize?: TAuthorize;
49
56
  context?: TContext;
50
57
  debug?: boolean;
51
58
  events?: InferEventConfig<TContext, TInput, TOutput>;
59
+ initialStorage?: (room: string) => MaybePromise<Maybe<string>>;
52
60
  platform: TPlatform;
53
- room?: string;
54
- }
61
+ };
55
62
 
56
63
  export class PluvIO<
57
64
  TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
@@ -61,6 +68,16 @@ export class PluvIO<
61
68
  TOutput extends EventRecord<string, any> = {}
62
69
  > implements IOLike<TAuthorize, TInput, TOutput>
63
70
  {
71
+ private _context: TContext = {} as TContext;
72
+ private _initialStorage:
73
+ | ((room: string) => MaybePromise<Maybe<string>>)
74
+ | null = null;
75
+ private _listeners: PluvIOListeners;
76
+ private _rooms = new Map<
77
+ string,
78
+ IORoom<TPlatform, TAuthorize, TContext, TInput, TOutput>
79
+ >();
80
+
64
81
  readonly _authorize: TAuthorize | null = null;
65
82
  readonly _debug: boolean;
66
83
  readonly _events: InferEventConfig<TContext, TInput, TOutput> = {
@@ -77,7 +94,7 @@ export class PluvIO<
77
94
  options: { type: "self" },
78
95
  },
79
96
  $GET_OTHERS: {
80
- resolver: (data, { session, sessions }) => {
97
+ resolver: (data, { room, session, sessions }) => {
81
98
  const others = Array.from(sessions.entries())
82
99
  .filter(([, wsSession]) => wsSession.id !== session?.id)
83
100
  .reduce<{
@@ -92,7 +109,7 @@ export class PluvIO<
92
109
  [connectionId]: {
93
110
  connectionId,
94
111
  presence,
95
- room: this.room,
112
+ room,
96
113
  user: user as InferIOAuthorizeUser<TAuthorize>,
97
114
  },
98
115
  }),
@@ -128,7 +145,11 @@ export class PluvIO<
128
145
  const updated = doc.applyUpdate(update);
129
146
  const state = updated.encodeStateAsUpdate();
130
147
 
131
- this._platform.persistance.setStorageState(room, state);
148
+ this._platform.persistance
149
+ .setStorageState(room, state)
150
+ .then(() => {
151
+ this._listeners.onStorageUpdated(room, state);
152
+ });
132
153
 
133
154
  return { $STORAGE_UPDATED: { state } };
134
155
  },
@@ -136,14 +157,6 @@ export class PluvIO<
136
157
  } as InferEventConfig<TContext, TInput, TOutput>;
137
158
  readonly _platform: TPlatform;
138
159
 
139
- readonly room: string | null = null;
140
-
141
- private _context: TContext = {} as TContext;
142
- private _rooms = new Map<
143
- string,
144
- IORoom<TPlatform, TAuthorize, TContext, TInput, TOutput>
145
- >();
146
-
147
160
  constructor(
148
161
  options: PluvIOConfig<TPlatform, TAuthorize, TContext, TInput, TOutput>
149
162
  ) {
@@ -152,8 +165,10 @@ export class PluvIO<
152
165
  context,
153
166
  debug = false,
154
167
  events,
168
+ initialStorage,
169
+ onRoomDeleted,
170
+ onStorageUpdated,
155
171
  platform,
156
- room,
157
172
  } = options;
158
173
 
159
174
  this._debug = debug;
@@ -162,7 +177,16 @@ export class PluvIO<
162
177
  if (authorize) this._authorize = authorize;
163
178
  if (context) this._context = context;
164
179
  if (events) this._events = events;
165
- if (room) this.room = room;
180
+ if (initialStorage) this._initialStorage = initialStorage;
181
+
182
+ this._listeners = {
183
+ onRoomDeleted: (room, encodedState) => {
184
+ return onRoomDeleted?.(room, encodedState);
185
+ },
186
+ onStorageUpdated: (room, encodedState) => {
187
+ return onStorageUpdated?.(room, encodedState);
188
+ },
189
+ };
166
190
  }
167
191
 
168
192
  public async createToken(
@@ -231,10 +255,6 @@ export class PluvIO<
231
255
  public getRoom(
232
256
  room: string
233
257
  ): IORoom<TPlatform, TAuthorize, TContext, TInput, TOutput> {
234
- if (this.room) {
235
- throw new Error("IO cannot getRoom from within a room.");
236
- }
237
-
238
258
  if (!/^[a-z0-9]+[a-z0-9\-_]+[a-z0-9]+$/i.test(room)) {
239
259
  throw new Error("Unsupported room name");
240
260
  }
@@ -255,7 +275,8 @@ export class PluvIO<
255
275
  context: this._context,
256
276
  debug: this._debug,
257
277
  events: this._events,
258
- onDestroy: () => {
278
+ initialStorage: () => this._initialStorage?.(room),
279
+ onDestroy: (encodedState) => {
259
280
  this._logDebug(`${chalk.blue("Deleting empty room:")} ${room}`);
260
281
 
261
282
  this._rooms.delete(room);
@@ -268,6 +289,12 @@ export class PluvIO<
268
289
  `${chalk.blue("Rooms available:")} ${rooms.join(", ")}`
269
290
  );
270
291
  }
292
+
293
+ this._platform.persistance.getSize(room).then((size) => {
294
+ if (size) return;
295
+
296
+ this._listeners.onRoomDeleted(room, encodedState);
297
+ });
271
298
  },
272
299
  platform,
273
300
  });
@@ -300,7 +327,6 @@ export class PluvIO<
300
327
  undefined) as InferIOAuthorize<TIO1>;
301
328
  const debug = first._debug;
302
329
  const platform = first._platform as InferIOPlatform<TIO1>;
303
- const room = first.room ?? undefined;
304
330
 
305
331
  const events1 = first._events;
306
332
  const events2 = second._events;
@@ -312,7 +338,6 @@ export class PluvIO<
312
338
  debug,
313
339
  events,
314
340
  platform,
315
- room,
316
341
  });
317
342
  }
318
343
 
package/src/createIO.ts CHANGED
@@ -4,21 +4,25 @@ import type {
4
4
  BaseUser,
5
5
  IOAuthorize,
6
6
  JsonObject,
7
+ Maybe,
8
+ MaybePromise,
7
9
  } from "@pluv/types";
8
10
  import type { AbstractPlatform } from "./AbstractPlatform";
11
+ import type { PluvIOListeners } from "./PluvIO";
9
12
  import { PluvIO } from "./PluvIO";
10
13
 
11
- export interface CreateIOParams<
14
+ export type CreateIOParams<
12
15
  TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
13
16
  TContext extends JsonObject = {},
14
17
  TAuthorizeUser extends BaseUser = BaseUser,
15
18
  TAuthorizeRequired extends boolean = false
16
- > {
19
+ > = Partial<PluvIOListeners> & {
17
20
  authorize?: IOAuthorize<TAuthorizeUser, TAuthorizeRequired>;
18
21
  context?: TContext;
19
22
  debug?: boolean;
23
+ initialStorage?: (room: string) => MaybePromise<Maybe<string>>;
20
24
  platform: TPlatform;
21
- }
25
+ };
22
26
 
23
27
  export const createIO = <
24
28
  TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
@@ -39,7 +43,15 @@ export const createIO = <
39
43
  BaseClientEventRecord,
40
44
  BaseIOEventRecord<IOAuthorize<TAuthorizeUser, TAuthorizeRequired>>
41
45
  > => {
42
- const { authorize, context, debug, platform } = params;
46
+ const {
47
+ authorize,
48
+ context,
49
+ debug,
50
+ initialStorage,
51
+ onRoomDeleted,
52
+ onStorageUpdated,
53
+ platform,
54
+ } = params;
43
55
 
44
56
  return new PluvIO<
45
57
  TPlatform,
@@ -47,5 +59,13 @@ export const createIO = <
47
59
  TContext,
48
60
  BaseClientEventRecord,
49
61
  BaseIOEventRecord<IOAuthorize<TAuthorizeUser, TAuthorizeRequired>>
50
- >({ authorize, context, debug, platform });
62
+ >({
63
+ authorize,
64
+ context,
65
+ debug,
66
+ initialStorage,
67
+ onRoomDeleted,
68
+ onStorageUpdated,
69
+ platform,
70
+ });
51
71
  };
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ export type {
29
29
  JWT,
30
30
  JWTEncodeParams,
31
31
  } from "./authorize";
32
+ export type { CreateIOParams } from "./createIO";
32
33
  export { createIO } from "./createIO";
33
34
  export type { IORoom } from "./IORoom";
34
35
  export type { InferIORoom, PluvIO } from "./PluvIO";