@pluv/react 0.28.0 → 0.29.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,559 +0,0 @@
1
- import type {
2
- AbstractRoom,
3
- CreateRoomOptions,
4
- MergeEvents,
5
- MockedRoomEvents,
6
- PluvClient,
7
- PluvRoom,
8
- PluvRoomAddon,
9
- PluvRoomDebug,
10
- PluvRouter,
11
- PluvRouterEventConfig,
12
- UserInfo,
13
- WebSocketConnection,
14
- } from "@pluv/client";
15
- import { MockedRoom } from "@pluv/client";
16
- import type { AbstractCrdtDoc, CrdtType, InferCrdtJson } from "@pluv/crdt";
17
- import type { IOEventMessage, IOLike, Id, InferIOInput, InferIOOutput, JsonObject } from "@pluv/types";
18
- import fastDeepEqual from "fast-deep-equal";
19
- import type { Dispatch, FC, ReactNode } from "react";
20
- import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
21
- import { identity, shallowArrayEqual, useRerender, useSyncExternalStoreWithSelector } from "./internal";
22
-
23
- export type CreateRoomBundleOptions<
24
- TIO extends IOLike,
25
- TMetadata extends JsonObject = {},
26
- TPresence extends JsonObject = {},
27
- TStorage extends Record<string, CrdtType<any, any>> = {},
28
- TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {},
29
- > = {
30
- addons?: readonly PluvRoomAddon<TIO, TMetadata, TPresence, TStorage>[];
31
- router?: PluvRouter<TIO, TPresence, TStorage, TEvents>;
32
- };
33
-
34
- type BaseRoomProviderProps<TPresence extends JsonObject, TStorage extends Record<string, CrdtType<any, any>>> = {
35
- children?: ReactNode;
36
- initialStorage?: keyof TStorage extends never ? never : () => TStorage;
37
- room: string;
38
- } & (keyof TPresence extends never ? { initialPresence?: never } : { initialPresence: TPresence });
39
-
40
- export type MockedRoomProviderProps<
41
- TIO extends IOLike,
42
- TPresence extends JsonObject,
43
- TStorage extends Record<string, CrdtType<any, any>>,
44
- TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {},
45
- > = BaseRoomProviderProps<TPresence, TStorage> & {
46
- events?: MockedRoomEvents<MergeEvents<TEvents, TIO>>;
47
- };
48
-
49
- export type PluvRoomProviderProps<
50
- TIO extends IOLike,
51
- TMetadata extends JsonObject,
52
- TPresence extends JsonObject,
53
- TStorage extends Record<string, CrdtType<any, any>>,
54
- > = BaseRoomProviderProps<TPresence, TStorage> & {
55
- debug?: boolean | PluvRoomDebug<TIO>;
56
- onAuthorizationFail?: (error: Error) => void;
57
- } & (keyof TMetadata extends never ? { metadata?: undefined } : { metadata: TMetadata });
58
-
59
- export interface SubscriptionHookOptions<T extends unknown> {
60
- isEqual?: (a: T, b: T) => boolean;
61
- }
62
-
63
- export type UpdateMyPresenceAction<TPresence extends JsonObject> =
64
- | Partial<TPresence>
65
- | ((oldPresence: TPresence | null) => Partial<TPresence>);
66
-
67
- export type BroadcastProxy<TIO extends IOLike> = (<TEvent extends keyof InferIOInput<TIO>>(
68
- event: TEvent,
69
- data: Id<InferIOInput<TIO>[TEvent]>,
70
- ) => void) & {
71
- [event in keyof InferIOInput<TIO>]: (input: Id<InferIOInput<TIO>[event]>) => void;
72
- };
73
-
74
- export type EventProxy<TIO extends IOLike> = {
75
- [event in keyof InferIOOutput<TIO>]: {
76
- useEvent: (callback: (data: Id<IOEventMessage<TIO, event>>) => void) => void;
77
- };
78
- };
79
-
80
- export interface CreateRoomBundle<
81
- TIO extends IOLike,
82
- TMetadata extends JsonObject,
83
- TPresence extends JsonObject,
84
- TStorage extends Record<string, CrdtType<any, any>>,
85
- TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {},
86
- > {
87
- // components
88
- MockedRoomProvider: FC<MockedRoomProviderProps<TIO, TPresence, TStorage>>;
89
- PluvRoomProvider: FC<PluvRoomProviderProps<TIO, TMetadata, TPresence, TStorage>>;
90
-
91
- // proxies
92
- event: EventProxy<MergeEvents<TEvents, TIO>>;
93
-
94
- // hooks
95
- useBroadcast: () => BroadcastProxy<MergeEvents<TEvents, TIO>>;
96
- useCanRedo: () => boolean;
97
- useCanUndo: () => boolean;
98
- useConnection: <T extends unknown = WebSocketConnection>(
99
- selector: (connection: WebSocketConnection) => T,
100
- options?: SubscriptionHookOptions<Id<T>>,
101
- ) => Id<T>;
102
- useDoc: () => AbstractCrdtDoc<TStorage>;
103
- useEvent: <TType extends keyof InferIOOutput<MergeEvents<TEvents, TIO>>>(
104
- type: TType,
105
- callback: (data: Id<IOEventMessage<MergeEvents<TEvents, TIO>, TType>>) => void,
106
- ) => void;
107
- useMyPresence: <T extends unknown = TPresence>(
108
- selector?: (myPresence: TPresence) => T,
109
- options?: SubscriptionHookOptions<Id<T> | null>,
110
- ) => [myPresence: Id<T>, updateMyPresence: Dispatch<UpdateMyPresenceAction<TPresence>>];
111
- useMyself: <T extends unknown = UserInfo<TIO, TPresence>>(
112
- selector?: (myself: Id<UserInfo<TIO, TPresence>>) => T,
113
- options?: SubscriptionHookOptions<Id<T> | null>,
114
- ) => Id<T> | null;
115
- useOther: <T extends unknown = UserInfo<TIO, TPresence>>(
116
- connectionId: string,
117
- selector?: (other: UserInfo<TIO, TPresence>) => T,
118
- options?: SubscriptionHookOptions<T | null>,
119
- ) => T | null;
120
- useOthers: <T extends unknown = readonly UserInfo<TIO, TPresence>[]>(
121
- selector?: (other: readonly Id<UserInfo<TIO, TPresence>>[]) => T,
122
- options?: SubscriptionHookOptions<T>,
123
- ) => T;
124
- useRedo: () => () => void;
125
- useRoom: () => AbstractRoom<TIO, TPresence, TStorage>;
126
- useStorage: <TKey extends keyof TStorage, TData extends unknown = InferCrdtJson<TStorage[TKey]>>(
127
- key: TKey,
128
- selector?: (data: InferCrdtJson<TStorage[TKey]>) => TData,
129
- options?: SubscriptionHookOptions<TData | null>,
130
- ) => [data: TData | null, sharedType: TStorage[TKey] | null];
131
- useTransact: () => (fn: (storage: TStorage) => void, origin?: string) => void;
132
- useUndo: () => () => void;
133
- }
134
-
135
- export type InferRoomStorage<TRoomBundle extends CreateRoomBundle<any, any, any, any, any>> =
136
- TRoomBundle extends CreateRoomBundle<any, any, any, infer IStorage> ? IStorage : never;
137
-
138
- export const createRoomBundle = <
139
- TIO extends IOLike,
140
- TMetadata extends JsonObject = {},
141
- TPresence extends JsonObject = {},
142
- TStorage extends Record<string, CrdtType<any, any>> = {},
143
- TEvents extends PluvRouterEventConfig<TIO, TPresence, TStorage> = {},
144
- >(
145
- client: PluvClient<TIO, TPresence, TStorage, TMetadata>,
146
- options: CreateRoomBundleOptions<TIO, TMetadata, TPresence, TStorage, TEvents>,
147
- ): CreateRoomBundle<TIO, TMetadata, TPresence, TStorage, TEvents> => {
148
- /**
149
- * !HACK
150
- * @description We'll let the context error out if the room is not provided,
151
- * and let the users deal with it.
152
- * @author leedavidcs
153
- * @date November 11, 2022
154
- */
155
- const PluvRoomContext = createContext<AbstractRoom<TIO, TPresence, TStorage>>(null as any);
156
-
157
- const MockedRoomContext = createContext<{
158
- room: AbstractRoom<TIO, TPresence, TStorage> | null;
159
- }>({ room: null });
160
-
161
- const MockedRoomProvider = memo<MockedRoomProviderProps<TIO, TPresence, TStorage>>((props) => {
162
- const { children, events, initialPresence, initialStorage, room: _room } = props;
163
-
164
- const [room] = useState<MockedRoom<TIO, TPresence, TStorage, TEvents>>(() => {
165
- return new MockedRoom<TIO, TPresence, TStorage, TEvents>(_room, {
166
- events,
167
- initialPresence,
168
- initialStorage:
169
- typeof initialStorage === "function"
170
- ? client._defs.initialStorage?.getFactory(initialStorage)
171
- : client._defs.initialStorage,
172
- });
173
- });
174
-
175
- return (
176
- <MockedRoomContext.Provider value={{ room }}>
177
- <PluvRoomContext.Provider value={room}>{children}</PluvRoomContext.Provider>
178
- </MockedRoomContext.Provider>
179
- );
180
- });
181
-
182
- MockedRoomProvider.displayName = "MockedRoomProvider";
183
-
184
- const PluvRoomProvider = memo<PluvRoomProviderProps<TIO, TMetadata, TPresence, TStorage>>((props) => {
185
- const { children, debug, initialPresence, initialStorage, metadata, onAuthorizationFail, room: _room } = props;
186
-
187
- const rerender = useRerender();
188
- const { room: mockedRoom } = useContext(MockedRoomContext);
189
-
190
- const [room] = useState<PluvRoom<TIO, TMetadata, TPresence, TStorage, TEvents>>(() => {
191
- return client.createRoom(_room, {
192
- addons: options.addons,
193
- debug,
194
- initialPresence,
195
- initialStorage:
196
- typeof initialStorage === "function"
197
- ? client._defs.initialStorage?.getFactory(initialStorage)
198
- : client._defs.initialStorage,
199
- metadata,
200
- onAuthorizationFail,
201
- router: options.router,
202
- } as CreateRoomOptions<TIO, TPresence, TStorage, TMetadata, TEvents>);
203
- });
204
-
205
- useEffect(() => {
206
- const unsubscribe = room.subscribe("connection", () => {
207
- rerender();
208
- });
209
-
210
- return () => {
211
- unsubscribe();
212
- };
213
- }, [rerender, room]);
214
-
215
- useEffect(() => {
216
- client.enter(room);
217
-
218
- return () => {
219
- client.leave(room);
220
- };
221
- }, [room]);
222
-
223
- return <PluvRoomContext.Provider value={mockedRoom ?? room}>{children}</PluvRoomContext.Provider>;
224
- });
225
-
226
- PluvRoomProvider.displayName = "PluvRoomProvider";
227
-
228
- const useRoom = () => useContext(PluvRoomContext);
229
-
230
- const useBroadcast = (): BroadcastProxy<MergeEvents<TEvents, TIO>> => {
231
- const room = useRoom();
232
-
233
- const broadcast = useCallback(
234
- <TEvent extends keyof InferIOInput<MergeEvents<TEvents, TIO>>>(
235
- event: TEvent,
236
- data: Id<InferIOInput<MergeEvents<TEvents, TIO>>[TEvent]>,
237
- ) => {
238
- room.broadcast(event, data);
239
- },
240
- [room],
241
- );
242
-
243
- return useMemo((): BroadcastProxy<MergeEvents<TEvents, TIO>> => {
244
- return new Proxy(broadcast, {
245
- get(fn, prop) {
246
- return (
247
- data: Id<
248
- InferIOInput<MergeEvents<TEvents, TIO>>[keyof InferIOInput<MergeEvents<TEvents, TIO>>]
249
- >,
250
- ): void => {
251
- return fn(prop as keyof InferIOInput<MergeEvents<TEvents, TIO>>, data);
252
- };
253
- },
254
- }) as BroadcastProxy<MergeEvents<TEvents, TIO>>;
255
- }, [broadcast]);
256
- };
257
-
258
- const useCanRedo = (): boolean => {
259
- const room = useRoom();
260
-
261
- const subscribe = useCallback((onStoreChange: () => void) => room.storageRoot(onStoreChange), [room]);
262
-
263
- const getSnapshot = useCallback((): boolean => room.canRedo(), [room]);
264
-
265
- const canRedo = useSyncExternalStoreWithSelector(subscribe, getSnapshot, getSnapshot, identity);
266
-
267
- return canRedo;
268
- };
269
-
270
- const useCanUndo = (): boolean => {
271
- const room = useRoom();
272
-
273
- const subscribe = useCallback((onStoreChange: () => void) => room.storageRoot(onStoreChange), [room]);
274
-
275
- const getSnapshot = useCallback((): boolean => room.canUndo(), [room]);
276
-
277
- const canUndo = useSyncExternalStoreWithSelector(subscribe, getSnapshot, getSnapshot, identity);
278
-
279
- return canUndo;
280
- };
281
-
282
- const useConnection = <T extends unknown = WebSocketConnection>(
283
- selector = identity as (connection: WebSocketConnection) => T,
284
- options?: SubscriptionHookOptions<Id<T>>,
285
- ): Id<T> => {
286
- const room = useRoom();
287
-
288
- const subscribe = useCallback(
289
- (onStoreChange: () => void) => {
290
- return room.subscribe("connection", onStoreChange);
291
- },
292
- [room],
293
- );
294
-
295
- const getSnapshot = room.getConnection;
296
-
297
- const _selector = useCallback((snapshot: WebSocketConnection) => selector(snapshot) as Id<T>, [selector]);
298
-
299
- return useSyncExternalStoreWithSelector(
300
- subscribe,
301
- getSnapshot,
302
- getSnapshot,
303
- _selector,
304
- options?.isEqual ?? fastDeepEqual,
305
- );
306
- };
307
-
308
- const useDoc = () => {
309
- const room = useRoom();
310
-
311
- return room.getDoc();
312
- };
313
-
314
- const useEvent = <TType extends keyof InferIOOutput<MergeEvents<TEvents, TIO>>>(
315
- type: TType,
316
- callback: (data: Id<IOEventMessage<MergeEvents<TEvents, TIO>, TType>>) => void,
317
- ): void => {
318
- const room = useRoom();
319
-
320
- useEffect(() => {
321
- const unsubscribe = room.event(type, callback);
322
-
323
- return () => {
324
- unsubscribe();
325
- };
326
- }, [callback, room, type]);
327
- };
328
-
329
- const event = new Proxy(
330
- {},
331
- {
332
- get(_, prop) {
333
- const useProxyEvent = (
334
- callback: (
335
- data: Id<
336
- IOEventMessage<MergeEvents<TEvents, TIO>, keyof InferIOOutput<MergeEvents<TEvents, TIO>>>
337
- >,
338
- ) => void,
339
- ): void => {
340
- return useEvent(prop as keyof InferIOOutput<MergeEvents<TEvents, TIO>>, callback);
341
- };
342
-
343
- return { useEvent: useProxyEvent };
344
- },
345
- },
346
- ) as EventProxy<MergeEvents<TEvents, TIO>>;
347
-
348
- const useMyPresence = <T extends unknown = TPresence>(
349
- selector = identity as (myPresence: TPresence) => T,
350
- options?: SubscriptionHookOptions<Id<T> | null>,
351
- ): [Id<T>, Dispatch<UpdateMyPresenceAction<TPresence>>] => {
352
- const room = useRoom();
353
-
354
- const subscribe = useCallback(
355
- (onStoreChange: () => void) => {
356
- return room.subscribe("my-presence", onStoreChange);
357
- },
358
- [room],
359
- );
360
-
361
- const getSnapshot = room.getMyPresence;
362
-
363
- const _selector = useCallback((snapshot: TPresence) => selector(snapshot) as Id<T>, [selector]);
364
-
365
- const myPresence = useSyncExternalStoreWithSelector(
366
- subscribe,
367
- getSnapshot,
368
- getSnapshot,
369
- _selector,
370
- options?.isEqual ?? fastDeepEqual,
371
- );
372
-
373
- return [myPresence, room.updateMyPresence];
374
- };
375
-
376
- const useMyself = <T extends unknown = UserInfo<TIO, TPresence>>(
377
- selector = identity as (myself: Id<UserInfo<TIO, TPresence>>) => T,
378
- options?: SubscriptionHookOptions<Id<T> | null>,
379
- ): Id<T> | null => {
380
- const room = useRoom();
381
-
382
- const subscribe = useCallback(
383
- (onStoreChange: () => void) => {
384
- return room.subscribe("myself", onStoreChange);
385
- },
386
- [room],
387
- );
388
-
389
- const getSnapshot = room.getMyself;
390
-
391
- const _selector = useCallback(
392
- (snapshot: Id<UserInfo<TIO, TPresence>> | null) => {
393
- return !snapshot ? null : (selector(snapshot) as Id<T>);
394
- },
395
- [selector],
396
- );
397
-
398
- return useSyncExternalStoreWithSelector(
399
- subscribe,
400
- getSnapshot,
401
- getSnapshot,
402
- _selector,
403
- options?.isEqual ?? fastDeepEqual,
404
- );
405
- };
406
-
407
- const useOther = <T extends unknown = UserInfo<TIO, TPresence>>(
408
- connectionId: string,
409
- selector = identity as (other: UserInfo<TIO, TPresence>) => T,
410
- options?: SubscriptionHookOptions<T | null>,
411
- ): T | null => {
412
- const room = useRoom();
413
-
414
- const subscribe = useCallback(
415
- (onStoreChange: () => void) => {
416
- return room.other(connectionId, onStoreChange);
417
- },
418
- [room, connectionId],
419
- );
420
-
421
- const getSnapshot = useCallback(() => room.getOther(connectionId), [room, connectionId]);
422
-
423
- const _selector = useCallback(
424
- (snapshot: Id<UserInfo<TIO, TPresence>> | null) => {
425
- return !snapshot ? null : (selector(snapshot) as T);
426
- },
427
- [selector],
428
- );
429
-
430
- return useSyncExternalStoreWithSelector(
431
- subscribe,
432
- getSnapshot,
433
- getSnapshot,
434
- _selector,
435
- options?.isEqual ?? fastDeepEqual,
436
- );
437
- };
438
-
439
- const useOthers = <T extends unknown = readonly UserInfo<TIO, TPresence>[]>(
440
- selector = identity as (other: readonly Id<UserInfo<TIO, TPresence>>[]) => T,
441
- options?: SubscriptionHookOptions<T>,
442
- ): T => {
443
- const room = useRoom();
444
-
445
- const subscribe = useCallback(
446
- (onStoreChange: () => void) => {
447
- return room.subscribe("others", onStoreChange);
448
- },
449
- [room],
450
- );
451
-
452
- const getSnapshot = room.getOthers;
453
-
454
- return useSyncExternalStoreWithSelector(
455
- subscribe,
456
- getSnapshot,
457
- getSnapshot,
458
- selector as (other: readonly Id<UserInfo<TIO, TPresence>>[]) => T,
459
- options?.isEqual ??
460
- ((a, b) => {
461
- /**
462
- * !HACK
463
- * @description Assume the return type will always be an array when this occurs
464
- * and do a shallow comparison instead like how standard React dependency
465
- * arrays work. This is for performance's sake and may lead to bugs if the
466
- * selector doesn't always return an array
467
- * @date June 22, 2024
468
- */
469
- if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
470
- return shallowArrayEqual(a, b);
471
- }
472
-
473
- return fastDeepEqual(a, b);
474
- }),
475
- );
476
- };
477
-
478
- const useRedo = () => {
479
- const room = useRoom();
480
-
481
- return room.redo;
482
- };
483
-
484
- const useStorage = <TKey extends keyof TStorage, TData extends unknown = InferCrdtJson<TStorage[TKey]>>(
485
- key: TKey,
486
- selector = identity as (data: InferCrdtJson<TStorage[TKey]>) => TData,
487
- options?: SubscriptionHookOptions<TData | null>,
488
- ): [data: TData | null, sharedType: TStorage[TKey] | null] => {
489
- const room = useRoom();
490
- const rerender = useRerender();
491
-
492
- room.subscribe("storage-loaded", () => {
493
- rerender();
494
- });
495
-
496
- const subscribe = useCallback((onStoreChange: () => void) => room.storage(key, onStoreChange), [key, room]);
497
-
498
- const getSnapshot = useCallback((): InferCrdtJson<TStorage[TKey]> | null => {
499
- return room.getStorageJson(key);
500
- }, [key, room]);
501
-
502
- const _selector = useCallback(
503
- (snapshot: InferCrdtJson<TStorage[TKey]> | null) => {
504
- return snapshot === null ? null : selector(snapshot);
505
- },
506
- [selector],
507
- );
508
-
509
- const data = useSyncExternalStoreWithSelector(
510
- subscribe,
511
- getSnapshot,
512
- getSnapshot,
513
- _selector,
514
- options?.isEqual ?? fastDeepEqual,
515
- );
516
-
517
- const sharedType = room.getStorage(key) ?? null;
518
-
519
- return [data, sharedType];
520
- };
521
-
522
- const useTransact = () => {
523
- const room = useRoom();
524
-
525
- return room.transact;
526
- };
527
-
528
- const useUndo = () => {
529
- const room = useRoom();
530
-
531
- return room.undo;
532
- };
533
-
534
- return {
535
- // components
536
- MockedRoomProvider,
537
- PluvRoomProvider,
538
-
539
- // proxies
540
- event,
541
-
542
- // hooks
543
- useBroadcast,
544
- useCanRedo,
545
- useCanUndo,
546
- useConnection,
547
- useDoc,
548
- useEvent,
549
- useMyPresence,
550
- useMyself,
551
- useOther,
552
- useOthers,
553
- useRedo,
554
- useRoom,
555
- useStorage,
556
- useTransact,
557
- useUndo,
558
- };
559
- };