@pluv/react 5.0.1 → 5.0.2

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,121 +0,0 @@
1
- import { useEffect, useRef, useState } from "react";
2
- import fastDeepEqual from "fast-deep-equal";
3
-
4
- export type UseDeepAsyncMemoResult<T extends unknown> =
5
- | {
6
- error: null;
7
- isInitialized: true;
8
- isLoading: false;
9
- value: T;
10
- }
11
- | {
12
- error: null;
13
- isInitialized: boolean;
14
- value: undefined;
15
- isLoading: true;
16
- }
17
- | {
18
- error: Error;
19
- isInitialized: boolean;
20
- value: undefined;
21
- isLoading: false;
22
- };
23
-
24
- export const useDeepAsyncMemo = <T>(
25
- factory: () => Promise<T>,
26
- isEqual: (a: T, b: T) => boolean = fastDeepEqual,
27
- ): UseDeepAsyncMemoResult<T> => {
28
- // Store the current async state (loading, value, error, etc.)
29
- const [state, setState] = useState<UseDeepAsyncMemoResult<T>>({
30
- isInitialized: false,
31
- isLoading: true,
32
- value: undefined,
33
- error: null,
34
- });
35
-
36
- // Keep track of the last resolved value to compare with new ones
37
- const lastValueRef = useRef<T | undefined>(undefined);
38
- const hasValueRef = useRef(false); // Tracks whether we've ever resolved a value
39
-
40
- // Tracks the current in-flight promise to guard against race conditions
41
- const inflightRef = useRef<Promise<T> | null>(null);
42
-
43
- // Used to prevent state updates after the component unmounts
44
- const isMountedRef = useRef(true);
45
-
46
- useEffect(() => {
47
- isMountedRef.current = true;
48
-
49
- // Trigger the async factory
50
- const promise = factory();
51
- inflightRef.current = promise;
52
-
53
- // Set loading state
54
- // eslint-disable-next-line react-hooks/set-state-in-effect
55
- setState((prev) => {
56
- return {
57
- ...prev,
58
- isLoading: true,
59
- error: null,
60
- } as UseDeepAsyncMemoResult<T>;
61
- });
62
-
63
- promise
64
- .then((result) => {
65
- // Ignore if component unmounted or if this promise is stale
66
- if (!isMountedRef.current || inflightRef.current !== promise) return;
67
-
68
- // Skip update if value hasn't changed
69
- const isSame = hasValueRef.current && isEqual(lastValueRef.current as T, result);
70
- if (isSame) {
71
- setState((prev) => {
72
- return {
73
- ...prev,
74
- isInitialized: true,
75
- isLoading: false,
76
- } as UseDeepAsyncMemoResult<T>;
77
- });
78
- return;
79
- }
80
-
81
- // Save the new value
82
- lastValueRef.current = result;
83
- hasValueRef.current = true;
84
-
85
- setState({
86
- isInitialized: true,
87
- isLoading: false,
88
- value: result,
89
- error: null,
90
- });
91
- })
92
- .catch((err) => {
93
- // Ignore if component unmounted or if this promise is stale
94
- if (!isMountedRef.current || inflightRef.current !== promise) return;
95
-
96
- setState({
97
- isInitialized: hasValueRef.current,
98
- isLoading: false,
99
- value: undefined,
100
- error: err instanceof Error ? err : new Error("Unknown error"),
101
- });
102
- });
103
-
104
- // Cleanup: prevent state updates after unmount
105
- return () => {
106
- isMountedRef.current = false;
107
- };
108
- // eslint-disable-next-line react-hooks/exhaustive-deps
109
- }, [
110
- /**
111
- * !HACK
112
- * @description We're only going to use the `isEqual` function to determine if this hook
113
- * should re-run. This is basically our hook's version of a dependency array
114
- * @date April 13, 2025
115
- */
116
- // factory,
117
- isEqual,
118
- ]);
119
-
120
- return state;
121
- };
@@ -1,23 +0,0 @@
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
- // eslint-disable-next-line react-hooks/set-state-in-effect
9
- useEffect(() => setDidMount(typeof window !== "undefined"), []);
10
-
11
- const noSsr = useCallback(
12
- <T = null>(value: T | (() => T), fallback: T = null as T): T => {
13
- return didMount
14
- ? typeof value !== "function"
15
- ? value
16
- : (value as () => T)()
17
- : fallback;
18
- },
19
- [didMount],
20
- );
21
-
22
- return noSsr;
23
- };
@@ -1,9 +0,0 @@
1
- import { useCallback, useState } from "react";
2
-
3
- export const useRerender = (): (() => void) => {
4
- const [, setCount] = useState(0);
5
-
6
- return useCallback(() => {
7
- setCount((oldCount) => (oldCount + 1) % Number.MAX_SAFE_INTEGER);
8
- }, []);
9
- };
@@ -1,57 +0,0 @@
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
- // eslint-disable-next-line react-hooks/refs
56
- return typeof window !== "undefined" ? noSsr(result, _getServerSnapshot?.()) : result;
57
- };
@@ -1,7 +0,0 @@
1
- export {
2
- useAsyncQueue,
3
- useDeepAsyncMemo,
4
- useRerender,
5
- useSyncExternalStoreWithSelector,
6
- } from "./hooks";
7
- export { identity, shallowArrayEqual } from "./utils";
@@ -1 +0,0 @@
1
- export const identity = <T extends unknown>(x: T): T => x;
@@ -1,3 +0,0 @@
1
- export { identity } from "./identity";
2
- export { shallowArrayEqual } from "./shallowArrayEqual";
3
- export { shallowEqual } from "./shallowEqual";
@@ -1,6 +0,0 @@
1
- export const shallowArrayEqual = <T extends unknown>(
2
- a: readonly T[] | T[],
3
- b: readonly T[] | T[],
4
- ): boolean => {
5
- return a.length === b.length && a.every((item, i) => item === b[i]);
6
- };
@@ -1 +0,0 @@
1
- export const shallowEqual = (a: any, b: any): boolean => a === b;
package/src/types.ts DELETED
@@ -1,150 +0,0 @@
1
- import type {
2
- MergeEvents,
3
- MockedRoomEvents,
4
- PluvClient,
5
- PluvRoomDebug,
6
- PluvRouterEventConfig,
7
- UserInfo,
8
- WebSocketConnection,
9
- } from "@pluv/client";
10
- import type {
11
- AbstractCrdtDocFactory,
12
- InferCrdtJson,
13
- InferDoc,
14
- InferInitialStorageFn,
15
- InferStorage,
16
- } from "@pluv/crdt";
17
- import type {
18
- CrdtDocLike,
19
- Id,
20
- InferIOCrdtKind,
21
- InferIOInput,
22
- InferIOOutput,
23
- IOEventMessage,
24
- IOLike,
25
- JsonObject,
26
- MaybePromise,
27
- RoomLike,
28
- UpdateMyPresenceAction,
29
- } from "@pluv/types";
30
- import type { Dispatch, FC, ReactNode } from "react";
31
-
32
- export interface PluvProviderProps {
33
- children?: ReactNode;
34
- }
35
-
36
- type BaseRoomProviderProps<
37
- TPresence extends Record<string, any>,
38
- TCrdt extends AbstractCrdtDocFactory<any, any>,
39
- > = {
40
- children?: ReactNode;
41
- initialStorage?: keyof InferStorage<TCrdt> extends never ? never : InferInitialStorageFn<TCrdt>;
42
- room: string;
43
- } & (keyof TPresence extends never ? { initialPresence?: never } : { initialPresence: TPresence });
44
-
45
- export type MockedRoomProviderProps<
46
- TIO extends IOLike,
47
- TPresence extends Record<string, any>,
48
- TCrdt extends AbstractCrdtDocFactory<any, any>,
49
- TEvents extends PluvRouterEventConfig<TIO, TPresence, InferStorage<TCrdt>> = {},
50
- > = BaseRoomProviderProps<TPresence, TCrdt> & {
51
- events?: MockedRoomEvents<MergeEvents<TEvents, TIO>>;
52
- };
53
-
54
- export type BroadcastProxy<TIO extends IOLike> = (<TEvent extends keyof InferIOInput<TIO>>(
55
- event: TEvent,
56
- data: Id<InferIOInput<TIO>[TEvent]>,
57
- ) => void) & {
58
- [event in keyof InferIOInput<TIO>]: (input: Id<InferIOInput<TIO>[event]>) => void;
59
- };
60
-
61
- export type EventProxy<TIO extends IOLike> = {
62
- [event in keyof InferIOOutput<TIO>]: {
63
- useEvent: (callback: (data: Id<IOEventMessage<TIO, event>>) => void) => void;
64
- };
65
- };
66
-
67
- export type MetadataGetter<TMetadata extends JsonObject> =
68
- TMetadata | (() => MaybePromise<TMetadata>);
69
-
70
- export type PluvRoomProviderProps<
71
- TIO extends IOLike<any, any, any>,
72
- TMetadata extends JsonObject,
73
- TPresence extends Record<string, any>,
74
- TCrdt extends AbstractCrdtDocFactory<any, any>,
75
- > = BaseRoomProviderProps<TPresence, TCrdt> & {
76
- connect?: boolean;
77
- debug?: boolean | PluvRoomDebug<TIO>;
78
- onAuthorizationFail?: (error: Error) => void;
79
- } & (keyof TMetadata extends never
80
- ? { metadata?: undefined }
81
- : { metadata: MetadataGetter<TMetadata> });
82
-
83
- export interface SubscriptionHookOptions<T extends unknown> {
84
- isEqual?: (a: T, b: T) => boolean;
85
- }
86
-
87
- export interface CreateBundle<
88
- TIO extends IOLike<any, any, any>,
89
- TMetadata extends JsonObject,
90
- TPresence extends Record<string, any> = {},
91
- TCrdt extends InferIOCrdtKind<TIO> = InferIOCrdtKind<TIO>,
92
- TEvents extends PluvRouterEventConfig<TIO, TPresence, InferStorage<TCrdt>> = {},
93
- > {
94
- // components
95
- MockedRoomProvider: FC<MockedRoomProviderProps<TIO, TPresence, TCrdt>>;
96
- PluvProvider: FC<PluvProviderProps>;
97
- PluvRoomProvider: FC<PluvRoomProviderProps<TIO, TMetadata, TPresence, TCrdt>>;
98
-
99
- // proxies
100
- event: EventProxy<MergeEvents<TEvents, TIO>>;
101
-
102
- // hooks
103
- useBroadcast: () => BroadcastProxy<MergeEvents<TEvents, TIO>>;
104
- useCanRedo: () => boolean;
105
- useCanUndo: () => boolean;
106
- useClient: () => PluvClient<TIO, TPresence, TCrdt, TMetadata>;
107
- useConnection: <T extends unknown = WebSocketConnection>(
108
- selector?: (connection: WebSocketConnection) => T,
109
- options?: SubscriptionHookOptions<Id<T>>,
110
- ) => Id<T>;
111
- useDoc: () => CrdtDocLike<InferDoc<TCrdt>, InferStorage<TCrdt>>;
112
- useEvent: <TType extends keyof InferIOOutput<MergeEvents<TEvents, TIO>>>(
113
- type: TType,
114
- callback: (data: Id<IOEventMessage<MergeEvents<TEvents, TIO>, TType>>) => void,
115
- ) => void;
116
- useMyPresence: <T extends unknown = TPresence>(
117
- selector?: (myPresence: TPresence) => T,
118
- options?: SubscriptionHookOptions<Id<T> | null>,
119
- ) => [myPresence: Id<T>, updateMyPresence: Dispatch<UpdateMyPresenceAction<TPresence>>];
120
- useMyself: <T extends unknown = UserInfo<TIO, TPresence>>(
121
- selector?: (myself: Id<UserInfo<TIO, TPresence>>) => T,
122
- options?: SubscriptionHookOptions<Id<T> | null>,
123
- ) => Id<T> | null;
124
- useOther: <T extends unknown = UserInfo<TIO, TPresence>>(
125
- connectionId: string,
126
- selector?: (other: UserInfo<TIO, TPresence>) => T,
127
- options?: SubscriptionHookOptions<T | null>,
128
- ) => T | null;
129
- useOthers: <T extends unknown = readonly UserInfo<TIO, TPresence>[]>(
130
- selector?: (other: readonly Id<UserInfo<TIO, TPresence>>[]) => T,
131
- options?: SubscriptionHookOptions<T>,
132
- ) => T;
133
- useRedo: () => () => void;
134
- useRoom: () => RoomLike<TIO, InferDoc<TCrdt>, TPresence, InferStorage<TCrdt>, TEvents>;
135
- useStorage: <
136
- TKey extends keyof InferStorage<TCrdt>,
137
- TData extends unknown = InferCrdtJson<InferStorage<TCrdt>[TKey]>,
138
- >(
139
- key: TKey,
140
- selector?: (data: InferCrdtJson<InferStorage<TCrdt>[TKey]>) => TData,
141
- options?: SubscriptionHookOptions<TData | null>,
142
- ) => [data: TData | null, sharedType: InferStorage<TCrdt>[TKey] | null];
143
- useTransact: () => (fn: (storage: InferStorage<TCrdt>) => void, origin?: string) => void;
144
- useUndo: () => () => void;
145
- }
146
-
147
- export type InferBundleRoom<TBundle extends CreateBundle<any, any, any, any, any>> =
148
- TBundle extends CreateBundle<infer IIO, any, infer IPresence, infer ICrdt, infer IEvents>
149
- ? RoomLike<IIO, InferDoc<ICrdt>, IPresence, InferStorage<ICrdt>, IEvents>
150
- : never;
package/tsconfig.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "@pluv/tsconfig/react-library.json",
3
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
4
- "exclude": ["node_modules"],
5
- "compilerOptions": {
6
- "lib": ["dom", "esnext"]
7
- }
8
- }
package/tsdown.config.ts DELETED
@@ -1,15 +0,0 @@
1
- import { defineConfig } from "tsdown";
2
-
3
- export default defineConfig({
4
- format: ["esm"],
5
- dts: true,
6
- outDir: "dist",
7
- entry: "src/index.ts",
8
- target: "esnext",
9
- sourcemap: true,
10
- clean: true,
11
- external: ["react", "react-dom"],
12
- exports: {
13
- all: true,
14
- },
15
- });