@noya-app/noya-multiplayer-react 0.1.63 → 0.1.65

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.63",
3
+ "version": "0.1.65",
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.49",
15
+ "@noya-app/state-manager": "0.1.51",
16
16
  "@noya-app/emitter": "0.1.0",
17
- "@noya-app/observable": "0.1.11",
17
+ "@noya-app/observable": "0.1.12",
18
18
  "@noya-app/task-runner": "0.1.6",
19
- "@noya-app/react-utils": "0.1.24",
19
+ "@noya-app/react-utils": "0.1.26",
20
20
  "react-inspector": "6.0.2"
21
21
  },
22
22
  "peerDependencies": {
@@ -1,15 +1,24 @@
1
1
  "use client";
2
2
 
3
3
  import { MenuItem } from "@noya-app/noya-designsystem";
4
- 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";
5
13
  import {
6
14
  GetAtPath,
7
15
  Observable,
8
16
  ObservableOptions,
9
17
  PathKey,
10
18
  } from "@noya-app/observable";
11
- import { useStableCallback } from "@noya-app/react-utils";
19
+ import { useJsonMemo, useStableCallback } from "@noya-app/react-utils";
12
20
  import {
21
+ ActivityEventsManager,
13
22
  ConnectedUsersManager,
14
23
  CreateParams,
15
24
  EphemeralUserDataManager,
@@ -17,18 +26,22 @@ import {
17
26
  MultiplayerStateManager,
18
27
  NoyaManager,
19
28
  PublishCallback,
29
+ ResourceManager,
20
30
  SafeEval,
21
31
  StateManagerOptions,
22
32
  Static,
33
+ StreamFilter,
23
34
  TSchema,
24
35
  } from "@noya-app/state-manager";
25
36
  import React, {
26
37
  createContext,
38
+ Dispatch,
27
39
  SetStateAction,
28
40
  useCallback,
29
41
  useContext,
30
42
  useEffect,
31
43
  useMemo,
44
+ useState,
32
45
  } from "react";
33
46
  import {
34
47
  AppTheme,
@@ -207,6 +220,27 @@ export function useConnectedUsersManager() {
207
220
  return useContext(ConnectedUsersContext);
208
221
  }
209
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
+
210
244
  export const AnyEphemeralUserDataManagerContext = createContext<
211
245
  EphemeralUserDataManager<object> | undefined
212
246
  >(undefined);
@@ -220,6 +254,163 @@ export function useCurrentUserId() {
220
254
  return useObservable(connectedUsersManager?.currentUserId$);
221
255
  }
222
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
+
223
414
  export const FallbackUntilInitialized = ({
224
415
  children,
225
416
  fallback,
@@ -267,6 +458,28 @@ export function createNoyaContext<
267
458
  EphemeralUserDataManager<E> | undefined
268
459
  >;
269
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
+
270
483
  function NoyaStateProvider({
271
484
  children,
272
485
  initialState,
@@ -294,23 +507,15 @@ export function createNoyaContext<
294
507
  );
295
508
 
296
509
  return (
297
- <AnyNoyaStateContext.Provider value={contextValue}>
298
- <ConnectedUsersContext.Provider
299
- value={noyaManager.connectedUsersManager}
300
- >
301
- <EphemeralUserDataManagerContext.Provider
302
- value={noyaManager.ephemeralUserDataManager}
303
- >
304
- {options.fallback !== undefined ? (
305
- <FallbackUntilInitialized fallback={options.fallback}>
306
- {children}
307
- </FallbackUntilInitialized>
308
- ) : (
309
- children
310
- )}
311
- </EphemeralUserDataManagerContext.Provider>
312
- </ConnectedUsersContext.Provider>
313
- </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>
314
519
  );
315
520
  }
316
521
 
@@ -513,6 +718,7 @@ export function createNoyaContext<
513
718
  return {
514
719
  Context: NoyaStateContext,
515
720
  Provider: NoyaStateProvider,
721
+ NoyaContextProvider,
516
722
  useValue,
517
723
  useSetValue,
518
724
  useValueState,
@@ -1,6 +1,8 @@
1
+ /* eslint-disable no-restricted-globals */
1
2
  "use client";
2
3
 
3
4
  import { Input, OutputTransform } from "@noya-app/noya-schemas";
5
+ import { Base64, uuid } from "@noya-app/noya-utils";
4
6
  import { downloadBlob, memoGeneric } from "@noya-app/react-utils";
5
7
  import { NoyaManager } from "@noya-app/state-manager";
6
8
  import React, {
@@ -18,6 +20,7 @@ import {
18
20
  getStateInspectorBorderColor,
19
21
  getStateInspectorTheme,
20
22
  } from "./inspectorTheme";
23
+ import { ActivityEventsSection } from "./sections/ActivityEventsSection";
21
24
  import { EventsSection } from "./sections/EventsSection";
22
25
  import { HistorySection } from "./sections/HistorySection";
23
26
  import { exportAll, importAll } from "./serialization";
@@ -27,6 +30,7 @@ import {
27
30
  StateInspectorDisclosureSection,
28
31
  } from "./StateInspectorDisclosureSection";
29
32
  import { StateInspectorRow } from "./StateInspectorRow";
33
+ import { StateInspectorTitleLabel } from "./StateInspectorTitleLabel";
30
34
  import { StateInspectorToggleButton } from "./StateInspectorToggleButton";
31
35
  import { useLocalStorageState } from "./useLocalStorageState";
32
36
  import { ellipsis, uploadFile } from "./utils";
@@ -67,6 +71,8 @@ export const StateInspector = memoGeneric(function StateInspector<
67
71
  connectionEventManager,
68
72
  taskManager,
69
73
  ioManager,
74
+ resourceManager,
75
+ activityEventsManager,
70
76
  } = noyaManager;
71
77
 
72
78
  const [didMount, setDidMount] = React.useState(false);
@@ -79,6 +85,7 @@ export const StateInspector = memoGeneric(function StateInspector<
79
85
  }, []);
80
86
 
81
87
  const eventsContainerRef = React.useRef<HTMLDivElement>(null);
88
+ const activityEventsContainerRef = React.useRef<HTMLDivElement>(null);
82
89
 
83
90
  const [showInspector, setShowInspector] = useLocalStorageState(
84
91
  "noya-multiplayer-react-show-inspector",
@@ -112,6 +119,10 @@ export const StateInspector = memoGeneric(function StateInspector<
112
119
  "noya-multiplayer-react-show-assets",
113
120
  false
114
121
  );
122
+ const [showResources, setShowResources] = useLocalStorageState(
123
+ "noya-multiplayer-react-show-resources",
124
+ false
125
+ );
115
126
  const [showSecrets, setShowSecrets] = useLocalStorageState(
116
127
  "noya-multiplayer-react-show-secrets",
117
128
  false
@@ -124,9 +135,12 @@ export const StateInspector = memoGeneric(function StateInspector<
124
135
  "noya-multiplayer-react-show-output-transforms",
125
136
  false
126
137
  );
138
+ const [showActivityEvents, setShowActivityEvents] = useLocalStorageState(
139
+ "noya-multiplayer-react-show-activity-events",
140
+ false
141
+ );
127
142
 
128
143
  const connectionEvents = useObservable(connectionEventManager.events$);
129
-
130
144
  useEffect(() => {
131
145
  if (eventsContainerRef.current) {
132
146
  eventsContainerRef.current.scrollTop =
@@ -140,6 +154,8 @@ export const StateInspector = memoGeneric(function StateInspector<
140
154
  const ephemeral = useObservable(ephemeralUserDataManager.data$);
141
155
  const historySnapshot = useManagedHistory(multiplayerStateManager.sm);
142
156
  const assets = useObservable(assetManager.assets$);
157
+ const resources = useObservable(resourceManager.resources$);
158
+ const resourcesInitialized = useObservable(resourceManager.isInitialized$);
143
159
  const assetsInitialized = useObservable(assetManager.isInitialized$);
144
160
  const secrets = useObservable(secretManager.secrets$);
145
161
  const secretsInitialized = useObservable(secretManager.isInitialized$);
@@ -150,7 +166,6 @@ export const StateInspector = memoGeneric(function StateInspector<
150
166
  const outputTransformsInitialized = useObservable(
151
167
  ioManager.outputTransformsInitialized$
152
168
  );
153
-
154
169
  const theme = getStateInspectorTheme(colorScheme);
155
170
  const solidBorderColor = getStateInspectorBorderColor(colorScheme);
156
171
 
@@ -290,12 +305,12 @@ export const StateInspector = memoGeneric(function StateInspector<
290
305
  </StateInspectorDisclosureSection>
291
306
  <StateInspectorDisclosureSection
292
307
  title={
293
- <TitleLabel>
308
+ <StateInspectorTitleLabel>
294
309
  Data
295
310
  <ColoredDot
296
311
  type={multipeerStateInitialized ? "success" : "error"}
297
312
  />
298
- </TitleLabel>
313
+ </StateInspectorTitleLabel>
299
314
  }
300
315
  colorScheme={colorScheme}
301
316
  setOpen={setShowData}
@@ -355,14 +370,28 @@ export const StateInspector = memoGeneric(function StateInspector<
355
370
  open={showAssets}
356
371
  setOpen={setShowAssets}
357
372
  title={
358
- <TitleLabel>
373
+ <StateInspectorTitleLabel>
359
374
  Assets ({assets.length})
360
375
  <ColoredDot type={assetsInitialized ? "success" : "error"} />
361
- </TitleLabel>
376
+ </StateInspectorTitleLabel>
362
377
  }
363
378
  colorScheme={colorScheme}
364
379
  right={
365
380
  <span style={{ display: "flex", gap: "10px" }}>
381
+ <StateInspectorButton
382
+ theme={theme}
383
+ onClick={async () => {
384
+ const ok = confirm(
385
+ "Are you sure you want to delete all assets?"
386
+ );
387
+ if (!ok) return;
388
+ await Promise.all(
389
+ assets.map((asset) => assetManager.delete(asset.id))
390
+ );
391
+ }}
392
+ >
393
+ Delete all
394
+ </StateInspectorButton>
366
395
  <StateInspectorButton
367
396
  theme={theme}
368
397
  onClick={() => {
@@ -416,10 +445,103 @@ export const StateInspector = memoGeneric(function StateInspector<
416
445
  </StateInspectorDisclosureSection>
417
446
  <StateInspectorDisclosureSection
418
447
  title={
419
- <TitleLabel>
448
+ <StateInspectorTitleLabel>
449
+ Resources ({resources.length})
450
+ <ColoredDot type={resourcesInitialized ? "success" : "error"} />
451
+ </StateInspectorTitleLabel>
452
+ }
453
+ colorScheme={colorScheme}
454
+ open={showResources}
455
+ setOpen={setShowResources}
456
+ right={
457
+ <span style={{ display: "flex", gap: "10px" }}>
458
+ <StateInspectorButton
459
+ theme={theme}
460
+ onClick={async () => {
461
+ const ok = confirm(
462
+ "Are you sure you want to delete all resources?"
463
+ );
464
+ if (!ok) return;
465
+ await Promise.all(
466
+ resources.map((resource) =>
467
+ resourceManager.deleteResource({ id: resource.id })
468
+ )
469
+ );
470
+ }}
471
+ >
472
+ Delete all
473
+ </StateInspectorButton>
474
+ <StateInspectorButton
475
+ theme={theme}
476
+ onClick={async () => {
477
+ const path = prompt("Enter directory path") || uuid();
478
+ await resourceManager.createResource({
479
+ type: "directory",
480
+ path,
481
+ });
482
+ }}
483
+ >
484
+ Create Directory
485
+ </StateInspectorButton>
486
+ <StateInspectorButton
487
+ theme={theme}
488
+ onClick={async () => {
489
+ const file = await uploadFile();
490
+
491
+ resourceManager.createResource({
492
+ type: "asset",
493
+ path: file.name || uuid(),
494
+ asset: {
495
+ content: Base64.encode(await file.arrayBuffer()),
496
+ contentType: file.type,
497
+ encoding: "base64",
498
+ },
499
+ });
500
+ }}
501
+ >
502
+ Create Asset
503
+ </StateInspectorButton>
504
+ </span>
505
+ }
506
+ >
507
+ <StateInspectorDisclosureRowInner>
508
+ {resources?.map((resource) => (
509
+ <StateInspectorRow key={resource.id} colorScheme={colorScheme}>
510
+ <ObjectInspector
511
+ name={resource.path}
512
+ data={resource}
513
+ theme={theme}
514
+ />
515
+ <div style={{ display: "flex", gap: "4px" }}>
516
+ <StateInspectorButton
517
+ theme={theme}
518
+ onClick={() =>
519
+ resourceManager.deleteResource({ id: resource.id })
520
+ }
521
+ >
522
+ Delete
523
+ </StateInspectorButton>
524
+ <StateInspectorButton
525
+ theme={theme}
526
+ onClick={() => {
527
+ const path = prompt("Enter new path", resource.path);
528
+ if (!path) return;
529
+ resourceManager.updateResource({ id: resource.id, path });
530
+ }}
531
+ >
532
+ Rename
533
+ </StateInspectorButton>
534
+ </div>
535
+ </StateInspectorRow>
536
+ ))}
537
+ </StateInspectorDisclosureRowInner>
538
+ </StateInspectorDisclosureSection>
539
+ <StateInspectorDisclosureSection
540
+ title={
541
+ <StateInspectorTitleLabel>
420
542
  Secrets
421
543
  <ColoredDot type={secretsInitialized ? "success" : "error"} />
422
- </TitleLabel>
544
+ </StateInspectorTitleLabel>
423
545
  }
424
546
  colorScheme={colorScheme}
425
547
  open={showSecrets}
@@ -457,10 +579,10 @@ export const StateInspector = memoGeneric(function StateInspector<
457
579
  open={showInputs}
458
580
  setOpen={setShowInputs}
459
581
  title={
460
- <TitleLabel>
582
+ <StateInspectorTitleLabel>
461
583
  Inputs
462
584
  <ColoredDot type={inputsInitialized ? "success" : "error"} />
463
- </TitleLabel>
585
+ </StateInspectorTitleLabel>
464
586
  }
465
587
  colorScheme={colorScheme}
466
588
  >
@@ -489,12 +611,12 @@ export const StateInspector = memoGeneric(function StateInspector<
489
611
  open={showOutputTransforms}
490
612
  setOpen={setShowOutputTransforms}
491
613
  title={
492
- <TitleLabel>
614
+ <StateInspectorTitleLabel>
493
615
  Output Transforms
494
616
  <ColoredDot
495
617
  type={outputTransformsInitialized ? "success" : "error"}
496
618
  />
497
- </TitleLabel>
619
+ </StateInspectorTitleLabel>
498
620
  }
499
621
  colorScheme={colorScheme}
500
622
  >
@@ -567,6 +689,13 @@ export const StateInspector = memoGeneric(function StateInspector<
567
689
  ))}
568
690
  </StateInspectorDisclosureRowInner>
569
691
  </StateInspectorDisclosureSection>
692
+ <ActivityEventsSection
693
+ colorScheme={colorScheme}
694
+ activityEventsContainerRef={activityEventsContainerRef}
695
+ showEvents={showActivityEvents}
696
+ setShowEvents={setShowActivityEvents}
697
+ activityEventsManager={activityEventsManager}
698
+ />
570
699
  <EventsSection<S>
571
700
  connectionEvents={connectionEvents}
572
701
  colorScheme={colorScheme}
@@ -577,11 +706,3 @@ export const StateInspector = memoGeneric(function StateInspector<
577
706
  </div>
578
707
  );
579
708
  });
580
-
581
- function TitleLabel({ children }: { children: React.ReactNode }) {
582
- return (
583
- <span style={{ display: "flex", alignItems: "center", gap: "4px" }}>
584
- {children}
585
- </span>
586
- );
587
- }
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+
3
+ export function StateInspectorTitleLabel({
4
+ children,
5
+ }: {
6
+ children: React.ReactNode;
7
+ }) {
8
+ return (
9
+ <span style={{ display: "flex", alignItems: "center", gap: "4px" }}>
10
+ {children}
11
+ </span>
12
+ );
13
+ }