@noya-app/noya-multiplayer-react 0.1.62 → 0.1.64

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-multiplayer-react",
3
- "version": "0.1.62",
3
+ "version": "0.1.64",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -12,11 +12,11 @@
12
12
  "dev": "npm run build -- --watch"
13
13
  },
14
14
  "dependencies": {
15
- "@noya-app/state-manager": "0.1.48",
15
+ "@noya-app/state-manager": "0.1.50",
16
16
  "@noya-app/emitter": "0.1.0",
17
- "@noya-app/observable": "0.1.10",
17
+ "@noya-app/observable": "0.1.12",
18
18
  "@noya-app/task-runner": "0.1.6",
19
- "@noya-app/react-utils": "0.1.23",
19
+ "@noya-app/react-utils": "0.1.25",
20
20
  "react-inspector": "6.0.2"
21
21
  },
22
22
  "peerDependencies": {
@@ -1,31 +1,47 @@
1
+ "use client";
2
+
1
3
  import { MenuItem } from "@noya-app/noya-designsystem";
2
- import { Input, OutputTransform } from "@noya-app/noya-schemas";
4
+ import {
5
+ diffResourceMaps,
6
+ Input,
7
+ MediaItem,
8
+ OutputTransform,
9
+ Resource,
10
+ ResourceCreateMap,
11
+ ResourceMap,
12
+ } from "@noya-app/noya-schemas";
3
13
  import {
4
14
  GetAtPath,
5
15
  Observable,
6
16
  ObservableOptions,
7
17
  PathKey,
8
18
  } from "@noya-app/observable";
9
- import { useStableCallback } from "@noya-app/react-utils";
19
+ import { useJsonMemo, useStableCallback } from "@noya-app/react-utils";
10
20
  import {
21
+ ActivityEventsManager,
11
22
  ConnectedUsersManager,
12
23
  CreateParams,
13
24
  EphemeralUserDataManager,
25
+ MultiplayerPatchMetadata,
14
26
  MultiplayerStateManager,
15
27
  NoyaManager,
16
28
  PublishCallback,
29
+ ResourceManager,
17
30
  SafeEval,
18
31
  StateManagerOptions,
19
32
  Static,
33
+ StreamFilter,
20
34
  TSchema,
21
35
  } from "@noya-app/state-manager";
22
36
  import React, {
23
37
  createContext,
38
+ Dispatch,
24
39
  SetStateAction,
25
40
  useCallback,
26
41
  useContext,
27
42
  useEffect,
28
43
  useMemo,
44
+ useState,
29
45
  } from "react";
30
46
  import {
31
47
  AppTheme,
@@ -204,6 +220,27 @@ export function useConnectedUsersManager() {
204
220
  return useContext(ConnectedUsersContext);
205
221
  }
206
222
 
223
+ export function useConnectedUsers() {
224
+ const connectedUsersManager = useConnectedUsersManager();
225
+ return useObservable(connectedUsersManager?.connectedUsers$);
226
+ }
227
+
228
+ export function useConnectedUser(userId?: string | null) {
229
+ const connectedUsersManager = useConnectedUsersManager();
230
+ return useObservable(
231
+ connectedUsersManager?.connectedUsers$,
232
+ useCallback(
233
+ (users) => {
234
+ return users.find((user) => {
235
+ const prefix = user.id.slice(0, 8);
236
+ return userId?.includes(prefix);
237
+ });
238
+ },
239
+ [userId]
240
+ )
241
+ );
242
+ }
243
+
207
244
  export const AnyEphemeralUserDataManagerContext = createContext<
208
245
  EphemeralUserDataManager<object> | undefined
209
246
  >(undefined);
@@ -217,6 +254,163 @@ export function useCurrentUserId() {
217
254
  return useObservable(connectedUsersManager?.currentUserId$);
218
255
  }
219
256
 
257
+ export function useIsProcessing() {
258
+ const { noyaManager } = useAnyNoyaStateContext();
259
+ return useObservable(noyaManager.isProcessing$);
260
+ }
261
+
262
+ export function useResourceManager(): ResourceManager {
263
+ const { noyaManager } = useAnyNoyaStateContext();
264
+ return noyaManager.resourceManager;
265
+ }
266
+
267
+ // export function useResources(): Resource[] {
268
+ // const { noyaManager } = useAnyNoyaStateContext();
269
+ // const resources = useObservable(noyaManager.resourceManager.resources$);
270
+ // return resources;
271
+ // }
272
+
273
+ export function resourceToMediaItem(resource: Resource): MediaItem {
274
+ switch (resource.type) {
275
+ case "asset":
276
+ return { kind: "asset", id: resource.id, assetId: resource.assetId };
277
+ case "file":
278
+ return { kind: "noyaFile", id: resource.id, fileId: resource.fileId };
279
+ default:
280
+ return { kind: "folder", id: resource.id };
281
+ }
282
+ }
283
+
284
+ export function useResources({
285
+ shouldDeleteAssets = true,
286
+ }: {
287
+ shouldDeleteAssets?: boolean;
288
+ } = {}): [ResourceMap, Dispatch<SetStateAction<ResourceCreateMap>>] {
289
+ const resourceManager = useResourceManager();
290
+ const assetManager = useAssetManager();
291
+
292
+ const resourceMap = useObservable(
293
+ resourceManager.optimisticResources$,
294
+ useCallback((resources) => {
295
+ return Object.fromEntries(
296
+ resources.map((resource) => [resource.path, resource])
297
+ );
298
+ }, [])
299
+ );
300
+
301
+ const setResourceMap = useCallback(
302
+ async (action: SetStateAction<ResourceCreateMap>) => {
303
+ const newResourceMap =
304
+ action instanceof Function ? action(resourceMap) : action;
305
+
306
+ const { addedResources, removedResources, modifiedResources } =
307
+ diffResourceMaps(resourceMap, newResourceMap, "id");
308
+
309
+ if (addedResources.length > 0) {
310
+ const payload = addedResources.map((resource) => {
311
+ switch (resource.type) {
312
+ case "directory":
313
+ return {
314
+ path: resource.path,
315
+ type: "directory" as const,
316
+ };
317
+ case "asset":
318
+ return {
319
+ path: resource.path,
320
+ type: "asset" as const,
321
+ assetId: resource.assetId,
322
+ asset: resource.asset,
323
+ };
324
+ case "file":
325
+ return {
326
+ path: resource.path,
327
+ type: "file" as const,
328
+ fileId: resource.fileId,
329
+ };
330
+ case "resource":
331
+ return {
332
+ path: resource.path,
333
+ type: "resource" as const,
334
+ fileId: resource.fileId,
335
+ resourceId: resource.resourceId,
336
+ };
337
+ default:
338
+ throw new Error(
339
+ `Unknown resource type: ${(resource as any).type}`
340
+ );
341
+ }
342
+ });
343
+
344
+ await resourceManager.createResource(payload);
345
+ }
346
+
347
+ for (const resource of removedResources) {
348
+ await resourceManager.deleteResource({ id: resource.id });
349
+
350
+ if (shouldDeleteAssets && resource.type === "asset") {
351
+ try {
352
+ await assetManager.delete(resource.assetId);
353
+ } catch (error) {
354
+ console.error("Failed to delete asset:", error);
355
+ }
356
+ }
357
+ }
358
+
359
+ await Promise.all(
360
+ modifiedResources.map(({ id, path, assetId, asset }) =>
361
+ resourceManager.updateResource({
362
+ id,
363
+ path,
364
+ assetId,
365
+ asset,
366
+ })
367
+ )
368
+ );
369
+ },
370
+ [resourceMap, resourceManager, shouldDeleteAssets, assetManager]
371
+ );
372
+
373
+ return [resourceMap, setResourceMap];
374
+ }
375
+
376
+ const emptyArray: unknown[] = [];
377
+
378
+ export function useActivityEvents(streamFilter: StreamFilter) {
379
+ const { noyaManager } = useAnyNoyaStateContext();
380
+ return useActivityEventsForManager(
381
+ streamFilter,
382
+ noyaManager.activityEventsManager
383
+ );
384
+ }
385
+
386
+ export function useActivityEventsForManager(
387
+ streamFilter: StreamFilter | undefined,
388
+ activityEventsManager: ActivityEventsManager
389
+ ) {
390
+ const [streamId, setStreamId] = useState<string | undefined>(undefined);
391
+ const stableStreamFilter = useJsonMemo(streamFilter);
392
+
393
+ useEffect(() => {
394
+ if (!stableStreamFilter) return;
395
+
396
+ const streamId = activityEventsManager.subscribe(stableStreamFilter);
397
+
398
+ setStreamId(streamId);
399
+
400
+ return () => {
401
+ setStreamId(undefined);
402
+
403
+ activityEventsManager.unsubscribe(streamId);
404
+ };
405
+ }, [activityEventsManager, stableStreamFilter]);
406
+
407
+ const observable = streamId
408
+ ? activityEventsManager.getStream(streamId)
409
+ : undefined;
410
+
411
+ return useObservable(observable) ?? emptyArray;
412
+ }
413
+
220
414
  export const FallbackUntilInitialized = ({
221
415
  children,
222
416
  fallback,
@@ -244,13 +438,16 @@ export function createNoyaContext<
244
438
  schema: Schema;
245
439
  mergeHistoryEntries?: StateManagerOptions<
246
440
  Static<Schema>,
247
- M
441
+ M & Pick<MultiplayerPatchMetadata, "name" | "timestamp">
248
442
  >["mergeHistoryEntries"];
249
443
  safeEval?: SafeEval;
250
444
  outputTransforms?: CreateParams<OutputTransform>[];
251
445
  inputs?: CreateParams<Input>[];
252
446
  }) {
253
447
  type S = Static<Schema>;
448
+ type PartialM = Partial<
449
+ M & Pick<MultiplayerPatchMetadata, "name" | "timestamp">
450
+ >;
254
451
 
255
452
  const NoyaStateContext = AnyNoyaStateContext as React.Context<
256
453
  NoyaManager<S, M, E> | undefined
@@ -261,6 +458,28 @@ export function createNoyaContext<
261
458
  EphemeralUserDataManager<E> | undefined
262
459
  >;
263
460
 
461
+ function NoyaContextProvider({
462
+ children,
463
+ contextValue,
464
+ }: {
465
+ children: React.ReactNode;
466
+ contextValue: AnyNoyaStateContextValue;
467
+ }) {
468
+ return (
469
+ <AnyNoyaStateContext.Provider value={contextValue}>
470
+ <ConnectedUsersContext.Provider
471
+ value={contextValue.noyaManager.connectedUsersManager}
472
+ >
473
+ <EphemeralUserDataManagerContext.Provider
474
+ value={contextValue.noyaManager.ephemeralUserDataManager}
475
+ >
476
+ {children}
477
+ </EphemeralUserDataManagerContext.Provider>
478
+ </ConnectedUsersContext.Provider>
479
+ </AnyNoyaStateContext.Provider>
480
+ );
481
+ }
482
+
264
483
  function NoyaStateProvider({
265
484
  children,
266
485
  initialState,
@@ -288,23 +507,15 @@ export function createNoyaContext<
288
507
  );
289
508
 
290
509
  return (
291
- <AnyNoyaStateContext.Provider value={contextValue}>
292
- <ConnectedUsersContext.Provider
293
- value={noyaManager.connectedUsersManager}
294
- >
295
- <EphemeralUserDataManagerContext.Provider
296
- value={noyaManager.ephemeralUserDataManager}
297
- >
298
- {options.fallback !== undefined ? (
299
- <FallbackUntilInitialized fallback={options.fallback}>
300
- {children}
301
- </FallbackUntilInitialized>
302
- ) : (
303
- children
304
- )}
305
- </EphemeralUserDataManagerContext.Provider>
306
- </ConnectedUsersContext.Provider>
307
- </AnyNoyaStateContext.Provider>
510
+ <NoyaContextProvider contextValue={contextValue}>
511
+ {options.fallback !== undefined ? (
512
+ <FallbackUntilInitialized fallback={options.fallback}>
513
+ {children}
514
+ </FallbackUntilInitialized>
515
+ ) : (
516
+ children
517
+ )}
518
+ </NoyaContextProvider>
308
519
  );
309
520
  }
310
521
 
@@ -339,14 +550,14 @@ export function createNoyaContext<
339
550
 
340
551
  function useSetValue(): (
341
552
  ...args:
342
- | [metadata: M, value: SetStateAction<S>]
553
+ | [metadata: PartialM, value: SetStateAction<S>]
343
554
  | [value: SetStateAction<S>]
344
555
  ) => void;
345
556
  function useSetValue<P extends PathKey[] | string>(
346
557
  path: P
347
558
  ): (
348
559
  ...args:
349
- | [metadata: M, value: SetStateAction<GetAtPath<S, P>>]
560
+ | [metadata: PartialM, value: SetStateAction<GetAtPath<S, P>>]
350
561
  | [value: SetStateAction<GetAtPath<S, P>>]
351
562
  ) => void;
352
563
  function useSetValue<P extends PathKey[] | string>(path?: P) {
@@ -355,7 +566,7 @@ export function createNoyaContext<
355
566
  const setValue = useCallback(
356
567
  (
357
568
  ...args:
358
- | [metadata: M, value: SetStateAction<GetAtPath<S, P>>]
569
+ | [metadata: PartialM, value: SetStateAction<GetAtPath<S, P>>]
359
570
  | [value: SetStateAction<GetAtPath<S, P>>]
360
571
  ): void => {
361
572
  const [metadata, value] = args.length === 2 ? args : [{}, args[0]];
@@ -376,7 +587,7 @@ export function createNoyaContext<
376
587
  S,
377
588
  (
378
589
  ...args:
379
- | [metadata: M, value: SetStateAction<S>]
590
+ | [metadata: PartialM, value: SetStateAction<S>]
380
591
  | [value: SetStateAction<S>]
381
592
  ) => void,
382
593
  ];
@@ -387,7 +598,7 @@ export function createNoyaContext<
387
598
  A,
388
599
  (
389
600
  ...args:
390
- | [metadata: M, value: SetStateAction<S>]
601
+ | [metadata: PartialM, value: SetStateAction<S>]
391
602
  | [value: SetStateAction<S>]
392
603
  ) => void,
393
604
  ];
@@ -397,7 +608,7 @@ export function createNoyaContext<
397
608
  GetAtPath<S, P>,
398
609
  (
399
610
  ...args:
400
- | [metadata: M, value: SetStateAction<GetAtPath<S, P>>]
611
+ | [metadata: PartialM, value: SetStateAction<GetAtPath<S, P>>]
401
612
  | [value: SetStateAction<GetAtPath<S, P>>]
402
613
  ) => void,
403
614
  ];
@@ -417,7 +628,10 @@ export function createNoyaContext<
417
628
  function useStateManager() {
418
629
  const { noyaManager } = useAnyNoyaStateContext();
419
630
 
420
- return noyaManager.multiplayerStateManager as MultiplayerStateManager<S, M>;
631
+ return noyaManager.multiplayerStateManager as MultiplayerStateManager<
632
+ S,
633
+ PartialM
634
+ >;
421
635
  }
422
636
 
423
637
  function useEphemeralUserDataManager() {
@@ -433,7 +647,12 @@ export function createNoyaContext<
433
647
  }
434
648
 
435
649
  function useNoyaManager() {
436
- return useAnyNoyaStateContext().noyaManager as NoyaManager<S, M, E, MenuT>;
650
+ return useAnyNoyaStateContext().noyaManager as NoyaManager<
651
+ S,
652
+ PartialM,
653
+ E,
654
+ MenuT
655
+ >;
437
656
  }
438
657
 
439
658
  function useLeftMenuItems() {
@@ -499,6 +718,7 @@ export function createNoyaContext<
499
718
  return {
500
719
  Context: NoyaStateContext,
501
720
  Provider: NoyaStateProvider,
721
+ NoyaContextProvider,
502
722
  useValue,
503
723
  useSetValue,
504
724
  useValueState,
@@ -0,0 +1,126 @@
1
+ import { Asset } from "@noya-app/noya-schemas";
2
+ import { Base64 } from "@noya-app/noya-utils";
3
+ import { NoyaManager, Static, Type } from "@noya-app/state-manager";
4
+ import { expect, it } from "bun:test";
5
+ import {
6
+ decodeAll,
7
+ encodeAll,
8
+ exportAll,
9
+ importAll,
10
+ } from "../inspector/serialization";
11
+
12
+ const asset1Data = new Uint8Array([0x00, 0x01, 0x02, 0x03]);
13
+ const asset1Id = "2717f281-307d-4e9e-aa3b-68d29d4a1723";
14
+ const asset1DataUrl = `data:application/octet-stream;base64,${Base64.encode(
15
+ asset1Data
16
+ )}`;
17
+ const asset1 = {
18
+ id: asset1Id,
19
+ url: asset1DataUrl,
20
+ createdAt: new Date().toISOString(),
21
+ size: asset1Data.byteLength,
22
+ };
23
+ const assets: Asset[] = [asset1];
24
+ const assetMap = {
25
+ [asset1Id]: asset1Data,
26
+ };
27
+
28
+ it("should serialize and deserialize the state", () => {
29
+ const schema = Type.Object({
30
+ name: Type.String(),
31
+ age: Type.Number(),
32
+ });
33
+
34
+ const initialState: Static<typeof schema> = {
35
+ name: "John",
36
+ age: 30,
37
+ };
38
+
39
+ const buffer = encodeAll({
40
+ state: initialState,
41
+ schema,
42
+ assets,
43
+ assetMap,
44
+ });
45
+
46
+ const decoded = decodeAll(buffer);
47
+
48
+ expect(decoded.state).toEqual(initialState);
49
+ expect(decoded.schema).toEqual(schema);
50
+ expect(decoded.assets).toEqual(assets);
51
+ expect(decoded.assetMap).toEqual(assetMap);
52
+ });
53
+
54
+ it("should import and export the noya manager state", async () => {
55
+ const schema = Type.Object({
56
+ name: Type.String(),
57
+ age: Type.Number(),
58
+ assetRef: Type.String(),
59
+ });
60
+
61
+ const initialState: Static<typeof schema> = {
62
+ name: "John",
63
+ age: 30,
64
+ assetRef: asset1Id,
65
+ };
66
+
67
+ const noyaManager = new NoyaManager(initialState, {
68
+ schema,
69
+ initialAssets: assets,
70
+ });
71
+
72
+ const buffer = await exportAll(noyaManager);
73
+
74
+ const stateToReplace = {
75
+ name: "Jane",
76
+ age: 20,
77
+ assetRef: "not-found",
78
+ };
79
+
80
+ const schemaToReplace = Type.Object({
81
+ name: Type.String(),
82
+ age: Type.Number(),
83
+ isAdmin: Type.Boolean(),
84
+ });
85
+
86
+ const newNoyaManager = new NoyaManager(stateToReplace, {
87
+ schema: schemaToReplace,
88
+ mode: "standalone",
89
+ });
90
+
91
+ let didCall = false;
92
+
93
+ await importAll(buffer, newNoyaManager, {
94
+ shouldAllowSchemaChange: () => {
95
+ didCall = true;
96
+ return true;
97
+ },
98
+ });
99
+
100
+ expect(didCall).toBe(true);
101
+
102
+ const importedAssets = newNoyaManager.assetManager.assets$.get();
103
+ const firstAsset = importedAssets.at(0);
104
+
105
+ if (!firstAsset) {
106
+ throw new Error("First asset not found");
107
+ }
108
+
109
+ expect(newNoyaManager.multiplayerStateManager.optimisticState$.get()).toEqual(
110
+ {
111
+ ...initialState,
112
+ assetRef: firstAsset.id,
113
+ }
114
+ );
115
+
116
+ expect(firstAsset).toMatchObject({
117
+ size: asset1.size,
118
+ });
119
+
120
+ // actualFetch to handle blob: urls
121
+ const firstAssetData = await (globalThis as any)
122
+ .actualFetch(firstAsset.url)
123
+ .then((res: any) => res.arrayBuffer());
124
+
125
+ expect(new Uint8Array(firstAssetData)).toEqual(asset1Data);
126
+ });
package/src/ai.ts CHANGED
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { AIToolDefinition } from "@noya-app/state-manager";
2
4
  import { useEffect } from "react";
3
5
  import { useAIManager } from "./NoyaStateContext";
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export * from "@noya-app/state-manager";
2
2
  export * from "./ai";
3
3
  export * from "./components/UserPointersOverlay";
4
4
  export * from "./hooks";
5
+ export * from "./inspector/serialization";
5
6
  export * from "./inspector/StateInspector";
6
7
  export * from "./inspector/useStateInspector";
7
8
  export * from "./noyaApp";
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+
3
+ export function ColoredDot({ type }: { type: "success" | "error" }) {
4
+ return (
5
+ <span
6
+ style={{
7
+ display: "inline-block",
8
+ width: 6,
9
+ height: 6,
10
+ background:
11
+ type === "success" ? "rgba(0,200,0,0.8)" : "rgba(200,0,0,0.8)",
12
+ borderRadius: "50%",
13
+ verticalAlign: "middle",
14
+ }}
15
+ />
16
+ );
17
+ }
@@ -0,0 +1,48 @@
1
+ import React, { FC } from "react";
2
+ import { ObjectName, ObjectPreview } from "react-inspector";
3
+
4
+ type MessageDirection = "up" | "down";
5
+
6
+ export const ObjectRootLabel: FC<{
7
+ name?: string;
8
+ data: any;
9
+ direction?: MessageDirection;
10
+ }> = ({ name, data, direction }) => {
11
+ if (typeof name === "string") {
12
+ return (
13
+ <span>
14
+ <ObjectName name={name} />
15
+ <span>: </span>
16
+ <ObjectPreview data={data} />
17
+ </span>
18
+ );
19
+ }
20
+ if (direction === "up" || direction === "down") {
21
+ const arrow = direction === "up" ? "↑" : "↓";
22
+
23
+ return (
24
+ <span>
25
+ <span
26
+ style={{
27
+ background:
28
+ direction === "up" ? "rgba(0,255,0,0.2)" : "rgba(255,0,0,0.2)",
29
+ // color: "white",
30
+ width: 12,
31
+ height: 12,
32
+ borderRadius: 2,
33
+ display: "inline-flex",
34
+ justifyContent: "center",
35
+ alignItems: "center",
36
+ marginRight: 4,
37
+ lineHeight: "12px",
38
+ }}
39
+ >
40
+ {arrow}
41
+ </span>
42
+ <ObjectPreview data={data} />
43
+ </span>
44
+ );
45
+ } else {
46
+ return <ObjectPreview data={data} />;
47
+ }
48
+ };