@pluv/client 0.2.0 → 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.2.0 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.38 KB
12
- ESM ⚡️ Build success in 86ms
13
- CJS dist/index.js 30.46 KB
14
- CJS ⚡️ Build success in 98ms
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 2621ms
17
- DTS dist/index.d.ts 8.26 KB
16
+ DTS ⚡️ Build success in 3519ms
17
+ DTS dist/index.d.ts 11.52 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## 0.2.0
4
10
 
5
11
  ### Minor Changes
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, 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;
@@ -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();
@@ -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,
@@ -998,6 +1007,123 @@ var createClient = (options) => {
998
1007
  return new PluvClient(options);
999
1008
  };
1000
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
+
1001
1127
  // src/y.ts
1002
1128
  var y_exports = {};
1003
1129
  __export(y_exports, {
@@ -1009,7 +1135,9 @@ __export(y_exports, {
1009
1135
  var import_crdt_yjs3 = require("@pluv/crdt-yjs");
1010
1136
  // Annotate the CommonJS export names for ESM import in node:
1011
1137
  0 && (module.exports = {
1138
+ AbstractRoom,
1012
1139
  ConnectionState,
1140
+ MockedRoom,
1013
1141
  PluvClient,
1014
1142
  PluvRoom,
1015
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();
@@ -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,
@@ -975,6 +982,123 @@ var createClient = (options) => {
975
982
  return new PluvClient(options);
976
983
  };
977
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
+
978
1102
  // src/y.ts
979
1103
  var y_exports = {};
980
1104
  __export(y_exports, {
@@ -985,7 +1109,9 @@ __export(y_exports, {
985
1109
  });
986
1110
  import { array, map, text, unstable__object } from "@pluv/crdt-yjs";
987
1111
  export {
1112
+ AbstractRoom,
988
1113
  ConnectionState,
1114
+ MockedRoom,
989
1115
  PluvClient,
990
1116
  PluvRoom,
991
1117
  createClient,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pluv/client",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "@pluv/io frontend client",
5
5
  "author": "leedavidcs",
6
6
  "license": "MIT",
@@ -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 = {
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;