@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.
@@ -1,5 +1,5 @@
1
1
  import type { PluvClient } from "@pluv/client";
2
- import type { AbstractCrdtType } from "@pluv/crdt";
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, AbstractCrdtType<any, any>> = {},
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);
@@ -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, AbstractCrdtType, InferCrdtStorageJson } from "@pluv/crdt";
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, AbstractCrdtType<any, any>> = {},
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, AbstractCrdtType<any, any>>,
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, AbstractCrdtType<any, any>>,
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>]: (callback: Id<InferIOInput<TIO>[event]>) => void;
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, AbstractCrdtType<any, any>>,
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<Id<T> | null>,
114
- ) => Id<T> | null;
115
- useOthers: <T extends unknown = UserInfo<TIO, TPresence>>(
116
- selector?: (other: readonly Id<UserInfo<TIO, TPresence>>[]) => T[],
117
- options?: SubscriptionHookOptions<readonly Id<T>[]>,
118
- ) => readonly Id<T>[];
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 = InferCrdtStorageJson<TStorage[TKey]>>(
118
+ useStorage: <TKey extends keyof TStorage, TData extends unknown = InferCrdtJson<TStorage[TKey]>>(
122
119
  key: TKey,
123
- selector?: (data: InferCrdtStorageJson<TStorage[TKey]>) => TData,
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, AbstractCrdtType<any, any>> = {},
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 canRedo = useSyncExternalStoreWithSelector(subscribe, getSnapshot, getSnapshot, identity);
258
+ const canUndo = useSyncExternalStoreWithSelector(subscribe, getSnapshot, getSnapshot, identity);
262
259
 
263
- return canRedo;
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
- const updateMyPresence: Dispatch<UpdateMyPresenceAction<TPresence>> = useCallback(
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<Id<T> | null>,
400
- ): Id<T> | null => {
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 Id<T>);
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<readonly Id<T>[]>,
431
- ): readonly Id<T>[] => {
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>>[]) => Id<T>[],
448
- options?.isEqual ?? shallowArrayEqual,
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 = InferCrdtStorageJson<TStorage[TKey]>>(
461
+ const useStorage = <TKey extends keyof TStorage, TData extends unknown = InferCrdtJson<TStorage[TKey]>>(
459
462
  key: TKey,
460
- selector = identity as (data: InferCrdtStorageJson<TStorage[TKey]>) => TData,
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((): InferCrdtStorageJson<TStorage[TKey]> | null => {
473
- return room.getStorage(key)?.toJson();
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: InferCrdtStorageJson<TStorage[TKey]> | null) => {
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 { AbstractCrdtType, InferCrdtStorageJson } from "@pluv/crdt";
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";
@@ -1,3 +1,3 @@
1
+ export { useNoSsr } from "./useNoSsr";
1
2
  export { useRerender } from "./useRerender";
2
- export { useSyncExternalStore } from "./useSyncExternalStore";
3
3
  export { useSyncExternalStoreWithSelector } from "./useSyncExternalStoreWithSelector";
@@ -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
- export { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector.js";
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
+ };
@@ -1,2 +1,3 @@
1
1
  export { identity } from "./identity";
2
2
  export { shallowArrayEqual } from "./shallowArrayEqual";
3
+ export { shallowEqual } from "./shallowEqual";
@@ -0,0 +1 @@
1
+ export const shallowEqual = (a: any, b: any): boolean => a === b;
package/tsconfig.json CHANGED
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "extends": "@pluv/tsconfig/react-library.json",
3
3
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
4
- "exclude": ["node_modules"]
4
+ "exclude": ["node_modules"],
5
+ "compilerOptions": {
6
+ "lib": ["dom", "esnext"]
7
+ }
5
8
  }
@@ -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
- };