@noya-app/noya-multiplayer-react 0.1.43 → 0.1.45
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 +11 -11
- package/CHANGELOG.md +17 -0
- package/dist/index.bundle.js +40 -7
- package/dist/index.d.mts +70 -60
- package/dist/index.d.ts +70 -60
- package/dist/index.js +306 -271
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +313 -278
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
- package/src/NoyaStateContext.tsx +163 -13
- package/src/components/UserPointersOverlay.tsx +56 -129
- package/src/hooks.ts +60 -92
- package/src/index.ts +0 -1
- package/src/inspector/StateInspector.tsx +59 -13
- package/src/inspector/useStateInspector.tsx +18 -15
- package/src/noyaApp.ts +20 -12
- package/src/useObservable.ts +54 -7
- package/src/avatarStyle.ts +0 -82
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
|
-
|
|
10
|
+
EphemeralUserDataManager,
|
|
9
11
|
MultiplayerStateManager,
|
|
10
12
|
MultiplayerStateManagerError,
|
|
11
13
|
MultiplayerStateManagerOptions,
|
|
12
|
-
|
|
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 =
|
|
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" | "
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
//
|
|
248
|
-
//
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
336
|
+
ephemeralUserDataManager,
|
|
369
337
|
multiplayerStateManager,
|
|
370
338
|
assetManager,
|
|
339
|
+
secretManager,
|
|
371
340
|
state,
|
|
372
341
|
connectionEvents,
|
|
373
|
-
|
|
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,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
|
-
|
|
8
|
+
EphemeralUserDataManager,
|
|
7
9
|
ExtendedPathKey,
|
|
8
10
|
MultiplayerStateManager,
|
|
9
|
-
|
|
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 {
|
|
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
|
-
? "
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
7
|
+
EphemeralUserDataManager,
|
|
7
8
|
MultiplayerStateManager,
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
118
|
+
export function useBroadcastConnectedUsers(
|
|
119
|
+
connectedUsersManager: ConnectedUsersManager
|
|
120
|
+
) {
|
|
119
121
|
useEffect(() => {
|
|
120
122
|
if (!isEmbeddedApp()) return;
|
|
121
123
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
const unsubscribe = connectedUsersManager.connectedUsers$.subscribe(
|
|
125
|
+
(users) => {
|
|
126
|
+
const message: IframeToHostMessage = {
|
|
127
|
+
type: "multiplayer.setUsers",
|
|
128
|
+
payload: { users },
|
|
129
|
+
};
|
|
126
130
|
|
|
127
|
-
|
|
128
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
255
|
+
useBroadcastConnectedUsers(extras.connectedUsersManager);
|
|
248
256
|
|
|
249
257
|
const mergedExtras = useMemo(() => {
|
|
250
258
|
return { ...extras, theme, viewType } as const;
|
package/src/useObservable.ts
CHANGED
|
@@ -1,25 +1,72 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
61
|
+
observable.subscribe(pathOrSelector, callback)
|
|
17
62
|
: observable.subscribe;
|
|
18
63
|
|
|
19
|
-
const get: () => void =
|
|
64
|
+
const get: () => void = pathOrSelector
|
|
65
|
+
? () => observable.get(pathOrSelector)
|
|
66
|
+
: observable.get;
|
|
20
67
|
|
|
21
68
|
return { get, listen };
|
|
22
|
-
}, [observable,
|
|
69
|
+
}, [observable, pathOrSelector, options?.isEqual]);
|
|
23
70
|
|
|
24
71
|
return useSyncExternalStore(listen, get, get);
|
|
25
72
|
}
|