@pluv/client 0.11.1 → 0.12.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.
- package/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +254 -0
- package/dist/index.d.mts +31 -8
- package/dist/index.d.ts +31 -8
- package/dist/index.js +150 -36
- package/dist/index.mjs +150 -36
- package/package.json +7 -7
- package/src/AbstractRoom.ts +29 -10
- package/src/AbstractStorageStore.ts +0 -2
- package/src/CrdtManager.ts +33 -10
- package/src/CrdtNotifier.ts +14 -2
- package/src/EventNotifier.ts +3 -3
- package/src/MockedRoom.ts +85 -22
- package/src/OtherNotifier.ts +3 -3
- package/src/PluvClient.ts +6 -6
- package/src/PluvRoom.ts +121 -26
- package/src/StateNotifier.ts +6 -6
- package/src/UsersManager.ts +4 -4
- package/src/createClient.ts +1 -1
- package/src/index.ts +4 -1
- package/src/types.ts +1 -1
package/src/EventNotifier.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { makeSubject, subscribe } from "wonka";
|
|
|
4
4
|
|
|
5
5
|
export type EventNotifierSubscriptionCallback<
|
|
6
6
|
TIO extends IOLike,
|
|
7
|
-
TEvent extends keyof InferIOOutput<TIO
|
|
7
|
+
TEvent extends keyof InferIOOutput<TIO>,
|
|
8
8
|
> = (value: Id<IOEventMessage<TIO, TEvent>>) => void;
|
|
9
9
|
|
|
10
10
|
export class EventNotifier<TIO extends IOLike> {
|
|
@@ -14,7 +14,7 @@ export class EventNotifier<TIO extends IOLike> {
|
|
|
14
14
|
>();
|
|
15
15
|
|
|
16
16
|
public subject<TEvent extends keyof InferIOOutput<TIO>>(
|
|
17
|
-
name: TEvent
|
|
17
|
+
name: TEvent,
|
|
18
18
|
): Subject<Id<IOEventMessage<TIO, TEvent>>> {
|
|
19
19
|
const subject = this.subjects.get(name);
|
|
20
20
|
|
|
@@ -29,7 +29,7 @@ export class EventNotifier<TIO extends IOLike> {
|
|
|
29
29
|
|
|
30
30
|
public subscribe<TEvent extends keyof InferIOOutput<TIO>>(
|
|
31
31
|
name: TEvent,
|
|
32
|
-
callback: EventNotifierSubscriptionCallback<TIO, TEvent
|
|
32
|
+
callback: EventNotifierSubscriptionCallback<TIO, TEvent>,
|
|
33
33
|
): () => void {
|
|
34
34
|
const subject = this.subject(name);
|
|
35
35
|
|
package/src/MockedRoom.ts
CHANGED
|
@@ -31,14 +31,14 @@ import { UsersManager } from "./UsersManager";
|
|
|
31
31
|
|
|
32
32
|
export type MockedRoomEvents<TIO extends IOLike> = Partial<{
|
|
33
33
|
[P in keyof InferIOInput<TIO>]: (
|
|
34
|
-
data: Id<InferIOInput<TIO>[P]
|
|
34
|
+
data: Id<InferIOInput<TIO>[P]>,
|
|
35
35
|
) => Partial<InferIOOutput<TIO>>;
|
|
36
36
|
}>;
|
|
37
37
|
|
|
38
38
|
export type MockedRoomConfig<
|
|
39
39
|
TIO extends IOLike,
|
|
40
40
|
TPresence extends JsonObject = {},
|
|
41
|
-
TStorage extends Record<string, AbstractType<any>> = {}
|
|
41
|
+
TStorage extends Record<string, AbstractType<any>> = {},
|
|
42
42
|
> = { events?: MockedRoomEvents<TIO> } & Omit<
|
|
43
43
|
CrdtManagerOptions<TStorage>,
|
|
44
44
|
"encodedState"
|
|
@@ -48,9 +48,9 @@ export type MockedRoomConfig<
|
|
|
48
48
|
export class MockedRoom<
|
|
49
49
|
TIO extends IOLike,
|
|
50
50
|
TPresence extends JsonObject = {},
|
|
51
|
-
TStorage extends Record<string, AbstractType<any>> = {}
|
|
51
|
+
TStorage extends Record<string, AbstractType<any>> = {},
|
|
52
52
|
> extends AbstractRoom<TIO, TPresence, TStorage> {
|
|
53
|
-
private _crdtManager: CrdtManager<TStorage
|
|
53
|
+
private _crdtManager: CrdtManager<TStorage>;
|
|
54
54
|
private _crdtNotifier = new CrdtNotifier<TStorage>();
|
|
55
55
|
private _eventNotifier = new EventNotifier<TIO>();
|
|
56
56
|
private _events?: MockedRoomEvents<TIO>;
|
|
@@ -75,7 +75,7 @@ export class MockedRoom<
|
|
|
75
75
|
|
|
76
76
|
constructor(
|
|
77
77
|
room: string,
|
|
78
|
-
options: MockedRoomConfig<TIO, TPresence, TStorage
|
|
78
|
+
options: MockedRoomConfig<TIO, TPresence, TStorage>,
|
|
79
79
|
) {
|
|
80
80
|
const { events, initialPresence, initialStorage, presence } = options;
|
|
81
81
|
|
|
@@ -97,7 +97,7 @@ export class MockedRoom<
|
|
|
97
97
|
|
|
98
98
|
public broadcast<TEvent extends keyof InferIOInput<TIO>>(
|
|
99
99
|
event: TEvent,
|
|
100
|
-
data: Id<InferIOInput<TIO>[TEvent]
|
|
100
|
+
data: Id<InferIOInput<TIO>[TEvent]>,
|
|
101
101
|
): void {
|
|
102
102
|
if (!this._events) return;
|
|
103
103
|
|
|
@@ -117,9 +117,17 @@ export class MockedRoom<
|
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
public canRedo = (): boolean => {
|
|
121
|
+
return !!this._crdtManager.doc.canRedo();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
public canUndo = (): boolean => {
|
|
125
|
+
return !!this._crdtManager.doc.canUndo();
|
|
126
|
+
};
|
|
127
|
+
|
|
120
128
|
public event = <TEvent extends keyof InferIOOutput<TIO>>(
|
|
121
129
|
event: TEvent,
|
|
122
|
-
callback: EventNotifierSubscriptionCallback<TIO, TEvent
|
|
130
|
+
callback: EventNotifierSubscriptionCallback<TIO, TEvent>,
|
|
123
131
|
): (() => void) => {
|
|
124
132
|
return this._eventNotifier.subscribe(event, callback);
|
|
125
133
|
};
|
|
@@ -127,7 +135,7 @@ export class MockedRoom<
|
|
|
127
135
|
public getConnection(): WebSocketConnection {
|
|
128
136
|
// Create a read-only clone of the connection state
|
|
129
137
|
return Object.freeze(
|
|
130
|
-
JSON.parse(JSON.stringify(this._state.connection))
|
|
138
|
+
JSON.parse(JSON.stringify(this._state.connection)),
|
|
131
139
|
);
|
|
132
140
|
}
|
|
133
141
|
|
|
@@ -140,7 +148,7 @@ export class MockedRoom<
|
|
|
140
148
|
};
|
|
141
149
|
|
|
142
150
|
public getOther = (
|
|
143
|
-
connectionId: string
|
|
151
|
+
connectionId: string,
|
|
144
152
|
): Id<UserInfo<TIO, TPresence>> | null => {
|
|
145
153
|
return this._usersManager.getOther(connectionId);
|
|
146
154
|
};
|
|
@@ -150,34 +158,78 @@ export class MockedRoom<
|
|
|
150
158
|
};
|
|
151
159
|
|
|
152
160
|
public getStorage = <TKey extends keyof TStorage>(
|
|
153
|
-
key: TKey
|
|
154
|
-
): TStorage[TKey]
|
|
155
|
-
|
|
161
|
+
key: TKey,
|
|
162
|
+
): TStorage[TKey] => {
|
|
163
|
+
const sharedType = this._crdtManager.get(key);
|
|
164
|
+
|
|
165
|
+
if (typeof sharedType === "undefined") {
|
|
166
|
+
throw new Error(`Could not find storege: ${key.toString()}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return sharedType;
|
|
156
170
|
};
|
|
157
171
|
|
|
158
172
|
public other = (
|
|
159
173
|
connectionId: string,
|
|
160
|
-
callback: OtherNotifierSubscriptionCallback<TIO
|
|
174
|
+
callback: OtherNotifierSubscriptionCallback<TIO>,
|
|
161
175
|
): (() => void) => {
|
|
162
176
|
return this._otherNotifier.subscribe(connectionId, callback);
|
|
163
177
|
};
|
|
164
178
|
|
|
179
|
+
public redo = (): void => {
|
|
180
|
+
this._crdtManager.doc.redo();
|
|
181
|
+
};
|
|
182
|
+
|
|
165
183
|
public storage = <TKey extends keyof TStorage>(
|
|
166
184
|
key: TKey,
|
|
167
|
-
fn: (value: InferYjsSharedTypeJson<TStorage[TKey]>) => void
|
|
185
|
+
fn: (value: InferYjsSharedTypeJson<TStorage[TKey]>) => void,
|
|
168
186
|
): (() => void) => {
|
|
169
187
|
return this._crdtNotifier.subscribe(key, fn);
|
|
170
188
|
};
|
|
171
189
|
|
|
190
|
+
public storageRoot = (
|
|
191
|
+
fn: (value: {
|
|
192
|
+
[P in keyof TStorage]: InferYjsSharedTypeJson<TStorage[P]>;
|
|
193
|
+
}) => void,
|
|
194
|
+
): (() => void) => {
|
|
195
|
+
return this._crdtNotifier.subcribeRoot(fn);
|
|
196
|
+
};
|
|
197
|
+
|
|
172
198
|
public subscribe = <
|
|
173
|
-
TSubject extends keyof StateNotifierSubjects<TIO, TPresence
|
|
199
|
+
TSubject extends keyof StateNotifierSubjects<TIO, TPresence>,
|
|
174
200
|
>(
|
|
175
201
|
name: TSubject,
|
|
176
|
-
callback: SubscriptionCallback<TIO, TPresence, TSubject
|
|
202
|
+
callback: SubscriptionCallback<TIO, TPresence, TSubject>,
|
|
177
203
|
): (() => void) => {
|
|
178
204
|
return this._stateNotifier.subscribe(name, callback);
|
|
179
205
|
};
|
|
180
206
|
|
|
207
|
+
public transact = (
|
|
208
|
+
fn: (storage: TStorage) => void,
|
|
209
|
+
origin?: string,
|
|
210
|
+
): void => {
|
|
211
|
+
const _origin = origin ?? this._state.connection.id;
|
|
212
|
+
const crdtManager = this._crdtManager;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* !HACK
|
|
216
|
+
* @description Don't transact anything if there is no origin, because
|
|
217
|
+
* that means the user isn't connected yet. This will mean events are
|
|
218
|
+
* lost unfortunately.
|
|
219
|
+
* @date September 23, 2023
|
|
220
|
+
*/
|
|
221
|
+
if (typeof _origin !== "string") return;
|
|
222
|
+
if (!crdtManager) return;
|
|
223
|
+
|
|
224
|
+
crdtManager.doc.transact(() => {
|
|
225
|
+
fn(crdtManager.doc.storage);
|
|
226
|
+
}, _origin);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
public undo = (): void => {
|
|
230
|
+
this._crdtManager.doc.undo();
|
|
231
|
+
};
|
|
232
|
+
|
|
181
233
|
public updateMyPresence = (presence: Partial<TPresence>): void => {
|
|
182
234
|
this._usersManager.updateMyPresence(presence);
|
|
183
235
|
|
|
@@ -202,14 +254,25 @@ export class MockedRoom<
|
|
|
202
254
|
|
|
203
255
|
const sharedTypes = this._crdtManager.doc.getSharedTypes();
|
|
204
256
|
|
|
205
|
-
Object.entries(sharedTypes).
|
|
206
|
-
|
|
257
|
+
const storageRoot = Object.entries(sharedTypes).reduce(
|
|
258
|
+
(acc, [prop, sharedType]) => {
|
|
259
|
+
if (!this._crdtManager) return acc;
|
|
260
|
+
|
|
261
|
+
const serialized = sharedType.toJSON();
|
|
262
|
+
|
|
263
|
+
this._crdtNotifier.subject(prop).next(serialized);
|
|
207
264
|
|
|
208
|
-
|
|
265
|
+
return { ...acc, [prop]: serialized };
|
|
266
|
+
},
|
|
267
|
+
{} as {
|
|
268
|
+
[P in keyof TStorage]: InferYjsSharedTypeJson<
|
|
269
|
+
TStorage[P]
|
|
270
|
+
>;
|
|
271
|
+
},
|
|
272
|
+
);
|
|
209
273
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
274
|
+
this._crdtNotifier.rootSubject.next(storageRoot);
|
|
275
|
+
},
|
|
213
276
|
);
|
|
214
277
|
|
|
215
278
|
this._subscriptions.observeCrdt = unsubscribe;
|
package/src/OtherNotifier.ts
CHANGED
|
@@ -4,14 +4,14 @@ import { makeSubject, subscribe } from "wonka";
|
|
|
4
4
|
import { UserInfo } from "./types";
|
|
5
5
|
|
|
6
6
|
export type OtherNotifierSubscriptionCallback<TIO extends IOLike> = (
|
|
7
|
-
value: Id<UserInfo<TIO>> | null
|
|
7
|
+
value: Id<UserInfo<TIO>> | null,
|
|
8
8
|
) => void;
|
|
9
9
|
|
|
10
10
|
export class OtherNotifier<TIO extends IOLike> {
|
|
11
11
|
public subjects = new Map<string, Subject<Id<UserInfo<TIO>> | null>>();
|
|
12
12
|
|
|
13
13
|
public subject<TEvent extends string>(
|
|
14
|
-
name: TEvent
|
|
14
|
+
name: TEvent,
|
|
15
15
|
): Subject<Id<UserInfo<TIO>> | null> {
|
|
16
16
|
const subject = this.subjects.get(name);
|
|
17
17
|
|
|
@@ -26,7 +26,7 @@ export class OtherNotifier<TIO extends IOLike> {
|
|
|
26
26
|
|
|
27
27
|
public subscribe<TEvent extends string>(
|
|
28
28
|
name: TEvent,
|
|
29
|
-
callback: OtherNotifierSubscriptionCallback<TIO
|
|
29
|
+
callback: OtherNotifierSubscriptionCallback<TIO>,
|
|
30
30
|
): () => void {
|
|
31
31
|
const source = this.subject(name).source;
|
|
32
32
|
|
package/src/PluvClient.ts
CHANGED
|
@@ -30,10 +30,10 @@ export class PluvClient<TIO extends IOLike = IOLike> {
|
|
|
30
30
|
|
|
31
31
|
public createRoom = <
|
|
32
32
|
TPresence extends JsonObject = {},
|
|
33
|
-
TStorage extends Record<string, AbstractType<any>> = {}
|
|
33
|
+
TStorage extends Record<string, AbstractType<any>> = {},
|
|
34
34
|
>(
|
|
35
35
|
room: string,
|
|
36
|
-
options: PluvRoomOptions<TIO, TPresence, TStorage> = {}
|
|
36
|
+
options: PluvRoomOptions<TIO, TPresence, TStorage> = {},
|
|
37
37
|
): PluvRoom<TIO, TPresence, TStorage> => {
|
|
38
38
|
const oldRoom = this.getRoom<TPresence, TStorage>(room);
|
|
39
39
|
|
|
@@ -57,7 +57,7 @@ export class PluvClient<TIO extends IOLike = IOLike> {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
public enter = async (
|
|
60
|
-
room: string | PluvRoom<TIO, any, any
|
|
60
|
+
room: string | PluvRoom<TIO, any, any>,
|
|
61
61
|
): Promise<PluvRoom<TIO, JsonObject, any>> => {
|
|
62
62
|
const toEnter = typeof room === "string" ? this.getRoom(room) : room;
|
|
63
63
|
|
|
@@ -76,9 +76,9 @@ export class PluvClient<TIO extends IOLike = IOLike> {
|
|
|
76
76
|
|
|
77
77
|
public getRoom = <
|
|
78
78
|
TPresence extends JsonObject = {},
|
|
79
|
-
TStorage extends Record<string, AbstractType<any>> = {}
|
|
79
|
+
TStorage extends Record<string, AbstractType<any>> = {},
|
|
80
80
|
>(
|
|
81
|
-
room: string
|
|
81
|
+
room: string,
|
|
82
82
|
): PluvRoom<TIO, TPresence, TStorage> | null => {
|
|
83
83
|
const found = this._rooms.get(room) as
|
|
84
84
|
| PluvRoom<TIO, TPresence, TStorage>
|
|
@@ -92,7 +92,7 @@ export class PluvClient<TIO extends IOLike = IOLike> {
|
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
public leave = async (
|
|
95
|
-
room: string | PluvRoom<TIO, any, any
|
|
95
|
+
room: string | PluvRoom<TIO, any, any>,
|
|
96
96
|
): Promise<void> => {
|
|
97
97
|
const toLeave = typeof room === "string" ? this.getRoom(room) : room;
|
|
98
98
|
|
package/src/PluvRoom.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InferYjsSharedTypeJson } from "@pluv/crdt-yjs";
|
|
1
|
+
import { InferYjsSharedTypeJson, TrackOriginOptions } from "@pluv/crdt-yjs";
|
|
2
2
|
import type {
|
|
3
3
|
BaseIOEventRecord,
|
|
4
4
|
EventMessage,
|
|
@@ -44,7 +44,8 @@ const ADD_TO_STORAGE_STATE_DEBOUNCE_MS = 1_000;
|
|
|
44
44
|
const HEARTBEAT_INTERVAL_MS = 10_000;
|
|
45
45
|
const PONG_TIMEOUT_MS = 2_000;
|
|
46
46
|
const RECONNECT_TIMEOUT_MS = 30_000;
|
|
47
|
-
const
|
|
47
|
+
const ORIGIN_INITIALIZED = "$INITIALIZED";
|
|
48
|
+
const ORIGIN_STORAGE_UPDATED = "$STORAGE_UPDATED";
|
|
48
49
|
|
|
49
50
|
export const DEFAULT_PLUV_CLIENT_ADDON = <
|
|
50
51
|
TIO extends IOLike = IOLike,
|
|
@@ -143,7 +144,9 @@ export type RoomConfig<
|
|
|
143
144
|
TIO extends IOLike,
|
|
144
145
|
TPresence extends JsonObject = {},
|
|
145
146
|
TStorage extends Record<string, AbstractType<any>> = {},
|
|
146
|
-
> = RoomEndpoints<TIO> &
|
|
147
|
+
> = RoomEndpoints<TIO> &
|
|
148
|
+
PluvRoomOptions<TIO, TPresence, TStorage> &
|
|
149
|
+
TrackOriginOptions;
|
|
147
150
|
|
|
148
151
|
export class PluvRoom<
|
|
149
152
|
TIO extends IOLike,
|
|
@@ -152,6 +155,7 @@ export class PluvRoom<
|
|
|
152
155
|
> extends AbstractRoom<TIO, TPresence, TStorage> {
|
|
153
156
|
readonly _endpoints: RoomEndpoints<TIO>;
|
|
154
157
|
|
|
158
|
+
private _captureTimeout: number | null = null;
|
|
155
159
|
private _crdtManager: CrdtManager<TStorage>;
|
|
156
160
|
private _crdtNotifier = new CrdtNotifier<TStorage>();
|
|
157
161
|
private _debug: boolean | PluvRoomDebug<TIO>;
|
|
@@ -182,6 +186,7 @@ export class PluvRoom<
|
|
|
182
186
|
pong: null,
|
|
183
187
|
reconnect: null,
|
|
184
188
|
};
|
|
189
|
+
private _trackedOrigins: readonly string[] | null = null;
|
|
185
190
|
private _windowListeners: WindowListeners | null = null;
|
|
186
191
|
private _wsListeners: WebSocketListeners | null = null;
|
|
187
192
|
private _usersManager: UsersManager<TIO, TPresence>;
|
|
@@ -190,11 +195,13 @@ export class PluvRoom<
|
|
|
190
195
|
const {
|
|
191
196
|
addons = [],
|
|
192
197
|
authEndpoint,
|
|
198
|
+
captureTimeout,
|
|
193
199
|
debug = false,
|
|
194
200
|
initialPresence,
|
|
195
201
|
initialStorage,
|
|
196
202
|
onAuthorizationFail,
|
|
197
203
|
presence,
|
|
204
|
+
trackedOrigins,
|
|
198
205
|
wsEndpoint,
|
|
199
206
|
} = options;
|
|
200
207
|
|
|
@@ -230,7 +237,14 @@ export class PluvRoom<
|
|
|
230
237
|
presence,
|
|
231
238
|
});
|
|
232
239
|
|
|
233
|
-
this._crdtManager = new CrdtManager<TStorage>({
|
|
240
|
+
this._crdtManager = new CrdtManager<TStorage>({
|
|
241
|
+
captureTimeout,
|
|
242
|
+
initialStorage,
|
|
243
|
+
trackedOrigins,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
this._captureTimeout = captureTimeout ?? null;
|
|
247
|
+
this._trackedOrigins = trackedOrigins ?? null;
|
|
234
248
|
}
|
|
235
249
|
|
|
236
250
|
public get webSocket(): WebSocket | null {
|
|
@@ -249,6 +263,14 @@ export class PluvRoom<
|
|
|
249
263
|
this._sendMessage({ data, type });
|
|
250
264
|
}
|
|
251
265
|
|
|
266
|
+
public canRedo = (): boolean => {
|
|
267
|
+
return !!this._crdtManager.doc.canRedo();
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
public canUndo = (): boolean => {
|
|
271
|
+
return !!this._crdtManager.doc.canUndo();
|
|
272
|
+
};
|
|
273
|
+
|
|
252
274
|
public async connect(): Promise<void> {
|
|
253
275
|
if (!this._endpoints.wsEndpoint) {
|
|
254
276
|
throw new Error("Must provide an wsEndpoint.");
|
|
@@ -272,8 +294,6 @@ export class PluvRoom<
|
|
|
272
294
|
await this._storageStore.initialize();
|
|
273
295
|
await this._applyStorageStore();
|
|
274
296
|
|
|
275
|
-
console.log(this._getWsEndpoint(this.id));
|
|
276
|
-
|
|
277
297
|
const url = new URL(this._getWsEndpoint(this.id));
|
|
278
298
|
|
|
279
299
|
let authToken: string | null = null;
|
|
@@ -325,7 +345,7 @@ export class PluvRoom<
|
|
|
325
345
|
|
|
326
346
|
await this._storageStore.destroy();
|
|
327
347
|
|
|
328
|
-
this._crdtManager
|
|
348
|
+
this._crdtManager.destroy();
|
|
329
349
|
|
|
330
350
|
this._stateNotifier.subjects["my-presence"].next(null);
|
|
331
351
|
}
|
|
@@ -364,8 +384,14 @@ export class PluvRoom<
|
|
|
364
384
|
|
|
365
385
|
public getStorage = <TKey extends keyof TStorage>(
|
|
366
386
|
key: TKey,
|
|
367
|
-
): TStorage[TKey]
|
|
368
|
-
|
|
387
|
+
): TStorage[TKey] => {
|
|
388
|
+
const sharedType = this._crdtManager.get(key);
|
|
389
|
+
|
|
390
|
+
if (typeof sharedType === "undefined") {
|
|
391
|
+
throw new Error(`Could not find storege: ${key.toString()}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return sharedType;
|
|
369
395
|
};
|
|
370
396
|
|
|
371
397
|
public other = (
|
|
@@ -375,6 +401,10 @@ export class PluvRoom<
|
|
|
375
401
|
return this._otherNotifier.subscribe(connectionId, callback);
|
|
376
402
|
};
|
|
377
403
|
|
|
404
|
+
public redo = (): void => {
|
|
405
|
+
this._crdtManager.doc.redo();
|
|
406
|
+
};
|
|
407
|
+
|
|
378
408
|
public storage = <TKey extends keyof TStorage>(
|
|
379
409
|
key: TKey,
|
|
380
410
|
fn: (value: InferYjsSharedTypeJson<TStorage[TKey]>) => void,
|
|
@@ -382,6 +412,14 @@ export class PluvRoom<
|
|
|
382
412
|
return this._crdtNotifier.subscribe(key, fn);
|
|
383
413
|
};
|
|
384
414
|
|
|
415
|
+
public storageRoot = (
|
|
416
|
+
fn: (value: {
|
|
417
|
+
[P in keyof TStorage]: InferYjsSharedTypeJson<TStorage[P]>;
|
|
418
|
+
}) => void,
|
|
419
|
+
): (() => void) => {
|
|
420
|
+
return this._crdtNotifier.subcribeRoot(fn);
|
|
421
|
+
};
|
|
422
|
+
|
|
385
423
|
public subscribe = <
|
|
386
424
|
TSubject extends keyof StateNotifierSubjects<TIO, TPresence>,
|
|
387
425
|
>(
|
|
@@ -391,6 +429,30 @@ export class PluvRoom<
|
|
|
391
429
|
return this._stateNotifier.subscribe(name, callback);
|
|
392
430
|
};
|
|
393
431
|
|
|
432
|
+
public transact = (
|
|
433
|
+
fn: (storage: TStorage) => void,
|
|
434
|
+
origin?: string,
|
|
435
|
+
): void => {
|
|
436
|
+
const _origin = origin ?? this._state.connection.id;
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* !HACK
|
|
440
|
+
* @description Don't transact anything if there is no origin, because
|
|
441
|
+
* that means the user isn't connected yet. This will mean events are
|
|
442
|
+
* lost unfortunately.
|
|
443
|
+
* @date September 23, 2023
|
|
444
|
+
*/
|
|
445
|
+
if (typeof _origin !== "string") return;
|
|
446
|
+
|
|
447
|
+
this._crdtManager.doc.transact(() => {
|
|
448
|
+
fn(this._crdtManager.doc.storage);
|
|
449
|
+
}, _origin);
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
public undo = (): void => {
|
|
453
|
+
this._crdtManager.doc.undo();
|
|
454
|
+
};
|
|
455
|
+
|
|
394
456
|
public updateMyPresence = (presence: Partial<TPresence>): void => {
|
|
395
457
|
this._usersManager.updateMyPresence(presence);
|
|
396
458
|
|
|
@@ -414,12 +476,15 @@ export class PluvRoom<
|
|
|
414
476
|
|
|
415
477
|
private async _applyStorageStore(): Promise<void> {
|
|
416
478
|
const updates = await this._storageStore.getUpdates();
|
|
479
|
+
const trackedOrigins = this._getTrackedOrigins();
|
|
417
480
|
|
|
418
481
|
this._crdtManager.initialize({
|
|
482
|
+
captureTimeout: this._captureTimeout ?? undefined,
|
|
419
483
|
onInitialized: () => {
|
|
420
484
|
this._observeCrdt();
|
|
421
485
|
},
|
|
422
|
-
origin:
|
|
486
|
+
origin: ORIGIN_INITIALIZED,
|
|
487
|
+
trackedOrigins: trackedOrigins ?? undefined,
|
|
423
488
|
update: updates,
|
|
424
489
|
});
|
|
425
490
|
|
|
@@ -548,11 +613,20 @@ export class PluvRoom<
|
|
|
548
613
|
private _emitSharedTypes(): void {
|
|
549
614
|
const sharedTypes = this._crdtManager.doc.getSharedTypes();
|
|
550
615
|
|
|
551
|
-
Object.entries(sharedTypes).
|
|
552
|
-
|
|
616
|
+
const storageRoot = Object.entries(sharedTypes).reduce(
|
|
617
|
+
(acc, [prop, sharedType]) => {
|
|
618
|
+
const serialized = sharedType.toJSON();
|
|
553
619
|
|
|
554
|
-
|
|
555
|
-
|
|
620
|
+
this._crdtNotifier.subject(prop).next(serialized);
|
|
621
|
+
|
|
622
|
+
return { ...acc, [prop]: serialized };
|
|
623
|
+
},
|
|
624
|
+
{} as {
|
|
625
|
+
[P in keyof TStorage]: InferYjsSharedTypeJson<TStorage[P]>;
|
|
626
|
+
},
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
this._crdtNotifier.rootSubject.next(storageRoot);
|
|
556
630
|
}
|
|
557
631
|
|
|
558
632
|
private _flattenStorageStore = debounce(
|
|
@@ -630,6 +704,14 @@ export class PluvRoom<
|
|
|
630
704
|
}
|
|
631
705
|
}
|
|
632
706
|
|
|
707
|
+
private _getTrackedOrigins(): readonly string[] | null {
|
|
708
|
+
const connectionId = this._state.connection.id;
|
|
709
|
+
|
|
710
|
+
return !connectionId
|
|
711
|
+
? this._trackedOrigins
|
|
712
|
+
: [connectionId, ...(this._trackedOrigins ?? [])];
|
|
713
|
+
}
|
|
714
|
+
|
|
633
715
|
private _getWsEndpoint(room: string): string {
|
|
634
716
|
switch (typeof this._endpoints.wsEndpoint) {
|
|
635
717
|
case "undefined":
|
|
@@ -750,7 +832,7 @@ export class PluvRoom<
|
|
|
750
832
|
|
|
751
833
|
const update =
|
|
752
834
|
this._state.connection.count > 1
|
|
753
|
-
? this._crdtManager
|
|
835
|
+
? this._crdtManager.doc.encodeStateAsUpdate() ?? null
|
|
754
836
|
: null;
|
|
755
837
|
|
|
756
838
|
this._sendMessage({
|
|
@@ -772,11 +854,15 @@ export class PluvRoom<
|
|
|
772
854
|
InferIOAuthorize<TIO>
|
|
773
855
|
>["$STORAGE_RECEIVED"];
|
|
774
856
|
|
|
857
|
+
const trackedOrigins = this._getTrackedOrigins();
|
|
858
|
+
|
|
775
859
|
this._crdtManager.initialize({
|
|
860
|
+
captureTimeout: this._captureTimeout ?? undefined,
|
|
776
861
|
onInitialized: async () => {
|
|
777
862
|
this._observeCrdt();
|
|
778
863
|
},
|
|
779
|
-
origin:
|
|
864
|
+
origin: ORIGIN_INITIALIZED,
|
|
865
|
+
trackedOrigins: trackedOrigins ?? undefined,
|
|
780
866
|
update: data.state,
|
|
781
867
|
});
|
|
782
868
|
|
|
@@ -790,7 +876,7 @@ export class PluvRoom<
|
|
|
790
876
|
|
|
791
877
|
this._sendMessage({
|
|
792
878
|
type: "$UPDATE_STORAGE",
|
|
793
|
-
data: { origin:
|
|
879
|
+
data: { origin: ORIGIN_INITIALIZED, update },
|
|
794
880
|
});
|
|
795
881
|
}
|
|
796
882
|
|
|
@@ -803,21 +889,30 @@ export class PluvRoom<
|
|
|
803
889
|
|
|
804
890
|
const data = message.data as BaseIOEventRecord<
|
|
805
891
|
InferIOAuthorize<TIO>
|
|
806
|
-
>[
|
|
892
|
+
>[typeof ORIGIN_STORAGE_UPDATED];
|
|
807
893
|
|
|
808
894
|
if (!this._crdtManager) return;
|
|
809
895
|
|
|
810
|
-
this._crdtManager.doc.applyUpdate(data.state,
|
|
896
|
+
this._crdtManager.doc.applyUpdate(data.state, ORIGIN_STORAGE_UPDATED);
|
|
811
897
|
|
|
812
898
|
const sharedTypes = this._crdtManager.doc.getSharedTypes();
|
|
813
899
|
|
|
814
|
-
Object.entries(sharedTypes).
|
|
815
|
-
|
|
900
|
+
const storageRoot = Object.entries(sharedTypes).reduce(
|
|
901
|
+
(acc, [prop, sharedType]) => {
|
|
902
|
+
if (!this._crdtManager) return acc;
|
|
816
903
|
|
|
817
|
-
|
|
904
|
+
const serialized = sharedType.toJSON();
|
|
818
905
|
|
|
819
|
-
|
|
820
|
-
|
|
906
|
+
this._crdtNotifier.subject(prop).next(serialized);
|
|
907
|
+
|
|
908
|
+
return { ...acc, [prop]: serialized };
|
|
909
|
+
},
|
|
910
|
+
{} as {
|
|
911
|
+
[P in keyof TStorage]: InferYjsSharedTypeJson<TStorage[P]>;
|
|
912
|
+
},
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
this._crdtNotifier.rootSubject.next(storageRoot);
|
|
821
916
|
}
|
|
822
917
|
|
|
823
918
|
private _handleUserJoinedMessage(message: IOEventMessage<TIO>): void {
|
|
@@ -880,13 +975,13 @@ export class PluvRoom<
|
|
|
880
975
|
this._addToStorageStore(update);
|
|
881
976
|
this._emitSharedTypes();
|
|
882
977
|
|
|
883
|
-
if (origin ===
|
|
978
|
+
if (origin === ORIGIN_INITIALIZED || origin === this) return;
|
|
884
979
|
|
|
885
980
|
if (!this._state.webSocket) return;
|
|
886
981
|
if (!this._state.connection.id) return;
|
|
887
982
|
if (this._state.webSocket.readyState !== WebSocket.OPEN) return;
|
|
888
983
|
|
|
889
|
-
if (origin ===
|
|
984
|
+
if (origin === ORIGIN_STORAGE_UPDATED) return;
|
|
890
985
|
|
|
891
986
|
this._sendMessage({
|
|
892
987
|
type: "$UPDATE_STORAGE",
|
package/src/StateNotifier.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { UserInfo, WebSocketState } from "./types";
|
|
|
5
5
|
|
|
6
6
|
export interface StateNotifierSubjects<
|
|
7
7
|
TIO extends IOLike,
|
|
8
|
-
TPresence extends JsonObject
|
|
8
|
+
TPresence extends JsonObject,
|
|
9
9
|
> {
|
|
10
10
|
connection: Subject<Id<WebSocketState<TIO>>>;
|
|
11
11
|
"my-presence": Subject<TPresence | null>;
|
|
@@ -16,7 +16,7 @@ export interface StateNotifierSubjects<
|
|
|
16
16
|
type InferSubjectValue<
|
|
17
17
|
TIO extends IOLike,
|
|
18
18
|
TPresence extends JsonObject,
|
|
19
|
-
TSubject extends keyof StateNotifierSubjects<TIO, TPresence
|
|
19
|
+
TSubject extends keyof StateNotifierSubjects<TIO, TPresence>,
|
|
20
20
|
> = StateNotifierSubjects<TIO, TPresence>[TSubject] extends Subject<
|
|
21
21
|
infer IValue
|
|
22
22
|
>
|
|
@@ -26,12 +26,12 @@ type InferSubjectValue<
|
|
|
26
26
|
export type SubscriptionCallback<
|
|
27
27
|
TIO extends IOLike,
|
|
28
28
|
TPresence extends JsonObject,
|
|
29
|
-
TSubject extends keyof StateNotifierSubjects<TIO, TPresence
|
|
29
|
+
TSubject extends keyof StateNotifierSubjects<TIO, TPresence>,
|
|
30
30
|
> = (value: InferSubjectValue<TIO, TPresence, TSubject>) => void;
|
|
31
31
|
|
|
32
32
|
export class StateNotifier<
|
|
33
33
|
TIO extends IOLike,
|
|
34
|
-
TPresence extends JsonObject = {}
|
|
34
|
+
TPresence extends JsonObject = {},
|
|
35
35
|
> {
|
|
36
36
|
public subjects: StateNotifierSubjects<TIO, TPresence> = {
|
|
37
37
|
connection: makeSubject<Id<WebSocketState<TIO>>>(),
|
|
@@ -41,10 +41,10 @@ export class StateNotifier<
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
public subscribe<
|
|
44
|
-
TSubject extends keyof StateNotifierSubjects<TIO, TPresence
|
|
44
|
+
TSubject extends keyof StateNotifierSubjects<TIO, TPresence>,
|
|
45
45
|
>(
|
|
46
46
|
name: TSubject,
|
|
47
|
-
callback: SubscriptionCallback<TIO, TPresence, TSubject
|
|
47
|
+
callback: SubscriptionCallback<TIO, TPresence, TSubject>,
|
|
48
48
|
): () => void {
|
|
49
49
|
const source = this.subjects[name]?.source as any;
|
|
50
50
|
|