@pluv/react 0.18.0 → 0.20.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.
- package/.turbo/turbo-build.log +15 -15
- package/CHANGELOG.md +94 -0
- package/README.md +0 -138
- package/dist/index.d.mts +12 -12
- package/dist/index.d.ts +12 -12
- package/dist/index.js +99 -59
- package/dist/index.mjs +81 -41
- package/package.json +10 -11
- package/src/createBundle.tsx +3 -6
- package/src/createRoomBundle.tsx +48 -45
- package/src/index.ts +1 -1
- package/src/internal/hooks/index.ts +1 -1
- package/src/internal/hooks/useNoSsr.ts +18 -0
- package/src/internal/hooks/useSyncExternalStoreWithSelector.ts +56 -1
- package/src/internal/utils/index.ts +1 -0
- package/src/internal/utils/shallowEqual.ts +1 -0
- package/tsconfig.json +4 -1
- package/src/internal/hooks/useSyncExternalStore.ts +0 -10
package/src/createBundle.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PluvClient } from "@pluv/client";
|
|
2
|
-
import type {
|
|
2
|
+
import type { CrdtType } from "@pluv/crdt";
|
|
3
3
|
import type { IOLike, JsonObject } from "@pluv/types";
|
|
4
4
|
import type { FC, ReactNode } from "react";
|
|
5
5
|
import { createContext, memo, useContext } from "react";
|
|
@@ -12,10 +12,7 @@ export interface PluvProviderProps {
|
|
|
12
12
|
|
|
13
13
|
export interface CreateBundle<TIO extends IOLike> {
|
|
14
14
|
// factories
|
|
15
|
-
createRoomBundle: <
|
|
16
|
-
TPresence extends JsonObject = {},
|
|
17
|
-
TStorage extends Record<string, AbstractCrdtType<any, any>> = {},
|
|
18
|
-
>(
|
|
15
|
+
createRoomBundle: <TPresence extends JsonObject = {}, TStorage extends Record<string, CrdtType<any, any>> = {}>(
|
|
19
16
|
options: CreateRoomBundleOptions<TIO, TPresence, TStorage>,
|
|
20
17
|
) => CreateRoomBundle<TIO, TPresence, TStorage>;
|
|
21
18
|
|
|
@@ -48,7 +45,7 @@ export const createBundle = <TIO extends IOLike>(client: PluvClient<TIO>): Creat
|
|
|
48
45
|
|
|
49
46
|
const createRoomBundle = <
|
|
50
47
|
TPresence extends JsonObject = {},
|
|
51
|
-
TStorage extends Record<string,
|
|
48
|
+
TStorage extends Record<string, CrdtType<any, any>> = {},
|
|
52
49
|
>(
|
|
53
50
|
options: CreateRoomBundleOptions<TIO, TPresence, TStorage>,
|
|
54
51
|
): CreateRoomBundle<TIO, TPresence, TStorage> => _createRoomBundle(client, options);
|
package/src/createRoomBundle.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
WebSocketConnection,
|
|
10
10
|
} from "@pluv/client";
|
|
11
11
|
import { MockedRoom } from "@pluv/client";
|
|
12
|
-
import type { AbstractCrdtDoc, AbstractCrdtDocFactory,
|
|
12
|
+
import type { AbstractCrdtDoc, AbstractCrdtDocFactory, CrdtType, InferCrdtJson } from "@pluv/crdt";
|
|
13
13
|
import { noop } from "@pluv/crdt";
|
|
14
14
|
import type { IOEventMessage, IOLike, Id, InferIOInput, InferIOOutput, InputZodLike, JsonObject } from "@pluv/types";
|
|
15
15
|
import fastDeepEqual from "fast-deep-equal";
|
|
@@ -20,17 +20,14 @@ import { identity, shallowArrayEqual, useRerender, useSyncExternalStoreWithSelec
|
|
|
20
20
|
export type CreateRoomBundleOptions<
|
|
21
21
|
TIO extends IOLike,
|
|
22
22
|
TPresence extends JsonObject = {},
|
|
23
|
-
TStorage extends Record<string,
|
|
23
|
+
TStorage extends Record<string, CrdtType<any, any>> = {},
|
|
24
24
|
> = {
|
|
25
25
|
addons?: readonly PluvRoomAddon<TIO, TPresence, TStorage>[];
|
|
26
26
|
initialStorage?: AbstractCrdtDocFactory<TStorage>;
|
|
27
27
|
presence?: InputZodLike<TPresence>;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
type BaseRoomProviderProps<
|
|
31
|
-
TPresence extends JsonObject,
|
|
32
|
-
TStorage extends Record<string, AbstractCrdtType<any, any>>,
|
|
33
|
-
> = {
|
|
30
|
+
type BaseRoomProviderProps<TPresence extends JsonObject, TStorage extends Record<string, CrdtType<any, any>>> = {
|
|
34
31
|
children?: ReactNode;
|
|
35
32
|
initialStorage?: keyof TStorage extends never ? never : () => TStorage;
|
|
36
33
|
room: string;
|
|
@@ -39,7 +36,7 @@ type BaseRoomProviderProps<
|
|
|
39
36
|
export type MockedRoomProviderProps<
|
|
40
37
|
TIO extends IOLike,
|
|
41
38
|
TPresence extends JsonObject,
|
|
42
|
-
TStorage extends Record<string,
|
|
39
|
+
TStorage extends Record<string, CrdtType<any, any>>,
|
|
43
40
|
> = BaseRoomProviderProps<TPresence, TStorage> & {
|
|
44
41
|
events?: MockedRoomEvents<TIO>;
|
|
45
42
|
};
|
|
@@ -47,7 +44,7 @@ export type MockedRoomProviderProps<
|
|
|
47
44
|
export type PluvRoomProviderProps<
|
|
48
45
|
TIO extends IOLike,
|
|
49
46
|
TPresence extends JsonObject,
|
|
50
|
-
TStorage extends Record<string,
|
|
47
|
+
TStorage extends Record<string, CrdtType<any, any>>,
|
|
51
48
|
> = BaseRoomProviderProps<TPresence, TStorage> & {
|
|
52
49
|
debug?: boolean | PluvRoomDebug<TIO>;
|
|
53
50
|
onAuthorizationFail?: (error: Error) => void;
|
|
@@ -65,7 +62,7 @@ export type BroadcastProxy<TIO extends IOLike> = (<TEvent extends keyof InferIOI
|
|
|
65
62
|
event: TEvent,
|
|
66
63
|
data: Id<InferIOInput<TIO>[TEvent]>,
|
|
67
64
|
) => void) & {
|
|
68
|
-
[event in keyof InferIOInput<TIO>]: (
|
|
65
|
+
[event in keyof InferIOInput<TIO>]: (input: Id<InferIOInput<TIO>[event]>) => void;
|
|
69
66
|
};
|
|
70
67
|
|
|
71
68
|
export type EventProxy<TIO extends IOLike> = {
|
|
@@ -77,7 +74,7 @@ export type EventProxy<TIO extends IOLike> = {
|
|
|
77
74
|
export interface CreateRoomBundle<
|
|
78
75
|
TIO extends IOLike,
|
|
79
76
|
TPresence extends JsonObject,
|
|
80
|
-
TStorage extends Record<string,
|
|
77
|
+
TStorage extends Record<string, CrdtType<any, any>>,
|
|
81
78
|
> {
|
|
82
79
|
// components
|
|
83
80
|
MockedRoomProvider: FC<MockedRoomProviderProps<TIO, TPresence, TStorage>>;
|
|
@@ -110,17 +107,17 @@ export interface CreateRoomBundle<
|
|
|
110
107
|
useOther: <T extends unknown = UserInfo<TIO, TPresence>>(
|
|
111
108
|
connectionId: string,
|
|
112
109
|
selector?: (other: UserInfo<TIO, TPresence>) => T,
|
|
113
|
-
options?: SubscriptionHookOptions<
|
|
114
|
-
) =>
|
|
115
|
-
useOthers: <T extends unknown = UserInfo<TIO, TPresence
|
|
116
|
-
selector?: (other: readonly Id<UserInfo<TIO, TPresence>>[]) => T
|
|
117
|
-
options?: SubscriptionHookOptions<
|
|
118
|
-
) =>
|
|
110
|
+
options?: SubscriptionHookOptions<T | null>,
|
|
111
|
+
) => T | null;
|
|
112
|
+
useOthers: <T extends unknown = readonly UserInfo<TIO, TPresence>[]>(
|
|
113
|
+
selector?: (other: readonly Id<UserInfo<TIO, TPresence>>[]) => T,
|
|
114
|
+
options?: SubscriptionHookOptions<T>,
|
|
115
|
+
) => T;
|
|
119
116
|
useRedo: () => () => void;
|
|
120
117
|
useRoom: () => AbstractRoom<TIO, TPresence, TStorage>;
|
|
121
|
-
useStorage: <TKey extends keyof TStorage, TData extends unknown =
|
|
118
|
+
useStorage: <TKey extends keyof TStorage, TData extends unknown = InferCrdtJson<TStorage[TKey]>>(
|
|
122
119
|
key: TKey,
|
|
123
|
-
selector?: (data:
|
|
120
|
+
selector?: (data: InferCrdtJson<TStorage[TKey]>) => TData,
|
|
124
121
|
options?: SubscriptionHookOptions<TData | null>,
|
|
125
122
|
) => [data: TData | null, sharedType: TStorage[TKey] | null];
|
|
126
123
|
useTransact: () => (fn: (storage: TStorage) => void, origin?: string) => void;
|
|
@@ -133,7 +130,7 @@ export type InferRoomStorage<TRoomBundle extends CreateRoomBundle<any, any, any>
|
|
|
133
130
|
export const createRoomBundle = <
|
|
134
131
|
TIO extends IOLike,
|
|
135
132
|
TPresence extends JsonObject = {},
|
|
136
|
-
TStorage extends Record<string,
|
|
133
|
+
TStorage extends Record<string, CrdtType<any, any>> = {},
|
|
137
134
|
>(
|
|
138
135
|
client: PluvClient<TIO>,
|
|
139
136
|
options: CreateRoomBundleOptions<TIO, TPresence, TStorage>,
|
|
@@ -258,9 +255,9 @@ export const createRoomBundle = <
|
|
|
258
255
|
|
|
259
256
|
const getSnapshot = useCallback((): boolean => room.canUndo(), [room]);
|
|
260
257
|
|
|
261
|
-
const
|
|
258
|
+
const canUndo = useSyncExternalStoreWithSelector(subscribe, getSnapshot, getSnapshot, identity);
|
|
262
259
|
|
|
263
|
-
return
|
|
260
|
+
return canUndo;
|
|
264
261
|
};
|
|
265
262
|
|
|
266
263
|
const useConnection = <T extends unknown = WebSocketConnection>(
|
|
@@ -350,16 +347,7 @@ export const createRoomBundle = <
|
|
|
350
347
|
options?.isEqual ?? fastDeepEqual,
|
|
351
348
|
);
|
|
352
349
|
|
|
353
|
-
|
|
354
|
-
(value: UpdateMyPresenceAction<TPresence>): void => {
|
|
355
|
-
const presence = typeof value === "function" ? value(room.getMyPresence()) : value;
|
|
356
|
-
|
|
357
|
-
room.updateMyPresence(presence);
|
|
358
|
-
},
|
|
359
|
-
[room],
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
return [myPresence, updateMyPresence];
|
|
350
|
+
return [myPresence, room.updateMyPresence];
|
|
363
351
|
};
|
|
364
352
|
|
|
365
353
|
const useMyself = <T extends unknown = UserInfo<TIO, TPresence>>(
|
|
@@ -396,8 +384,8 @@ export const createRoomBundle = <
|
|
|
396
384
|
const useOther = <T extends unknown = UserInfo<TIO, TPresence>>(
|
|
397
385
|
connectionId: string,
|
|
398
386
|
selector = identity as (other: UserInfo<TIO, TPresence>) => T,
|
|
399
|
-
options?: SubscriptionHookOptions<
|
|
400
|
-
):
|
|
387
|
+
options?: SubscriptionHookOptions<T | null>,
|
|
388
|
+
): T | null => {
|
|
401
389
|
const room = useRoom();
|
|
402
390
|
|
|
403
391
|
const subscribe = useCallback(
|
|
@@ -411,7 +399,7 @@ export const createRoomBundle = <
|
|
|
411
399
|
|
|
412
400
|
const _selector = useCallback(
|
|
413
401
|
(snapshot: Id<UserInfo<TIO, TPresence>> | null) => {
|
|
414
|
-
return !snapshot ? null : (selector(snapshot) as
|
|
402
|
+
return !snapshot ? null : (selector(snapshot) as T);
|
|
415
403
|
},
|
|
416
404
|
[selector],
|
|
417
405
|
);
|
|
@@ -425,10 +413,10 @@ export const createRoomBundle = <
|
|
|
425
413
|
);
|
|
426
414
|
};
|
|
427
415
|
|
|
428
|
-
const useOthers = <T extends unknown = UserInfo<TIO, TPresence
|
|
429
|
-
selector = identity as (other: readonly Id<UserInfo<TIO, TPresence>>[]) => T
|
|
430
|
-
options?: SubscriptionHookOptions<
|
|
431
|
-
):
|
|
416
|
+
const useOthers = <T extends unknown = readonly UserInfo<TIO, TPresence>[]>(
|
|
417
|
+
selector = identity as (other: readonly Id<UserInfo<TIO, TPresence>>[]) => T,
|
|
418
|
+
options?: SubscriptionHookOptions<T>,
|
|
419
|
+
): T => {
|
|
432
420
|
const room = useRoom();
|
|
433
421
|
|
|
434
422
|
const subscribe = useCallback(
|
|
@@ -444,8 +432,23 @@ export const createRoomBundle = <
|
|
|
444
432
|
subscribe,
|
|
445
433
|
getSnapshot,
|
|
446
434
|
getSnapshot,
|
|
447
|
-
selector as (other: readonly Id<UserInfo<TIO, TPresence>>[]) =>
|
|
448
|
-
options?.isEqual ??
|
|
435
|
+
selector as (other: readonly Id<UserInfo<TIO, TPresence>>[]) => T,
|
|
436
|
+
options?.isEqual ??
|
|
437
|
+
((a, b) => {
|
|
438
|
+
/**
|
|
439
|
+
* !HACK
|
|
440
|
+
* @description Assume the return type will always be an array when this occurs
|
|
441
|
+
* and do a shallow comparison instead like how standard React dependency
|
|
442
|
+
* arrays work. This is for performance's sake and may lead to bugs if the
|
|
443
|
+
* selector doesn't always return an array
|
|
444
|
+
* @date June 22, 2024
|
|
445
|
+
*/
|
|
446
|
+
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
|
|
447
|
+
return shallowArrayEqual(a, b);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return fastDeepEqual(a, b);
|
|
451
|
+
}),
|
|
449
452
|
);
|
|
450
453
|
};
|
|
451
454
|
|
|
@@ -455,9 +458,9 @@ export const createRoomBundle = <
|
|
|
455
458
|
return room.redo;
|
|
456
459
|
};
|
|
457
460
|
|
|
458
|
-
const useStorage = <TKey extends keyof TStorage, TData extends unknown =
|
|
461
|
+
const useStorage = <TKey extends keyof TStorage, TData extends unknown = InferCrdtJson<TStorage[TKey]>>(
|
|
459
462
|
key: TKey,
|
|
460
|
-
selector = identity as (data:
|
|
463
|
+
selector = identity as (data: InferCrdtJson<TStorage[TKey]>) => TData,
|
|
461
464
|
options?: SubscriptionHookOptions<TData | null>,
|
|
462
465
|
): [data: TData | null, sharedType: TStorage[TKey] | null] => {
|
|
463
466
|
const room = useRoom();
|
|
@@ -469,12 +472,12 @@ export const createRoomBundle = <
|
|
|
469
472
|
|
|
470
473
|
const subscribe = useCallback((onStoreChange: () => void) => room.storage(key, onStoreChange), [key, room]);
|
|
471
474
|
|
|
472
|
-
const getSnapshot = useCallback(():
|
|
473
|
-
return room.
|
|
475
|
+
const getSnapshot = useCallback((): InferCrdtJson<TStorage[TKey]> | null => {
|
|
476
|
+
return room.getStorageJson(key);
|
|
474
477
|
}, [key, room]);
|
|
475
478
|
|
|
476
479
|
const _selector = useCallback(
|
|
477
|
-
(snapshot:
|
|
480
|
+
(snapshot: InferCrdtJson<TStorage[TKey]> | null) => {
|
|
478
481
|
return snapshot === null ? null : selector(snapshot);
|
|
479
482
|
},
|
|
480
483
|
[selector],
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { ConnectionState, createClient } from "@pluv/client";
|
|
2
|
-
export type {
|
|
2
|
+
export type { CrdtType, InferCrdtJson } from "@pluv/crdt";
|
|
3
3
|
export type { BaseUser, EventMessage, EventRecord, IOEventMessage } from "@pluv/types";
|
|
4
4
|
export { createBundle } from "./createBundle";
|
|
5
5
|
export type { CreateBundle, PluvProviderProps } from "./createBundle";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export type NoSsrFunction = <T = null>(value: T | (() => T), fallback?: T) => T;
|
|
4
|
+
|
|
5
|
+
export const useNoSsr = (): NoSsrFunction => {
|
|
6
|
+
const [didMount, setDidMount] = useState<boolean>(false);
|
|
7
|
+
|
|
8
|
+
useEffect(() => setDidMount(typeof window !== "undefined"), []);
|
|
9
|
+
|
|
10
|
+
const noSsr = useCallback(
|
|
11
|
+
<T = null>(value: T | (() => T), fallback: T = null as T): T => {
|
|
12
|
+
return didMount ? (typeof value !== "function" ? value : (value as () => T)()) : fallback;
|
|
13
|
+
},
|
|
14
|
+
[didMount],
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
return noSsr;
|
|
18
|
+
};
|
|
@@ -1 +1,56 @@
|
|
|
1
|
-
|
|
1
|
+
import { useRef, useSyncExternalStore } from "react";
|
|
2
|
+
import { shallowEqual } from "../utils";
|
|
3
|
+
import { useNoSsr } from "./useNoSsr";
|
|
4
|
+
|
|
5
|
+
export const useSyncExternalStoreWithSelector = <TSnapshot, TSelection>(
|
|
6
|
+
subscribe: (onStoreChange: () => void) => () => void,
|
|
7
|
+
getClientSnapshot: () => TSnapshot,
|
|
8
|
+
getServerSnapshot: undefined | null | (() => TSnapshot),
|
|
9
|
+
selector: (snapshot: TSnapshot) => TSelection,
|
|
10
|
+
isEqual?: (a: TSelection, b: TSelection) => boolean,
|
|
11
|
+
): TSelection => {
|
|
12
|
+
const noSsr = useNoSsr();
|
|
13
|
+
const cacheRef = useRef<TSelection | null>(null);
|
|
14
|
+
|
|
15
|
+
const _getServerSnapshot = getServerSnapshot
|
|
16
|
+
? () => {
|
|
17
|
+
const data = getServerSnapshot();
|
|
18
|
+
const snapshot = selector(data);
|
|
19
|
+
const compare = isEqual ?? shallowEqual;
|
|
20
|
+
const cached = cacheRef.current;
|
|
21
|
+
|
|
22
|
+
const isUnchanged = cached === null ? snapshot === null : compare(cached, snapshot);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* !HACK
|
|
26
|
+
* @description Memoize only the last snapshot so that we can only rerender on changes
|
|
27
|
+
* @date June 22, 2024
|
|
28
|
+
*/
|
|
29
|
+
cacheRef.current = isUnchanged ? cached ?? snapshot : snapshot;
|
|
30
|
+
|
|
31
|
+
return cacheRef.current;
|
|
32
|
+
}
|
|
33
|
+
: undefined;
|
|
34
|
+
|
|
35
|
+
const _getClientSnapshot = () => {
|
|
36
|
+
const data = getClientSnapshot();
|
|
37
|
+
const snapshot = selector(data);
|
|
38
|
+
const compare = isEqual ?? shallowEqual;
|
|
39
|
+
const cached = cacheRef.current;
|
|
40
|
+
|
|
41
|
+
const isUnchanged = cached === null ? snapshot === null : compare(cached, snapshot);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* !HACK
|
|
45
|
+
* @description Memoize only the last snapshot so that we can only rerender on changes
|
|
46
|
+
* @date June 22, 2024
|
|
47
|
+
*/
|
|
48
|
+
cacheRef.current = isUnchanged ? cached ?? snapshot : snapshot;
|
|
49
|
+
|
|
50
|
+
return cacheRef.current;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const result = useSyncExternalStore(subscribe, _getClientSnapshot, _getServerSnapshot);
|
|
54
|
+
|
|
55
|
+
return typeof window !== "undefined" ? noSsr(result, _getServerSnapshot?.()) : result;
|
|
56
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const shallowEqual = (a: any, b: any): boolean => a === b;
|
package/tsconfig.json
CHANGED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { useSyncExternalStoreWithSelector } from "./useSyncExternalStoreWithSelector";
|
|
2
|
-
import { identity } from "../utils";
|
|
3
|
-
|
|
4
|
-
export const useSyncExternalStore = <TSnapshot extends unknown>(
|
|
5
|
-
subscribe: (onStoreChange: () => void) => () => void,
|
|
6
|
-
getSnapshot: () => TSnapshot,
|
|
7
|
-
getServerSnapshot: undefined | null | (() => TSnapshot),
|
|
8
|
-
) => {
|
|
9
|
-
return useSyncExternalStoreWithSelector(subscribe, getSnapshot, getServerSnapshot, identity);
|
|
10
|
-
};
|