@pluv/react 5.0.1 → 5.0.3
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/dist/index.d.mts +70 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +423 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +9 -9
- package/CHANGELOG.md +0 -2499
- package/eslint.config.mjs +0 -4
- package/src/createBundle.tsx +0 -626
- package/src/index.ts +0 -9
- package/src/internal/hooks/index.ts +0 -5
- package/src/internal/hooks/useAsyncQueue.ts +0 -18
- package/src/internal/hooks/useDeepAsyncMemo.ts +0 -121
- package/src/internal/hooks/useNoSsr.ts +0 -23
- package/src/internal/hooks/useRerender.ts +0 -9
- package/src/internal/hooks/useSyncExternalStoreWithSelector.ts +0 -57
- package/src/internal/index.ts +0 -7
- package/src/internal/utils/identity.ts +0 -1
- package/src/internal/utils/index.ts +0 -3
- package/src/internal/utils/shallowArrayEqual.ts +0 -6
- package/src/internal/utils/shallowEqual.ts +0 -1
- package/src/types.ts +0 -150
- package/tsconfig.json +0 -8
- package/tsdown.config.ts +0 -15
|
@@ -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,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
|
-
};
|
package/src/internal/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const identity = <T extends unknown>(x: T): T => x;
|
|
@@ -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
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
|
-
});
|