@noya-app/noya-multiplayer-react 0.1.42 → 0.1.44

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/src/hooks.ts CHANGED
@@ -3,19 +3,24 @@
3
3
  import { GetAtPath } from "@noya-app/observable";
4
4
  import {
5
5
  ActionHandler,
6
+ AIManager,
6
7
  AssetManager,
8
+ ConnectedUsersManager,
7
9
  ConnectionEvent,
8
- EphemeralUserData,
10
+ EphemeralUserDataManager,
9
11
  MultiplayerStateManager,
10
12
  MultiplayerStateManagerError,
11
13
  MultiplayerStateManagerOptions,
12
- MultiplayerUser,
14
+ RPCManager,
15
+ StandardRPCRequestPayload,
16
+ StandardRPCResponsePayload,
13
17
  StateManager,
14
18
  StateManagerOptions,
15
19
  stubSync,
16
20
  SyncAdapter,
17
21
  SyncAdapterOptions,
18
22
  Task,
23
+ WorkflowManager,
19
24
  } from "@noya-app/state-manager";
20
25
  import {
21
26
  createElement,
@@ -28,6 +33,7 @@ import {
28
33
  useSyncExternalStore,
29
34
  } from "react";
30
35
  import { createRoot } from "react-dom/client";
36
+ import { SecretManager } from "../../noya-state-manager/src/SecretManager";
31
37
  import { ErrorOverlay } from "./components/ErrorOverlay";
32
38
  import { shouldTrackEvents$, shouldTrackTasks$ } from "./globals";
33
39
  import { StateInspectorOptions } from "./inspector/StateInspector";
@@ -138,68 +144,17 @@ export function useSyncMultiplayerStateManager<S, M = void>(
138
144
  );
139
145
  }
140
146
 
141
- function throttle<F extends (...args: any[]) => any>(fn: F, ms: number) {
142
- let lastCall = 0;
143
- let timeoutId: number | null = null;
144
- return (...args: Parameters<F>) => {
145
- const now = Date.now();
146
- if (now - lastCall < ms) {
147
- if (timeoutId) clearTimeout(timeoutId);
148
- timeoutId = setTimeout(
149
- () => {
150
- lastCall = now;
151
- fn(...args);
152
- },
153
- ms - (now - lastCall)
154
- ) as unknown as number;
155
- return;
156
- }
157
- lastCall = now;
158
- return fn(...args);
159
- };
160
- }
161
-
162
- export function useEphemeralUserData<E>(
163
- ephemeralUserData: EphemeralUserData<E>,
164
- options?: {
165
- throttle?: number;
166
- }
167
- ) {
168
- const throttledAddListener = useMemo(() => {
169
- const throttleMs = options?.throttle ?? 0;
170
-
171
- if (throttleMs > 0) {
172
- const addListener = (
173
- fn: Parameters<typeof ephemeralUserData.addListener>[0]
174
- ) => {
175
- const throttledFn = throttle(fn, throttleMs);
176
- return ephemeralUserData.addListener(throttledFn);
177
- };
178
-
179
- return addListener;
180
- }
181
-
182
- return ephemeralUserData.addListener;
183
- }, [ephemeralUserData, options?.throttle]);
184
-
185
- return useSyncExternalStore(
186
- throttledAddListener,
187
- ephemeralUserData.getSnapshot,
188
- ephemeralUserData.getSnapshot
189
- );
190
- }
191
-
192
147
  const defaultStubSync = stubSync();
193
148
 
194
149
  export type UseMultiplayerStateOptions<
195
150
  S,
196
151
  M = void,
197
- E = void,
152
+ E extends object = object,
198
153
  > = MultiplayerStateManagerOptions<S, M> & {
199
154
  sync?: (
200
155
  options: Pick<
201
156
  SyncAdapterOptions<S, M, E>,
202
- "ms" | "ephemeralUserData" | "assetManager"
157
+ "ms" | "ephemeralUserDataManager" | "assetManager"
203
158
  >
204
159
  ) => void;
205
160
  trackConnectionEvents?: boolean;
@@ -207,60 +162,71 @@ export type UseMultiplayerStateOptions<
207
162
  inspector?: boolean | StateInspectorOptions;
208
163
  };
209
164
 
210
- export function useMultiplayerState<S, M = void, E = void>(
165
+ export function useMultiplayerState<S, M = void, E extends object = object>(
211
166
  initialState: S | (() => S),
212
167
  options?: UseMultiplayerStateOptions<S, M, E>
213
168
  ) {
214
169
  const { sync = defaultStubSync as unknown as SyncAdapter<S, M, E>, ...rest } =
215
170
  options ?? {};
216
171
 
172
+ /** RPC */
173
+
174
+ const [rpcManager] = useState(
175
+ () =>
176
+ new RPCManager<StandardRPCRequestPayload, StandardRPCResponsePayload>()
177
+ );
217
178
  const [multiplayerStateManager] = useState(
218
179
  () => new MultiplayerStateManager<S, M>(initialState, rest)
219
180
  );
220
181
  const [assetManager] = useState(() => new AssetManager());
182
+ const [aiManager] = useState(() => new AIManager(rpcManager));
183
+ const [workflowManager] = useState(() => new WorkflowManager(rpcManager));
184
+ const [secretManager] = useState(() => new SecretManager(rpcManager));
185
+ const [connectedUsersManager] = useState(() => new ConnectedUsersManager());
221
186
  const syncRef = useRef(sync);
222
187
 
223
188
  const [connectionEvents, setConnectionEvents] = useState<
224
189
  ConnectionEvent<S>[] | undefined
225
190
  >();
226
-
227
- const [connectedUsers, setConnectedUsers] = useState<MultiplayerUser[]>([]);
228
- const [ephemeralUserData] = useState<EphemeralUserData<E>>(
229
- () => new EphemeralUserData()
191
+ const [ephemeralUserDataManager] = useState<EphemeralUserDataManager<E>>(
192
+ () => new EphemeralUserDataManager()
230
193
  );
231
194
  const [tasks, setTasks] = useState<Task[]>([]);
232
- const [userId, setUserId] = useState<string | undefined>();
233
195
  const assets = useObservable(assetManager.assets$);
196
+ // const connectionStatus = useObservable(
197
+ // multiplayerStateManager.connectionStatus$
198
+ // );
234
199
 
235
200
  const extras = useMemo(() => {
236
201
  return {
237
202
  tasks,
238
- userId,
239
203
  connectionEvents,
240
- connectedUsers,
241
- ephemeralUserData,
204
+ connectedUsersManager,
205
+ ephemeralUserDataManager,
242
206
  multiplayerStateManager,
243
207
  assets,
244
208
  assetManager,
209
+ aiManager,
210
+ rpcManager,
211
+ workflowManager,
212
+ secretManager,
245
213
  createAsset: assetManager.create,
246
214
  deleteAsset: assetManager.delete,
247
- // evaluate: async (code: string) => {
248
- // const payload = await rpcManager.request({ type: "eval", code });
249
-
250
- // if (payload.type !== "eval") {
251
- // throw new Error(`Unexpected rpc response type: ${payload.type}`);
252
- // }
253
- // },
215
+ // connectionStatus,
216
+ // isConnected: connectionStatus === "connected",
254
217
  };
255
218
  }, [
256
219
  tasks,
257
- userId,
258
220
  connectionEvents,
259
- connectedUsers,
260
- ephemeralUserData,
221
+ connectedUsersManager,
222
+ ephemeralUserDataManager,
261
223
  multiplayerStateManager,
262
- assetManager,
263
224
  assets,
225
+ assetManager,
226
+ aiManager,
227
+ rpcManager,
228
+ workflowManager,
229
+ secretManager,
264
230
  ]);
265
231
 
266
232
  const globalTrackEvents = useObservable(shouldTrackEvents$);
@@ -272,9 +238,6 @@ export function useMultiplayerState<S, M = void, E = void>(
272
238
  const trackEventsCallbackRef = useRef<
273
239
  ((e: ConnectionEvent<S>) => void) | undefined
274
240
  >();
275
- const trackConnectedUsersCallbackRef = useRef<
276
- ((users: MultiplayerUser[], userId: string) => void) | undefined
277
- >();
278
241
  const trackTasksCallbackRef = useRef<((tasks: Task[]) => void) | undefined>();
279
242
 
280
243
  useEffect(() => {
@@ -287,13 +250,6 @@ export function useMultiplayerState<S, M = void, E = void>(
287
250
  : undefined;
288
251
  }, [shouldTrackEvents]);
289
252
 
290
- useEffect(() => {
291
- trackConnectedUsersCallbackRef.current = (users, userId) => {
292
- setConnectedUsers(users);
293
- setUserId(userId);
294
- };
295
- }, []);
296
-
297
253
  useEffect(() => {
298
254
  trackTasksCallbackRef.current = shouldTrackTasks
299
255
  ? (tasks) => {
@@ -314,14 +270,26 @@ export function useMultiplayerState<S, M = void, E = void>(
314
270
  return syncRef.current({
315
271
  ms: multiplayerStateManager,
316
272
  assetManager,
317
- ephemeralUserData,
273
+ workflowManager,
274
+ secretManager,
275
+ rpcManager,
276
+ aiManager,
277
+ ephemeralUserDataManager,
278
+ connectedUsersManager,
318
279
  onConnectionEvent: (e) => trackEventsCallbackRef.current?.(e),
319
- onChangeConnectedUsers: (users, userId) =>
320
- trackConnectedUsersCallbackRef.current?.(users, userId),
321
280
  onChangeTasks: (tasks) => trackTasksCallbackRef.current?.(tasks),
322
281
  });
323
282
  }
324
- }, [assetManager, ephemeralUserData, multiplayerStateManager]);
283
+ }, [
284
+ assetManager,
285
+ ephemeralUserDataManager,
286
+ multiplayerStateManager,
287
+ workflowManager,
288
+ secretManager,
289
+ rpcManager,
290
+ aiManager,
291
+ connectedUsersManager,
292
+ ]);
325
293
 
326
294
  const state = useSyncMultiplayerStateManager(multiplayerStateManager);
327
295
 
@@ -365,14 +333,14 @@ export function useMultiplayerState<S, M = void, E = void>(
365
333
  });
366
334
 
367
335
  useStateInspector({
368
- ephemeralUserData,
336
+ ephemeralUserDataManager,
369
337
  multiplayerStateManager,
370
338
  assetManager,
339
+ secretManager,
371
340
  state,
372
341
  connectionEvents,
373
- connectedUsers,
342
+ connectedUsersManager,
374
343
  tasks,
375
- userId,
376
344
  forceInit,
377
345
  disabled: !options?.inspector,
378
346
  ...(typeof options?.inspector === "object" && options.inspector),
package/src/index.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export * from "@noya-app/state-manager";
2
- export * from "./avatarStyle";
3
2
  export * from "./components/UserPointersOverlay";
4
3
  export * from "./hooks";
5
4
  export * from "./inspector/StateInspector";
@@ -1,19 +1,20 @@
1
1
  "use client";
2
2
 
3
+ import { memoGeneric } from "@noya-app/react-utils";
3
4
  import {
4
5
  AssetManager,
6
+ ConnectedUsersManager,
5
7
  ConnectionEvent,
6
- EphemeralUserData,
8
+ EphemeralUserDataManager,
7
9
  ExtendedPathKey,
8
10
  MultiplayerStateManager,
9
- MultiplayerUser,
11
+ SecretManager,
10
12
  Task,
11
13
  } from "@noya-app/state-manager";
12
14
  import React, {
13
15
  CSSProperties,
14
16
  ComponentPropsWithoutRef,
15
17
  FC,
16
- memo,
17
18
  useEffect,
18
19
  useLayoutEffect,
19
20
  } from "react";
@@ -26,7 +27,7 @@ import {
26
27
  chromeLight,
27
28
  } from "react-inspector";
28
29
  import { shouldTrackEvents$, shouldTrackTasks$ } from "../globals";
29
- import { useEphemeralUserData, useManagedHistory } from "../hooks";
30
+ import { useManagedHistory } from "../hooks";
30
31
  import { useObservable } from "../useObservable";
31
32
  import { useLocalStorageState } from "./useLocalStorageState";
32
33
 
@@ -213,7 +214,7 @@ function InspectorRow({
213
214
  variant === "up"
214
215
  ? "rgba(0,255,0,0.1)"
215
216
  : variant === "down"
216
- ? "rgba(255,0,0,0.1)"
217
+ ? "transparent"
217
218
  : selected
218
219
  ? "rgb(59 130 246 / 15%)"
219
220
  : undefined,
@@ -251,30 +252,34 @@ export type StateInspectorOptions = {
251
252
  anchor?: StateInspectorAnchor;
252
253
  };
253
254
 
254
- export const StateInspector = memo(function StateInspector<S, M, E>({
255
+ export const StateInspector = memoGeneric(function StateInspector<
256
+ S,
257
+ M,
258
+ E extends object,
259
+ >({
255
260
  state,
256
261
  connectionEvents,
257
- connectedUsers,
258
262
  tasks,
259
- userId,
260
263
  unstyled,
261
264
  colorScheme = "light",
262
265
  multiplayerStateManager,
263
266
  assetManager,
264
- ephemeralUserData,
267
+ secretManager,
268
+ connectedUsersManager,
269
+ ephemeralUserDataManager,
265
270
  anchor = "right",
266
271
  forceInit,
267
272
  ...props
268
273
  }: {
269
274
  state: S;
270
275
  connectionEvents?: ConnectionEvent<S>[];
271
- connectedUsers?: MultiplayerUser[];
272
276
  tasks?: Task[];
273
- userId?: string;
274
277
  unstyled?: boolean;
275
278
  multiplayerStateManager: MultiplayerStateManager<S, M>;
276
279
  assetManager: AssetManager;
277
- ephemeralUserData: EphemeralUserData<E>;
280
+ connectedUsersManager: ConnectedUsersManager;
281
+ secretManager: SecretManager;
282
+ ephemeralUserDataManager: EphemeralUserDataManager<E>;
278
283
  forceInit: () => void;
279
284
  } & StateInspectorOptions &
280
285
  ComponentPropsWithoutRef<"div">) {
@@ -319,6 +324,10 @@ export const StateInspector = memo(function StateInspector<S, M, E>({
319
324
  "noya-multiplayer-react-show-assets",
320
325
  false
321
326
  );
327
+ const [showSecrets, setShowSecrets] = useLocalStorageState(
328
+ "noya-multiplayer-react-show-secrets",
329
+ false
330
+ );
322
331
 
323
332
  useEffect(() => {
324
333
  shouldTrackEvents$.set(showEvents);
@@ -335,9 +344,12 @@ export const StateInspector = memo(function StateInspector<S, M, E>({
335
344
  }
336
345
  }, [connectionEvents]);
337
346
 
338
- const ephemeral = useEphemeralUserData(ephemeralUserData);
347
+ const ephemeral = useObservable(ephemeralUserDataManager.data$);
339
348
  const historySnapshot = useManagedHistory(multiplayerStateManager.sm);
340
349
  const assets = useObservable(assetManager.assets$);
350
+ const secrets = useObservable(secretManager.secrets$);
351
+ const connectedUsers = useObservable(connectedUsersManager.connectedUsers$);
352
+ const userId = useObservable(connectedUsersManager.currentUserId$);
341
353
 
342
354
  useEffect(() => {
343
355
  if (historyContainerRef.current) {
@@ -766,6 +778,40 @@ export const StateInspector = memo(function StateInspector<S, M, E>({
766
778
  ))}
767
779
  </div>
768
780
  </DisclosureSection>
781
+ <DisclosureSection
782
+ title="Secrets"
783
+ colorScheme={colorScheme}
784
+ open={showSecrets}
785
+ setOpen={setShowSecrets}
786
+ right={
787
+ <SmallButton
788
+ theme={theme}
789
+ onClick={() => {
790
+ const name = prompt("Enter secret name");
791
+ if (!name) return;
792
+ const value = prompt("Enter secret value");
793
+ if (!value) return;
794
+ secretManager.createSecret(name, value);
795
+ }}
796
+ >
797
+ Create
798
+ </SmallButton>
799
+ }
800
+ >
801
+ <div style={styles.sectionInner}>
802
+ {secrets.map((secret) => (
803
+ <InspectorRow key={secret.id} colorScheme={colorScheme}>
804
+ <ObjectInspector data={secret} theme={theme} />
805
+ <SmallButton
806
+ theme={theme}
807
+ onClick={() => secretManager.deleteSecret(secret.name)}
808
+ >
809
+ Delete
810
+ </SmallButton>
811
+ </InspectorRow>
812
+ ))}
813
+ </div>
814
+ </DisclosureSection>
769
815
  <DisclosureSection
770
816
  open={showEphemeral}
771
817
  setOpen={setShowEphemeral}
@@ -2,10 +2,11 @@
2
2
 
3
3
  import {
4
4
  AssetManager,
5
+ ConnectedUsersManager,
5
6
  ConnectionEvent,
6
- EphemeralUserData,
7
+ EphemeralUserDataManager,
7
8
  MultiplayerStateManager,
8
- MultiplayerUser,
9
+ SecretManager,
9
10
  Task,
10
11
  } from "@noya-app/state-manager";
11
12
  import React, { ComponentProps, useLayoutEffect } from "react";
@@ -23,29 +24,29 @@ function createShadowRootElement() {
23
24
  return { host, container };
24
25
  }
25
26
 
26
- export function useStateInspector<S, M, E>({
27
+ export function useStateInspector<S, M, E extends object>({
27
28
  state,
28
29
  connectionEvents,
29
- connectedUsers,
30
30
  tasks,
31
- userId,
32
31
  disabled = false,
33
32
  colorScheme,
34
33
  anchor,
35
34
  multiplayerStateManager,
36
35
  assetManager,
37
- ephemeralUserData,
36
+ secretManager,
37
+ connectedUsersManager,
38
+ ephemeralUserDataManager,
38
39
  forceInit,
39
40
  }: {
40
41
  state: S;
41
42
  connectionEvents?: ConnectionEvent<S>[];
42
- connectedUsers?: MultiplayerUser[];
43
43
  tasks?: Task[];
44
- userId?: string;
45
44
  disabled: boolean;
46
45
  multiplayerStateManager: MultiplayerStateManager<S, M>;
47
46
  assetManager: AssetManager;
48
- ephemeralUserData: EphemeralUserData<E>;
47
+ secretManager: SecretManager;
48
+ connectedUsersManager: ConnectedUsersManager;
49
+ ephemeralUserDataManager: EphemeralUserDataManager<E>;
49
50
  forceInit: () => void;
50
51
  } & Pick<ComponentProps<typeof StateInspector>, "colorScheme" | "anchor">) {
51
52
  const [root, setRoot] = React.useState<Root | null>(null);
@@ -72,9 +73,7 @@ export function useStateInspector<S, M, E>({
72
73
  <StateInspector
73
74
  state={state}
74
75
  connectionEvents={connectionEvents}
75
- connectedUsers={connectedUsers}
76
76
  tasks={tasks}
77
- userId={userId}
78
77
  colorScheme={colorScheme}
79
78
  anchor={anchor}
80
79
  forceInit={forceInit}
@@ -82,21 +81,25 @@ export function useStateInspector<S, M, E>({
82
81
  multiplayerStateManager as MultiplayerStateManager<any, any>
83
82
  }
84
83
  assetManager={assetManager}
85
- ephemeralUserData={ephemeralUserData as EphemeralUserData<any>}
84
+ secretManager={secretManager}
85
+ ephemeralUserDataManager={
86
+ ephemeralUserDataManager as EphemeralUserDataManager<any>
87
+ }
88
+ connectedUsersManager={connectedUsersManager}
86
89
  />
87
90
  );
88
91
  }, [
89
92
  anchor,
90
93
  colorScheme,
91
- connectedUsers,
92
94
  connectionEvents,
93
95
  root,
94
96
  state,
95
97
  multiplayerStateManager,
96
- userId,
97
98
  tasks,
98
99
  forceInit,
99
- ephemeralUserData,
100
+ ephemeralUserDataManager,
100
101
  assetManager,
102
+ secretManager,
103
+ connectedUsersManager,
101
104
  ]);
102
105
  }
package/src/noyaApp.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /* eslint-disable no-restricted-globals */
2
2
  import {
3
+ ConnectedUsersManager,
3
4
  createOrCastValue,
4
5
  findAllDefs,
5
6
  localStorageSync,
6
- MultiplayerUser,
7
7
  Static,
8
8
  stubSync,
9
9
  SyncAdapter,
@@ -115,17 +115,25 @@ export type HostToIframeMessage = {
115
115
  };
116
116
  };
117
117
 
118
- export function useBroadcastConnectedUsers(connectedUsers: MultiplayerUser[]) {
118
+ export function useBroadcastConnectedUsers(
119
+ connectedUsersManager: ConnectedUsersManager
120
+ ) {
119
121
  useEffect(() => {
120
122
  if (!isEmbeddedApp()) return;
121
123
 
122
- const message: IframeToHostMessage = {
123
- type: "multiplayer.setUsers",
124
- payload: { users: connectedUsers },
125
- };
124
+ const unsubscribe = connectedUsersManager.connectedUsers$.subscribe(
125
+ (users) => {
126
+ const message: IframeToHostMessage = {
127
+ type: "multiplayer.setUsers",
128
+ payload: { users },
129
+ };
126
130
 
127
- parent?.postMessage(message, "*");
128
- }, [connectedUsers]);
131
+ parent?.postMessage(message, "*");
132
+ }
133
+ );
134
+
135
+ return unsubscribe;
136
+ }, [connectedUsersManager]);
129
137
  }
130
138
 
131
139
  export function useBroadcastMenuItems<T>(
@@ -163,12 +171,12 @@ export function isEmbeddedApp() {
163
171
  export type UseNoyaStateOptions<
164
172
  S,
165
173
  M = void,
166
- E = void,
174
+ E extends object = object,
167
175
  > = UseMultiplayerStateOptions<S, M, E> & {
168
176
  offlineStorageKey?: string;
169
177
  };
170
178
 
171
- export type UseNoyaStateResult<S, M = void, E = void> = [
179
+ export type UseNoyaStateResult<S, M = void, E extends object = object> = [
172
180
  ReturnType<typeof useMultiplayerState<S, M, E>>[0],
173
181
  ReturnType<typeof useMultiplayerState<S, M, E>>[1],
174
182
  ReturnType<typeof useMultiplayerState<S, M, E>>[2] & {
@@ -189,7 +197,7 @@ export type UseNoyaStateResult<S, M = void, E = void> = [
189
197
  export function useNoyaState<
190
198
  S,
191
199
  M = void,
192
- E = void,
200
+ E extends object = object,
193
201
  O extends UseNoyaStateOptions<S, M, E> = UseNoyaStateOptions<S, M, E>,
194
202
  >(
195
203
  ...args:
@@ -244,7 +252,7 @@ export function useNoyaState<
244
252
 
245
253
  const [, , extras] = result;
246
254
 
247
- useBroadcastConnectedUsers(extras.connectedUsers);
255
+ useBroadcastConnectedUsers(extras.connectedUsersManager);
248
256
 
249
257
  const mergedExtras = useMemo(() => {
250
258
  return { ...extras, theme, viewType } as const;
@@ -1,25 +1,72 @@
1
- import { GetAtPath, Observable, PathKey } from "@noya-app/observable";
1
+ import {
2
+ GetAtPath,
3
+ Observable,
4
+ ObservableOptions,
5
+ PathKey,
6
+ } from "@noya-app/observable";
2
7
  import { useMemo, useSyncExternalStore } from "react";
3
8
 
9
+ // Overloads for when observable is definitely defined
4
10
  export function useObservable<T>(observable: Observable<T>): T;
11
+ export function useObservable<T, A>(
12
+ observable: Observable<T>,
13
+ selector: (value: T) => A,
14
+ options?: Pick<ObservableOptions, "isEqual">
15
+ ): A;
5
16
  export function useObservable<T, P extends PathKey[] | string>(
6
17
  observable: Observable<T>,
7
18
  path: P
8
19
  ): GetAtPath<T, P>;
20
+
21
+ // Overloads for when observable might be undefined
22
+ export function useObservable<T>(
23
+ observable: Observable<T> | undefined
24
+ ): T | undefined;
25
+ export function useObservable<T, A>(
26
+ observable: Observable<T> | undefined,
27
+ selector: (value: T) => A,
28
+ options?: Pick<ObservableOptions, "isEqual">
29
+ ): A | undefined;
30
+ export function useObservable<T, P extends PathKey[] | string>(
31
+ observable: Observable<T> | undefined,
32
+ path: P
33
+ ): GetAtPath<T, P> | undefined;
34
+
35
+ // Implementation
9
36
  export function useObservable<T, P extends PathKey[]>(
10
- observable: Observable<T>,
11
- path?: P
37
+ observable: Observable<T> | undefined,
38
+ pathOrSelector?: P | ((value: T) => any),
39
+ options?: Pick<ObservableOptions, "isEqual">
12
40
  ) {
13
41
  const { get, listen } = useMemo(() => {
14
- const listen = path
42
+ if (!observable) {
43
+ return {
44
+ get: () => undefined,
45
+ listen: () => () => {},
46
+ };
47
+ }
48
+
49
+ if (typeof pathOrSelector === "function") {
50
+ const mappedObservable = observable.map(pathOrSelector, {
51
+ isEqual: options?.isEqual,
52
+ });
53
+ return {
54
+ get: mappedObservable.get,
55
+ listen: mappedObservable.subscribe,
56
+ };
57
+ }
58
+
59
+ const listen = pathOrSelector
15
60
  ? (callback: (value: GetAtPath<T, P>) => void) =>
16
- observable.subscribe(path, callback)
61
+ observable.subscribe(pathOrSelector, callback)
17
62
  : observable.subscribe;
18
63
 
19
- const get: () => void = path ? () => observable.get(path) : observable.get;
64
+ const get: () => void = pathOrSelector
65
+ ? () => observable.get(pathOrSelector)
66
+ : observable.get;
20
67
 
21
68
  return { get, listen };
22
- }, [observable, path]);
69
+ }, [observable, pathOrSelector, options?.isEqual]);
23
70
 
24
71
  return useSyncExternalStore(listen, get, get);
25
72
  }