@pluv/client 0.1.3 → 0.3.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @pluv/client@0.1.3 build /home/runner/work/pluv/pluv/packages/client
2
+ > @pluv/client@0.3.0 build /home/runner/work/pluv/pluv/packages/client
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 29.33 KB
12
- ESM ⚡️ Build success in 95ms
13
- CJS dist/index.js 30.41 KB
14
- CJS ⚡️ Build success in 96ms
11
+ CJS dist/index.js 34.07 KB
12
+ CJS ⚡️ Build success in 83ms
13
+ ESM dist/index.mjs 32.92 KB
14
+ ESM ⚡️ Build success in 88ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 3487ms
17
- DTS dist/index.d.ts 8.23 KB
16
+ DTS ⚡️ Build success in 3519ms
17
+ DTS dist/index.d.ts 11.52 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @pluv/client
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 595e66f: added mocked-room for mocking a client-side room for testing
8
+
9
+ ## 0.2.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 95b5ef8: breaking: update PluvRoom.broadcast api
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [6858682]
18
+ - @pluv/crdt-yjs@0.2.0
19
+ - @pluv/types@0.1.2
20
+
3
21
  ## 0.1.3
4
22
 
5
23
  ### Patch Changes
package/README.md CHANGED
@@ -26,7 +26,9 @@
26
26
  <img src="https://img.shields.io/badge/docs-coming%20soon!-blue" alt="Docs coming soon!" />
27
27
  </p>
28
28
 
29
- <img src="https://github.com/pluv-io/pluv/blob/master/assets/demo-events.gif?raw=true" alt="Demo" />
29
+ <p align="center">
30
+ <img src="https://github.com/pluv-io/pluv/blob/master/assets/demo-events.gif?raw=true" alt="Demo" />
31
+ </p>
30
32
 
31
33
  ## `@pluv/client`
32
34
 
package/dist/index.d.ts CHANGED
@@ -1,17 +1,12 @@
1
1
  import { InferYjsSharedTypeJson, array, map, text, unstable__object } from '@pluv/crdt-yjs';
2
2
  export { InferYjsSharedTypeJson } from '@pluv/crdt-yjs';
3
3
  import * as _pluv_types from '@pluv/types';
4
- import { IOLike, InferIOOutput, Id, IOEventMessage, JsonObject, InferIOAuthorizeUser, InferIOAuthorize, InputZodLike, InferIOInput, InferEventMessage, InferIOAuthorizeRequired } from '@pluv/types';
4
+ import { IOLike, InferIOOutput, Id, IOEventMessage, JsonObject, InferIOAuthorizeUser, InferIOAuthorize, InferIOInput, InputZodLike, InferIOAuthorizeRequired } from '@pluv/types';
5
5
  export { BaseUser, EventMessage, EventRecord, IOEventMessage } from '@pluv/types';
6
6
  import { AbstractType } from 'yjs';
7
7
  export { AbstractType } from 'yjs';
8
8
  import { Subject } from 'wonka';
9
9
 
10
- interface CrdtManagerOptions<TStorage extends Record<string, AbstractType<any>> = {}> {
11
- encodedState?: string | Uint8Array | null;
12
- initialStorage?: () => TStorage;
13
- }
14
-
15
10
  type EventNotifierSubscriptionCallback<TIO extends IOLike, TEvent extends keyof InferIOOutput<TIO>> = (value: Id<IOEventMessage<TIO, TEvent>>) => void;
16
11
 
17
12
  interface AuthorizationState<TIO extends IOLike> {
@@ -51,6 +46,27 @@ interface StateNotifierSubjects<TIO extends IOLike, TPresence extends JsonObject
51
46
  type InferSubjectValue<TIO extends IOLike, TPresence extends JsonObject, TSubject extends keyof StateNotifierSubjects<TIO, TPresence>> = StateNotifierSubjects<TIO, TPresence>[TSubject] extends Subject<infer IValue> ? Id<IValue> : never;
52
47
  type SubscriptionCallback<TIO extends IOLike, TPresence extends JsonObject, TSubject extends keyof StateNotifierSubjects<TIO, TPresence>> = (value: InferSubjectValue<TIO, TPresence, TSubject>) => void;
53
48
 
49
+ declare abstract class AbstractRoom<TIO extends IOLike, TPresence extends JsonObject = {}, TStorage extends Record<string, AbstractType<any>> = {}> {
50
+ readonly id: string;
51
+ constructor(room: string);
52
+ abstract broadcast<TEvent extends keyof InferIOInput<TIO>>(event: TEvent, data: Id<InferIOInput<TIO>[TEvent]>): void;
53
+ abstract event<TEvent extends keyof InferIOOutput<TIO>>(event: TEvent, callback: EventNotifierSubscriptionCallback<TIO, TEvent>): () => void;
54
+ abstract getConnection(): WebSocketConnection;
55
+ abstract getMyPresence(): TPresence | null;
56
+ abstract getMyself(): Id<UserInfo<TIO, TPresence>> | null;
57
+ abstract getOther(connectionId: string): Id<UserInfo<TIO, TPresence>> | null;
58
+ abstract getOthers(): readonly Id<UserInfo<TIO, TPresence>>[];
59
+ abstract getStorage<TKey extends keyof TStorage>(key: TKey): TStorage[TKey] | null;
60
+ abstract other(connectionId: string, callback: OtherNotifierSubscriptionCallback<TIO>): () => void;
61
+ abstract storage<TKey extends keyof TStorage>(key: TKey, fn: (value: InferYjsSharedTypeJson<TStorage[TKey]>) => void): () => void;
62
+ abstract subscribe<TSubject extends keyof StateNotifierSubjects<TIO, TPresence>>(name: TSubject, callback: SubscriptionCallback<TIO, TPresence, TSubject>): () => void;
63
+ }
64
+
65
+ interface CrdtManagerOptions<TStorage extends Record<string, AbstractType<any>> = {}> {
66
+ encodedState?: string | Uint8Array | null;
67
+ initialStorage?: () => TStorage;
68
+ }
69
+
54
70
  type UsersManagerConfig<TPresence extends JsonObject = {}> = {
55
71
  initialPresence?: TPresence;
56
72
  presence?: InputZodLike<TPresence>;
@@ -85,9 +101,8 @@ type PluvRoomOptions<TIO extends IOLike, TPresence extends JsonObject = {}, TSto
85
101
  onAuthorizationFail?: (error: Error) => void;
86
102
  } & Omit<CrdtManagerOptions<TStorage>, "encodedState"> & UsersManagerConfig<TPresence>;
87
103
  type RoomConfig<TIO extends IOLike, TPresence extends JsonObject = {}, TStorage extends Record<string, AbstractType<any>> = {}> = RoomEndpoints<TIO> & PluvRoomOptions<TIO, TPresence, TStorage>;
88
- declare class PluvRoom<TIO extends IOLike, TPresence extends JsonObject = {}, TStorage extends Record<string, AbstractType<any>> = {}> {
104
+ declare class PluvRoom<TIO extends IOLike, TPresence extends JsonObject = {}, TStorage extends Record<string, AbstractType<any>> = {}> extends AbstractRoom<TIO, TPresence, TStorage> {
89
105
  readonly _endpoints: RoomEndpoints<TIO>;
90
- readonly id: string;
91
106
  private _crdtManager;
92
107
  private _crdtNotifier;
93
108
  private _debug;
@@ -105,10 +120,10 @@ declare class PluvRoom<TIO extends IOLike, TPresence extends JsonObject = {}, TS
105
120
  private _usersManager;
106
121
  constructor(room: string, options: RoomConfig<TIO, TPresence, TStorage>);
107
122
  get webSocket(): WebSocket | null;
108
- broadcast(message: Id<InferEventMessage<InferIOInput<TIO>>>): void;
123
+ broadcast<TEvent extends keyof InferIOInput<TIO>>(event: TEvent, data: Id<InferIOInput<TIO>[TEvent]>): void;
109
124
  connect(): Promise<void>;
110
125
  disconnect(): void;
111
- event: <TEvent extends keyof InferIOOutput<TIO>>(name: TEvent, callback: EventNotifierSubscriptionCallback<TIO, TEvent>) => (() => void);
126
+ event: <TEvent extends keyof InferIOOutput<TIO>>(event: TEvent, callback: EventNotifierSubscriptionCallback<TIO, TEvent>) => (() => void);
112
127
  getConnection(): WebSocketConnection;
113
128
  getMyPresence: () => TPresence | null;
114
129
  getMyself: () => Id<UserInfo<TIO, TPresence>> | null;
@@ -182,6 +197,37 @@ declare class PluvClient<TIO extends IOLike = IOLike> {
182
197
 
183
198
  declare const createClient: <TIO extends IOLike<_pluv_types.IOAuthorize<any, any>, {}, {}> = IOLike<_pluv_types.IOAuthorize<any, any>, {}, {}>>(options: PluvClientOptions<TIO>) => PluvClient<TIO>;
184
199
 
200
+ type MockedRoomEvents<TIO extends IOLike> = {
201
+ [P in keyof InferIOInput<TIO>]: (data: Id<InferIOInput<TIO>[P]>) => Partial<InferIOOutput<TIO>>;
202
+ };
203
+ type MockedRoomConfig<TIO extends IOLike, TPresence extends JsonObject = {}, TStorage extends Record<string, AbstractType<any>> = {}> = {
204
+ events?: MockedRoomEvents<TIO>;
205
+ } & Omit<CrdtManagerOptions<TStorage>, "encodedState"> & UsersManagerConfig<TPresence>;
206
+ declare class MockedRoom<TIO extends IOLike, TPresence extends JsonObject = {}, TStorage extends Record<string, AbstractType<any>> = {}> extends AbstractRoom<TIO, TPresence, TStorage> {
207
+ private _crdtManager;
208
+ private _crdtNotifier;
209
+ private _eventNotifier;
210
+ private _events?;
211
+ private _otherNotifier;
212
+ private _state;
213
+ private _stateNotifier;
214
+ private _subscriptions;
215
+ private _usersManager;
216
+ constructor(room: string, options: MockedRoomConfig<TIO, TPresence, TStorage>);
217
+ broadcast<TEvent extends keyof InferIOInput<TIO>>(event: TEvent, data: Id<InferIOInput<TIO>[TEvent]>): void;
218
+ event: <TEvent extends keyof InferIOOutput<TIO>>(event: TEvent, callback: EventNotifierSubscriptionCallback<TIO, TEvent>) => (() => void);
219
+ getConnection(): WebSocketConnection;
220
+ getMyPresence: () => TPresence | null;
221
+ getMyself: () => Id<UserInfo<TIO, TPresence>> | null;
222
+ getOther: (connectionId: string) => Id<UserInfo<TIO, TPresence>> | null;
223
+ getOthers: () => readonly Id<UserInfo<TIO, TPresence>>[];
224
+ getStorage: <TKey extends keyof TStorage>(key: TKey) => TStorage[TKey] | null;
225
+ other: (connectionId: string, callback: OtherNotifierSubscriptionCallback<TIO>) => (() => void);
226
+ storage: <TKey extends keyof TStorage>(key: TKey, fn: (value: InferYjsSharedTypeJson<TStorage[TKey]>) => void) => (() => void);
227
+ subscribe: <TSubject extends keyof StateNotifierSubjects<TIO, TPresence>>(name: TSubject, callback: SubscriptionCallback<TIO, TPresence, TSubject>) => (() => void);
228
+ private _observeCrdt;
229
+ }
230
+
185
231
  declare const y_array: typeof array;
186
232
  declare const y_map: typeof map;
187
233
  declare const y_text: typeof text;
@@ -195,4 +241,4 @@ declare namespace y {
195
241
  };
196
242
  }
197
243
 
198
- export { ConnectionState, PluvClient, PluvClientOptions, PluvRoom, PluvRoomDebug, UserInfo, WebSocketConnection, WebSocketState, createClient, y };
244
+ export { AbstractRoom, ConnectionState, MockedRoom, MockedRoomConfig, MockedRoomEvents, PluvClient, PluvClientOptions, PluvRoom, PluvRoomDebug, UserInfo, WebSocketConnection, WebSocketState, createClient, y };
package/dist/index.js CHANGED
@@ -57,7 +57,9 @@ var __async = (__this, __arguments, generator) => {
57
57
  // src/index.ts
58
58
  var src_exports = {};
59
59
  __export(src_exports, {
60
+ AbstractRoom: () => AbstractRoom,
60
61
  ConnectionState: () => ConnectionState,
62
+ MockedRoom: () => MockedRoom,
61
63
  PluvClient: () => PluvClient,
62
64
  PluvRoom: () => PluvRoom,
63
65
  createClient: () => createClient,
@@ -65,6 +67,13 @@ __export(src_exports, {
65
67
  });
66
68
  module.exports = __toCommonJS(src_exports);
67
69
 
70
+ // src/AbstractRoom.ts
71
+ var AbstractRoom = class {
72
+ constructor(room) {
73
+ this.id = room;
74
+ }
75
+ };
76
+
68
77
  // src/PluvRoom.ts
69
78
  var import_crdt_yjs2 = require("@pluv/crdt-yjs");
70
79
 
@@ -267,8 +276,18 @@ var UsersManager = class {
267
276
  var HEARTBEAT_INTERVAL_MS = 1e4;
268
277
  var PONG_TIMEOUT_MS = 2e3;
269
278
  var RECONNECT_TIMEOUT_MS = 3e4;
270
- var PluvRoom = class {
279
+ var PluvRoom = class extends AbstractRoom {
271
280
  constructor(room, options) {
281
+ const {
282
+ authEndpoint,
283
+ debug = false,
284
+ initialPresence,
285
+ initialStorage,
286
+ onAuthorizationFail,
287
+ presence,
288
+ wsEndpoint
289
+ } = options;
290
+ super(room);
272
291
  this._crdtManager = null;
273
292
  this._crdtNotifier = new CrdtNotifier();
274
293
  this._eventNotifier = new EventNotifier();
@@ -298,8 +317,8 @@ var PluvRoom = class {
298
317
  };
299
318
  this._windowListeners = null;
300
319
  this._wsListeners = null;
301
- this.event = (name, callback) => {
302
- return this._eventNotifier.subscribe(name, callback);
320
+ this.event = (event, callback) => {
321
+ return this._eventNotifier.subscribe(event, callback);
303
322
  };
304
323
  this.getMyPresence = () => {
305
324
  var _a, _b;
@@ -327,16 +346,6 @@ var PluvRoom = class {
327
346
  this.subscribe = (name, callback) => {
328
347
  return this._stateNotifier.subscribe(name, callback);
329
348
  };
330
- const {
331
- authEndpoint,
332
- debug = false,
333
- initialPresence,
334
- initialStorage,
335
- onAuthorizationFail,
336
- presence,
337
- wsEndpoint
338
- } = options;
339
- this.id = room;
340
349
  this._debug = debug;
341
350
  this._endpoints = {
342
351
  authEndpoint,
@@ -362,12 +371,13 @@ var PluvRoom = class {
362
371
  get webSocket() {
363
372
  return this._state.webSocket;
364
373
  }
365
- broadcast(message) {
374
+ broadcast(event, data) {
366
375
  if (!this._state.webSocket)
367
376
  return;
368
377
  if (this._state.connection.state !== "Open" /* Open */)
369
378
  return;
370
- this._sendMessage(message);
379
+ const type = event.toString();
380
+ this._sendMessage({ data, type });
371
381
  }
372
382
  connect() {
373
383
  return __async(this, null, function* () {
@@ -997,6 +1007,123 @@ var createClient = (options) => {
997
1007
  return new PluvClient(options);
998
1008
  };
999
1009
 
1010
+ // src/MockedRoom.ts
1011
+ var MockedRoom = class extends AbstractRoom {
1012
+ constructor(room, options) {
1013
+ const { events, initialPresence, initialStorage, presence } = options;
1014
+ super(room);
1015
+ this._crdtManager = null;
1016
+ this._crdtNotifier = new CrdtNotifier();
1017
+ this._eventNotifier = new EventNotifier();
1018
+ this._otherNotifier = new OtherNotifier();
1019
+ this._state = {
1020
+ authorization: {
1021
+ token: null,
1022
+ user: null
1023
+ },
1024
+ connection: {
1025
+ id: null,
1026
+ state: "Untouched" /* Untouched */
1027
+ },
1028
+ webSocket: null
1029
+ };
1030
+ this._stateNotifier = new StateNotifier();
1031
+ this._subscriptions = {
1032
+ observeCrdt: null
1033
+ };
1034
+ this.event = (event, callback) => {
1035
+ return this._eventNotifier.subscribe(event, callback);
1036
+ };
1037
+ this.getMyPresence = () => {
1038
+ var _a, _b;
1039
+ return (_b = (_a = this._usersManager.myself) == null ? void 0 : _a.presence) != null ? _b : null;
1040
+ };
1041
+ this.getMyself = () => {
1042
+ return this._usersManager.myself;
1043
+ };
1044
+ this.getOther = (connectionId) => {
1045
+ return this._usersManager.getOther(connectionId);
1046
+ };
1047
+ this.getOthers = () => {
1048
+ return this._usersManager.getOthers();
1049
+ };
1050
+ this.getStorage = (key) => {
1051
+ var _a, _b;
1052
+ return (_b = (_a = this._crdtManager) == null ? void 0 : _a.get(key)) != null ? _b : null;
1053
+ };
1054
+ this.other = (connectionId, callback) => {
1055
+ return this._otherNotifier.subscribe(connectionId, callback);
1056
+ };
1057
+ this.storage = (key, fn) => {
1058
+ return this._crdtNotifier.subscribe(key, fn);
1059
+ };
1060
+ this.subscribe = (name, callback) => {
1061
+ return this._stateNotifier.subscribe(name, callback);
1062
+ };
1063
+ this._events = events;
1064
+ this._usersManager = new UsersManager({
1065
+ initialPresence,
1066
+ presence
1067
+ });
1068
+ this._crdtManager = new CrdtManager({
1069
+ initialStorage
1070
+ });
1071
+ this._observeCrdt();
1072
+ }
1073
+ broadcast(event, data) {
1074
+ if (!this._events)
1075
+ return;
1076
+ const resolver = this._events[event];
1077
+ if (!resolver)
1078
+ return;
1079
+ const result = resolver(data);
1080
+ Object.keys(result).forEach((type) => {
1081
+ const _type = type.toString();
1082
+ const data2 = result[_type];
1083
+ if (!data2)
1084
+ return;
1085
+ this._eventNotifier.subject(_type).next(data2);
1086
+ });
1087
+ }
1088
+ getConnection() {
1089
+ return Object.freeze(
1090
+ JSON.parse(JSON.stringify(this._state.connection))
1091
+ );
1092
+ }
1093
+ _observeCrdt() {
1094
+ var _a, _b;
1095
+ (_b = (_a = this._subscriptions).observeCrdt) == null ? void 0 : _b.call(_a);
1096
+ if (!this._crdtManager)
1097
+ return;
1098
+ const unsubscribe = this._crdtManager.doc.subscribe(
1099
+ (update, _origin) => {
1100
+ const origin = _origin != null ? _origin : null;
1101
+ if (!this._crdtManager)
1102
+ return;
1103
+ if (!this._state.webSocket)
1104
+ return;
1105
+ if (!this._state.connection.id)
1106
+ return;
1107
+ if (this._state.webSocket.readyState !== WebSocket.OPEN)
1108
+ return;
1109
+ if (origin === "$STORAGE_UPDATED")
1110
+ return;
1111
+ const sharedTypes = this._crdtManager.doc.getSharedTypes();
1112
+ Object.keys(sharedTypes).forEach((key) => {
1113
+ if (!this._crdtManager)
1114
+ return;
1115
+ const sharedType = sharedTypes[key];
1116
+ if (!sharedType)
1117
+ return;
1118
+ const serialized = sharedType.toJSON();
1119
+ this._crdtNotifier.subject(key).next(serialized);
1120
+ });
1121
+ }
1122
+ );
1123
+ this._subscriptions.observeCrdt = unsubscribe;
1124
+ }
1125
+ };
1126
+
1000
1127
  // src/y.ts
1001
1128
  var y_exports = {};
1002
1129
  __export(y_exports, {
@@ -1008,7 +1135,9 @@ __export(y_exports, {
1008
1135
  var import_crdt_yjs3 = require("@pluv/crdt-yjs");
1009
1136
  // Annotate the CommonJS export names for ESM import in node:
1010
1137
  0 && (module.exports = {
1138
+ AbstractRoom,
1011
1139
  ConnectionState,
1140
+ MockedRoom,
1012
1141
  PluvClient,
1013
1142
  PluvRoom,
1014
1143
  createClient,
package/dist/index.mjs CHANGED
@@ -42,6 +42,13 @@ var __async = (__this, __arguments, generator) => {
42
42
  });
43
43
  };
44
44
 
45
+ // src/AbstractRoom.ts
46
+ var AbstractRoom = class {
47
+ constructor(room) {
48
+ this.id = room;
49
+ }
50
+ };
51
+
45
52
  // src/PluvRoom.ts
46
53
  import { doc as doc2 } from "@pluv/crdt-yjs";
47
54
 
@@ -244,8 +251,18 @@ var UsersManager = class {
244
251
  var HEARTBEAT_INTERVAL_MS = 1e4;
245
252
  var PONG_TIMEOUT_MS = 2e3;
246
253
  var RECONNECT_TIMEOUT_MS = 3e4;
247
- var PluvRoom = class {
254
+ var PluvRoom = class extends AbstractRoom {
248
255
  constructor(room, options) {
256
+ const {
257
+ authEndpoint,
258
+ debug = false,
259
+ initialPresence,
260
+ initialStorage,
261
+ onAuthorizationFail,
262
+ presence,
263
+ wsEndpoint
264
+ } = options;
265
+ super(room);
249
266
  this._crdtManager = null;
250
267
  this._crdtNotifier = new CrdtNotifier();
251
268
  this._eventNotifier = new EventNotifier();
@@ -275,8 +292,8 @@ var PluvRoom = class {
275
292
  };
276
293
  this._windowListeners = null;
277
294
  this._wsListeners = null;
278
- this.event = (name, callback) => {
279
- return this._eventNotifier.subscribe(name, callback);
295
+ this.event = (event, callback) => {
296
+ return this._eventNotifier.subscribe(event, callback);
280
297
  };
281
298
  this.getMyPresence = () => {
282
299
  var _a, _b;
@@ -304,16 +321,6 @@ var PluvRoom = class {
304
321
  this.subscribe = (name, callback) => {
305
322
  return this._stateNotifier.subscribe(name, callback);
306
323
  };
307
- const {
308
- authEndpoint,
309
- debug = false,
310
- initialPresence,
311
- initialStorage,
312
- onAuthorizationFail,
313
- presence,
314
- wsEndpoint
315
- } = options;
316
- this.id = room;
317
324
  this._debug = debug;
318
325
  this._endpoints = {
319
326
  authEndpoint,
@@ -339,12 +346,13 @@ var PluvRoom = class {
339
346
  get webSocket() {
340
347
  return this._state.webSocket;
341
348
  }
342
- broadcast(message) {
349
+ broadcast(event, data) {
343
350
  if (!this._state.webSocket)
344
351
  return;
345
352
  if (this._state.connection.state !== "Open" /* Open */)
346
353
  return;
347
- this._sendMessage(message);
354
+ const type = event.toString();
355
+ this._sendMessage({ data, type });
348
356
  }
349
357
  connect() {
350
358
  return __async(this, null, function* () {
@@ -974,6 +982,123 @@ var createClient = (options) => {
974
982
  return new PluvClient(options);
975
983
  };
976
984
 
985
+ // src/MockedRoom.ts
986
+ var MockedRoom = class extends AbstractRoom {
987
+ constructor(room, options) {
988
+ const { events, initialPresence, initialStorage, presence } = options;
989
+ super(room);
990
+ this._crdtManager = null;
991
+ this._crdtNotifier = new CrdtNotifier();
992
+ this._eventNotifier = new EventNotifier();
993
+ this._otherNotifier = new OtherNotifier();
994
+ this._state = {
995
+ authorization: {
996
+ token: null,
997
+ user: null
998
+ },
999
+ connection: {
1000
+ id: null,
1001
+ state: "Untouched" /* Untouched */
1002
+ },
1003
+ webSocket: null
1004
+ };
1005
+ this._stateNotifier = new StateNotifier();
1006
+ this._subscriptions = {
1007
+ observeCrdt: null
1008
+ };
1009
+ this.event = (event, callback) => {
1010
+ return this._eventNotifier.subscribe(event, callback);
1011
+ };
1012
+ this.getMyPresence = () => {
1013
+ var _a, _b;
1014
+ return (_b = (_a = this._usersManager.myself) == null ? void 0 : _a.presence) != null ? _b : null;
1015
+ };
1016
+ this.getMyself = () => {
1017
+ return this._usersManager.myself;
1018
+ };
1019
+ this.getOther = (connectionId) => {
1020
+ return this._usersManager.getOther(connectionId);
1021
+ };
1022
+ this.getOthers = () => {
1023
+ return this._usersManager.getOthers();
1024
+ };
1025
+ this.getStorage = (key) => {
1026
+ var _a, _b;
1027
+ return (_b = (_a = this._crdtManager) == null ? void 0 : _a.get(key)) != null ? _b : null;
1028
+ };
1029
+ this.other = (connectionId, callback) => {
1030
+ return this._otherNotifier.subscribe(connectionId, callback);
1031
+ };
1032
+ this.storage = (key, fn) => {
1033
+ return this._crdtNotifier.subscribe(key, fn);
1034
+ };
1035
+ this.subscribe = (name, callback) => {
1036
+ return this._stateNotifier.subscribe(name, callback);
1037
+ };
1038
+ this._events = events;
1039
+ this._usersManager = new UsersManager({
1040
+ initialPresence,
1041
+ presence
1042
+ });
1043
+ this._crdtManager = new CrdtManager({
1044
+ initialStorage
1045
+ });
1046
+ this._observeCrdt();
1047
+ }
1048
+ broadcast(event, data) {
1049
+ if (!this._events)
1050
+ return;
1051
+ const resolver = this._events[event];
1052
+ if (!resolver)
1053
+ return;
1054
+ const result = resolver(data);
1055
+ Object.keys(result).forEach((type) => {
1056
+ const _type = type.toString();
1057
+ const data2 = result[_type];
1058
+ if (!data2)
1059
+ return;
1060
+ this._eventNotifier.subject(_type).next(data2);
1061
+ });
1062
+ }
1063
+ getConnection() {
1064
+ return Object.freeze(
1065
+ JSON.parse(JSON.stringify(this._state.connection))
1066
+ );
1067
+ }
1068
+ _observeCrdt() {
1069
+ var _a, _b;
1070
+ (_b = (_a = this._subscriptions).observeCrdt) == null ? void 0 : _b.call(_a);
1071
+ if (!this._crdtManager)
1072
+ return;
1073
+ const unsubscribe = this._crdtManager.doc.subscribe(
1074
+ (update, _origin) => {
1075
+ const origin = _origin != null ? _origin : null;
1076
+ if (!this._crdtManager)
1077
+ return;
1078
+ if (!this._state.webSocket)
1079
+ return;
1080
+ if (!this._state.connection.id)
1081
+ return;
1082
+ if (this._state.webSocket.readyState !== WebSocket.OPEN)
1083
+ return;
1084
+ if (origin === "$STORAGE_UPDATED")
1085
+ return;
1086
+ const sharedTypes = this._crdtManager.doc.getSharedTypes();
1087
+ Object.keys(sharedTypes).forEach((key) => {
1088
+ if (!this._crdtManager)
1089
+ return;
1090
+ const sharedType = sharedTypes[key];
1091
+ if (!sharedType)
1092
+ return;
1093
+ const serialized = sharedType.toJSON();
1094
+ this._crdtNotifier.subject(key).next(serialized);
1095
+ });
1096
+ }
1097
+ );
1098
+ this._subscriptions.observeCrdt = unsubscribe;
1099
+ }
1100
+ };
1101
+
977
1102
  // src/y.ts
978
1103
  var y_exports = {};
979
1104
  __export(y_exports, {
@@ -984,7 +1109,9 @@ __export(y_exports, {
984
1109
  });
985
1110
  import { array, map, text, unstable__object } from "@pluv/crdt-yjs";
986
1111
  export {
1112
+ AbstractRoom,
987
1113
  ConnectionState,
1114
+ MockedRoom,
988
1115
  PluvClient,
989
1116
  PluvRoom,
990
1117
  createClient,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pluv/client",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "description": "@pluv/io frontend client",
5
5
  "author": "leedavidcs",
6
6
  "license": "MIT",
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "wonka": "^6.1.2",
21
- "@pluv/crdt-yjs": "^0.1.3",
21
+ "@pluv/crdt-yjs": "^0.2.0",
22
22
  "@pluv/types": "^0.1.2"
23
23
  },
24
24
  "peerDependencies": {
@@ -29,7 +29,7 @@
29
29
  "typescript": "^4.9.4",
30
30
  "yjs": "^13.5.44",
31
31
  "@pluv/tsconfig": "^0.1.2",
32
- "eslint-config-pluv": "^0.1.2"
32
+ "eslint-config-pluv": "^0.1.3"
33
33
  },
34
34
  "scripts": {
35
35
  "build": "tsup src/index.ts --format esm,cjs --dts",
@@ -0,0 +1,71 @@
1
+ import type { InferYjsSharedTypeJson } from "@pluv/crdt-yjs";
2
+ import type {
3
+ Id,
4
+ InferIOInput,
5
+ InferIOOutput,
6
+ IOLike,
7
+ JsonObject,
8
+ } from "@pluv/types";
9
+ import type { AbstractType } from "yjs";
10
+ import type { EventNotifierSubscriptionCallback } from "./EventNotifier";
11
+ import type { OtherNotifierSubscriptionCallback } from "./OtherNotifier";
12
+ import type {
13
+ StateNotifierSubjects,
14
+ SubscriptionCallback,
15
+ } from "./StateNotifier";
16
+ import type { UserInfo, WebSocketConnection } from "./types";
17
+
18
+ export abstract class AbstractRoom<
19
+ TIO extends IOLike,
20
+ TPresence extends JsonObject = {},
21
+ TStorage extends Record<string, AbstractType<any>> = {}
22
+ > {
23
+ public readonly id: string;
24
+
25
+ constructor(room: string) {
26
+ this.id = room;
27
+ }
28
+
29
+ public abstract broadcast<TEvent extends keyof InferIOInput<TIO>>(
30
+ event: TEvent,
31
+ data: Id<InferIOInput<TIO>[TEvent]>
32
+ ): void;
33
+
34
+ public abstract event<TEvent extends keyof InferIOOutput<TIO>>(
35
+ event: TEvent,
36
+ callback: EventNotifierSubscriptionCallback<TIO, TEvent>
37
+ ): () => void;
38
+
39
+ public abstract getConnection(): WebSocketConnection;
40
+
41
+ public abstract getMyPresence(): TPresence | null;
42
+
43
+ public abstract getMyself(): Id<UserInfo<TIO, TPresence>> | null;
44
+
45
+ public abstract getOther(
46
+ connectionId: string
47
+ ): Id<UserInfo<TIO, TPresence>> | null;
48
+
49
+ public abstract getOthers(): readonly Id<UserInfo<TIO, TPresence>>[];
50
+
51
+ public abstract getStorage<TKey extends keyof TStorage>(
52
+ key: TKey
53
+ ): TStorage[TKey] | null;
54
+
55
+ public abstract other(
56
+ connectionId: string,
57
+ callback: OtherNotifierSubscriptionCallback<TIO>
58
+ ): () => void;
59
+
60
+ public abstract storage<TKey extends keyof TStorage>(
61
+ key: TKey,
62
+ fn: (value: InferYjsSharedTypeJson<TStorage[TKey]>) => void
63
+ ): () => void;
64
+
65
+ public abstract subscribe<
66
+ TSubject extends keyof StateNotifierSubjects<TIO, TPresence>
67
+ >(
68
+ name: TSubject,
69
+ callback: SubscriptionCallback<TIO, TPresence, TSubject>
70
+ ): () => void;
71
+ }
@@ -0,0 +1,214 @@
1
+ import type { InferYjsSharedTypeJson } from "@pluv/crdt-yjs";
2
+ import type {
3
+ Id,
4
+ InferIOInput,
5
+ InferIOOutput,
6
+ IOLike,
7
+ JsonObject,
8
+ } from "@pluv/types";
9
+ import type { AbstractType } from "yjs";
10
+ import { AbstractRoom } from "./AbstractRoom";
11
+ import { CrdtManager, CrdtManagerOptions } from "./CrdtManager";
12
+ import { CrdtNotifier } from "./CrdtNotifier";
13
+ import type { EventNotifierSubscriptionCallback } from "./EventNotifier";
14
+ import { EventNotifier } from "./EventNotifier";
15
+ import type { OtherNotifierSubscriptionCallback } from "./OtherNotifier";
16
+ import { OtherNotifier } from "./OtherNotifier";
17
+ import type {
18
+ StateNotifierSubjects,
19
+ SubscriptionCallback,
20
+ } from "./StateNotifier";
21
+ import { StateNotifier } from "./StateNotifier";
22
+ import type {
23
+ InternalSubscriptions,
24
+ UserInfo,
25
+ WebSocketConnection,
26
+ WebSocketState,
27
+ } from "./types";
28
+ import { ConnectionState } from "./types";
29
+ import type { UsersManagerConfig } from "./UsersManager";
30
+ import { UsersManager } from "./UsersManager";
31
+
32
+ export type MockedRoomEvents<TIO extends IOLike> = {
33
+ [P in keyof InferIOInput<TIO>]: (
34
+ data: Id<InferIOInput<TIO>[P]>
35
+ ) => Partial<InferIOOutput<TIO>>;
36
+ };
37
+
38
+ export type MockedRoomConfig<
39
+ TIO extends IOLike,
40
+ TPresence extends JsonObject = {},
41
+ TStorage extends Record<string, AbstractType<any>> = {}
42
+ > = { events?: MockedRoomEvents<TIO> } & Omit<
43
+ CrdtManagerOptions<TStorage>,
44
+ "encodedState"
45
+ > &
46
+ UsersManagerConfig<TPresence>;
47
+
48
+ export class MockedRoom<
49
+ TIO extends IOLike,
50
+ TPresence extends JsonObject = {},
51
+ TStorage extends Record<string, AbstractType<any>> = {}
52
+ > extends AbstractRoom<TIO, TPresence, TStorage> {
53
+ private _crdtManager: CrdtManager<TStorage> | null = null;
54
+ private _crdtNotifier = new CrdtNotifier<TStorage>();
55
+ private _eventNotifier = new EventNotifier<TIO>();
56
+ private _events?: MockedRoomEvents<TIO>;
57
+ private _otherNotifier = new OtherNotifier<TIO>();
58
+ private _state: WebSocketState<TIO> = {
59
+ authorization: {
60
+ token: null,
61
+ user: null,
62
+ },
63
+ connection: {
64
+ id: null,
65
+ state: ConnectionState.Untouched,
66
+ },
67
+ webSocket: null,
68
+ };
69
+ private _stateNotifier = new StateNotifier<TIO, TPresence>();
70
+ private _subscriptions: InternalSubscriptions = {
71
+ observeCrdt: null,
72
+ };
73
+ private _usersManager: UsersManager<TIO, TPresence>;
74
+
75
+ constructor(
76
+ room: string,
77
+ options: MockedRoomConfig<TIO, TPresence, TStorage>
78
+ ) {
79
+ const { events, initialPresence, initialStorage, presence } = options;
80
+
81
+ super(room);
82
+
83
+ this._events = events;
84
+
85
+ this._usersManager = new UsersManager<TIO, TPresence>({
86
+ initialPresence,
87
+ presence,
88
+ });
89
+
90
+ this._crdtManager = new CrdtManager<TStorage>({
91
+ initialStorage,
92
+ });
93
+
94
+ this._observeCrdt();
95
+ }
96
+
97
+ public broadcast<TEvent extends keyof InferIOInput<TIO>>(
98
+ event: TEvent,
99
+ data: Id<InferIOInput<TIO>[TEvent]>
100
+ ): void {
101
+ if (!this._events) return;
102
+
103
+ const resolver = this._events[event];
104
+
105
+ if (!resolver) return;
106
+
107
+ const result = resolver(data);
108
+
109
+ Object.keys(result).forEach((type) => {
110
+ const _type = type.toString() as keyof Partial<InferIOOutput<TIO>>;
111
+ const data = result[_type] as any;
112
+
113
+ if (!data) return;
114
+
115
+ this._eventNotifier.subject(_type).next(data);
116
+ });
117
+ }
118
+
119
+ public event = <TEvent extends keyof InferIOOutput<TIO>>(
120
+ event: TEvent,
121
+ callback: EventNotifierSubscriptionCallback<TIO, TEvent>
122
+ ): (() => void) => {
123
+ return this._eventNotifier.subscribe(event, callback);
124
+ };
125
+
126
+ public getConnection(): WebSocketConnection {
127
+ // Create a read-only clone of the connection state
128
+ return Object.freeze(
129
+ JSON.parse(JSON.stringify(this._state.connection))
130
+ );
131
+ }
132
+
133
+ public getMyPresence = (): TPresence | null => {
134
+ return this._usersManager.myself?.presence ?? null;
135
+ };
136
+
137
+ public getMyself = (): Id<UserInfo<TIO, TPresence>> | null => {
138
+ return this._usersManager.myself;
139
+ };
140
+
141
+ public getOther = (
142
+ connectionId: string
143
+ ): Id<UserInfo<TIO, TPresence>> | null => {
144
+ return this._usersManager.getOther(connectionId);
145
+ };
146
+
147
+ public getOthers = (): readonly Id<UserInfo<TIO, TPresence>>[] => {
148
+ return this._usersManager.getOthers();
149
+ };
150
+
151
+ public getStorage = <TKey extends keyof TStorage>(
152
+ key: TKey
153
+ ): TStorage[TKey] | null => {
154
+ return this._crdtManager?.get(key) ?? null;
155
+ };
156
+
157
+ public other = (
158
+ connectionId: string,
159
+ callback: OtherNotifierSubscriptionCallback<TIO>
160
+ ): (() => void) => {
161
+ return this._otherNotifier.subscribe(connectionId, callback);
162
+ };
163
+
164
+ public storage = <TKey extends keyof TStorage>(
165
+ key: TKey,
166
+ fn: (value: InferYjsSharedTypeJson<TStorage[TKey]>) => void
167
+ ): (() => void) => {
168
+ return this._crdtNotifier.subscribe(key, fn);
169
+ };
170
+
171
+ public subscribe = <
172
+ TSubject extends keyof StateNotifierSubjects<TIO, TPresence>
173
+ >(
174
+ name: TSubject,
175
+ callback: SubscriptionCallback<TIO, TPresence, TSubject>
176
+ ): (() => void) => {
177
+ return this._stateNotifier.subscribe(name, callback);
178
+ };
179
+
180
+ private _observeCrdt(): void {
181
+ this._subscriptions.observeCrdt?.();
182
+
183
+ if (!this._crdtManager) return;
184
+
185
+ const unsubscribe = this._crdtManager.doc.subscribe(
186
+ (update, _origin) => {
187
+ const origin = _origin ?? null;
188
+
189
+ if (!this._crdtManager) return;
190
+ if (!this._state.webSocket) return;
191
+ if (!this._state.connection.id) return;
192
+ if (this._state.webSocket.readyState !== WebSocket.OPEN) return;
193
+
194
+ if (origin === "$STORAGE_UPDATED") return;
195
+
196
+ const sharedTypes = this._crdtManager.doc.getSharedTypes();
197
+
198
+ Object.keys(sharedTypes).forEach((key) => {
199
+ if (!this._crdtManager) return;
200
+
201
+ const sharedType = sharedTypes[key];
202
+
203
+ if (!sharedType) return;
204
+
205
+ const serialized = sharedType.toJSON();
206
+
207
+ this._crdtNotifier.subject(key).next(serialized);
208
+ });
209
+ }
210
+ );
211
+
212
+ this._subscriptions.observeCrdt = unsubscribe;
213
+ }
214
+ }
package/src/PluvRoom.ts CHANGED
@@ -3,7 +3,6 @@ import type {
3
3
  BaseIOEventRecord,
4
4
  EventMessage,
5
5
  Id,
6
- InferEventMessage,
7
6
  InferIOAuthorize,
8
7
  InferIOAuthorizeRequired,
9
8
  InferIOAuthorizeUser,
@@ -14,6 +13,7 @@ import type {
14
13
  JsonObject,
15
14
  } from "@pluv/types";
16
15
  import type { AbstractType } from "yjs";
16
+ import { AbstractRoom } from "./AbstractRoom";
17
17
  import type { CrdtManagerOptions } from "./CrdtManager";
18
18
  import { CrdtManager } from "./CrdtManager";
19
19
  import { CrdtNotifier } from "./CrdtNotifier";
@@ -28,6 +28,7 @@ import type {
28
28
  import { StateNotifier } from "./StateNotifier";
29
29
  import type {
30
30
  AuthorizationState,
31
+ InternalSubscriptions,
31
32
  UserInfo,
32
33
  WebSocketConnection,
33
34
  WebSocketState,
@@ -89,10 +90,6 @@ interface InternalListeners {
89
90
  onAuthorizationFail: (error: Error) => void;
90
91
  }
91
92
 
92
- interface InternalSubscriptions {
93
- observeCrdt: (() => void) | null;
94
- }
95
-
96
93
  export type PluvRoomDebug<TIO extends IOLike> = Id<{
97
94
  output: readonly (keyof InferIOOutput<TIO>)[];
98
95
  input: readonly (keyof InferIOInput<TIO>)[];
@@ -118,11 +115,9 @@ export class PluvRoom<
118
115
  TIO extends IOLike,
119
116
  TPresence extends JsonObject = {},
120
117
  TStorage extends Record<string, AbstractType<any>> = {}
121
- > {
118
+ > extends AbstractRoom<TIO, TPresence, TStorage> {
122
119
  readonly _endpoints: RoomEndpoints<TIO>;
123
120
 
124
- public readonly id: string;
125
-
126
121
  private _crdtManager: CrdtManager<TStorage> | null = null;
127
122
  private _crdtNotifier = new CrdtNotifier<TStorage>();
128
123
  private _debug: boolean | PluvRoomDebug<TIO>;
@@ -167,7 +162,8 @@ export class PluvRoom<
167
162
  wsEndpoint,
168
163
  } = options;
169
164
 
170
- this.id = room;
165
+ super(room);
166
+
171
167
  this._debug = debug;
172
168
 
173
169
  this._endpoints = {
@@ -201,11 +197,16 @@ export class PluvRoom<
201
197
  return this._state.webSocket;
202
198
  }
203
199
 
204
- public broadcast(message: Id<InferEventMessage<InferIOInput<TIO>>>): void {
200
+ public broadcast<TEvent extends keyof InferIOInput<TIO>>(
201
+ event: TEvent,
202
+ data: Id<InferIOInput<TIO>[TEvent]>
203
+ ): void {
205
204
  if (!this._state.webSocket) return;
206
205
  if (this._state.connection.state !== ConnectionState.Open) return;
207
206
 
208
- this._sendMessage(message);
207
+ const type = event.toString();
208
+
209
+ this._sendMessage({ data, type });
209
210
  }
210
211
 
211
212
  public async connect(): Promise<void> {
@@ -291,10 +292,10 @@ export class PluvRoom<
291
292
  }
292
293
 
293
294
  public event = <TEvent extends keyof InferIOOutput<TIO>>(
294
- name: TEvent,
295
+ event: TEvent,
295
296
  callback: EventNotifierSubscriptionCallback<TIO, TEvent>
296
297
  ): (() => void) => {
297
- return this._eventNotifier.subscribe(name, callback);
298
+ return this._eventNotifier.subscribe(event, callback);
298
299
  };
299
300
 
300
301
  public getConnection(): WebSocketConnection {
package/src/index.ts CHANGED
@@ -6,7 +6,10 @@ export type {
6
6
  IOEventMessage,
7
7
  } from "@pluv/types";
8
8
  export type { AbstractType } from "yjs";
9
+ export { AbstractRoom } from "./AbstractRoom";
9
10
  export { createClient } from "./createClient";
11
+ export type { MockedRoomConfig, MockedRoomEvents } from "./MockedRoom";
12
+ export { MockedRoom } from "./MockedRoom";
10
13
  export type { PluvClientOptions } from "./PluvClient";
11
14
  export { PluvClient } from "./PluvClient";
12
15
  export type { PluvRoomDebug } from "./PluvRoom";
package/src/types.ts CHANGED
@@ -19,6 +19,10 @@ export enum ConnectionState {
19
19
  Untouched = "Untouched",
20
20
  }
21
21
 
22
+ export interface InternalSubscriptions {
23
+ observeCrdt: (() => void) | null;
24
+ }
25
+
22
26
  export interface WebSocketConnection {
23
27
  id: string | null;
24
28
  state: ConnectionState;