@noya-app/noya-multiplayer-react 0.1.70 → 0.1.72

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.70",
3
+ "version": "0.1.72",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -12,7 +12,7 @@
12
12
  "dev": "npm run build -- --watch"
13
13
  },
14
14
  "dependencies": {
15
- "@noya-app/state-manager": "0.1.56",
15
+ "@noya-app/state-manager": "0.1.58",
16
16
  "@noya-app/emitter": "0.1.0",
17
17
  "@noya-app/observable": "0.1.12",
18
18
  "@noya-app/task-runner": "0.1.6",
@@ -3,6 +3,7 @@
3
3
  import { MenuItem } from "@noya-app/noya-designsystem";
4
4
  import {
5
5
  CreateResourceParametersWithoutAccessibleByFileId,
6
+ DeleteResourceParameters,
6
7
  diffResourceMaps,
7
8
  Input,
8
9
  MediaItem,
@@ -22,17 +23,20 @@ import {
22
23
  import { useJsonMemo, useStableCallback } from "@noya-app/react-utils";
23
24
  import {
24
25
  ActivityEventsManager,
26
+ AssetTranscriptionResult,
25
27
  CreateParams,
26
- EphemeralUserDataManager,
28
+ ExtractRequestBody,
27
29
  MultiplayerPatchMetadata,
28
30
  MultiplayerStateManager,
29
31
  NoyaManager,
30
32
  PublishCallback,
31
33
  ResourceManager,
32
34
  SafeEval,
35
+ SharedConnectionDataManager,
33
36
  StateManagerOptions,
34
37
  Static,
35
38
  StreamFilter,
39
+ TranscriptionManager,
36
40
  TSchema,
37
41
  } from "@noya-app/state-manager";
38
42
  import React, {
@@ -88,70 +92,56 @@ export function resourceToCreateParameters(
88
92
  }
89
93
  }
90
94
 
91
- type BuildCreateResourcePayloadParameters = {
95
+ type ResourcePatchRequestBody = ExtractRequestBody<"PATCH /api/resources">;
96
+
97
+ type BuildResourcePatchPayloadParameters = {
92
98
  addedResources: ResourceCreate[];
93
99
  modifiedResources: ModifiedResource[];
94
- newResourceMap: ResourceCreateMap;
95
- resourceMap: ResourceMap;
96
- };
97
-
98
- type BuildCreateResourcePayloadResult = {
99
- payload: CreateResourceParametersWithoutAccessibleByFileId[];
100
- consumedModifiedResourceIds: string[];
100
+ removedResources: Resource[];
101
101
  };
102
102
 
103
- export function buildCreateResourcePayload({
103
+ export function buildResourcePatchPayload({
104
104
  addedResources,
105
105
  modifiedResources,
106
- newResourceMap,
107
- resourceMap,
108
- }: BuildCreateResourcePayloadParameters): BuildCreateResourcePayloadResult {
109
- const toCreateParameters = resourceToCreateParameters;
106
+ removedResources,
107
+ }: BuildResourcePatchPayloadParameters): ResourcePatchRequestBody {
108
+ const createEntries = addedResources.map(resourceToCreateParameters);
110
109
 
111
- const resourcesById = new Map(
112
- Object.values(resourceMap).map((resource) => [resource.id, resource])
110
+ const dedupedCreateEntries = Array.from(
111
+ new Map(createEntries.map((entry) => [entry.path, entry])).values()
113
112
  );
114
113
 
115
- const payload: CreateResourceParametersWithoutAccessibleByFileId[] = [
116
- ...addedResources.map(toCreateParameters),
117
- ];
118
-
119
- const handledModifiedResourceIds: string[] = [];
120
-
121
- const assetReplacements = modifiedResources
122
- .filter(
123
- (resource) =>
124
- resource.assetId !== undefined || resource.asset !== undefined
125
- )
126
- .map((resource) => {
127
- const previousResource = resourcesById.get(resource.id);
128
- const path = resource.path ?? previousResource?.path;
129
-
130
- if (!path) return undefined;
131
-
132
- const updated = newResourceMap[path];
114
+ const updateEntries =
115
+ modifiedResources.map((resource) => {
116
+ const update: NonNullable<ResourcePatchRequestBody["update"]>[number] = {
117
+ id: resource.id,
118
+ ...(resource.path !== undefined ? { path: resource.path } : {}),
119
+ ...(resource.assetId !== undefined
120
+ ? { assetId: resource.assetId }
121
+ : {}),
122
+ ...(resource.asset ? { asset: resource.asset } : {}),
123
+ };
133
124
 
134
- if (!updated || updated.type !== "asset") return undefined;
125
+ return update;
126
+ }) ?? [];
135
127
 
136
- handledModifiedResourceIds.push(resource.id);
128
+ const deleteEntries: DeleteResourceParameters[] = removedResources.map(
129
+ (resource) => ({ id: resource.id })
130
+ );
137
131
 
138
- return toCreateParameters(updated);
139
- })
140
- .filter(
141
- (
142
- resource
143
- ): resource is CreateResourceParametersWithoutAccessibleByFileId =>
144
- resource !== undefined
145
- );
132
+ const patch: ResourcePatchRequestBody = {};
146
133
 
147
- const deduped = new Map(
148
- [...payload, ...assetReplacements].map((item) => [item.path, item])
149
- );
134
+ if (dedupedCreateEntries.length) {
135
+ patch.create = dedupedCreateEntries;
136
+ }
137
+ if (updateEntries.length) {
138
+ patch.update = updateEntries;
139
+ }
140
+ if (deleteEntries.length) {
141
+ patch.delete = deleteEntries;
142
+ }
150
143
 
151
- return {
152
- payload: Array.from(deduped.values()),
153
- consumedModifiedResourceIds: [...new Set(handledModifiedResourceIds)],
154
- };
144
+ return patch;
155
145
  }
156
146
 
157
147
  export interface NoyaStateProviderProps<
@@ -159,8 +149,9 @@ export interface NoyaStateProviderProps<
159
149
  M extends object,
160
150
  E extends object,
161
151
  MenuT extends string,
152
+ I extends Record<string, any> = Record<string, unknown>,
162
153
  > extends Omit<
163
- UseNoyaStateOptions<S, M, E, MenuT>,
154
+ UseNoyaStateOptions<S, M, E, MenuT, I>,
164
155
  "schema" | "mergeHistoryEntries"
165
156
  > {
166
157
  children: React.ReactNode;
@@ -169,7 +160,7 @@ export interface NoyaStateProviderProps<
169
160
  }
170
161
 
171
162
  export type AnyNoyaStateContextValue = {
172
- noyaManager: NoyaManager<any, any, any, any>;
163
+ noyaManager: NoyaManager<any, any, any, any, any>;
173
164
  theme: AppTheme;
174
165
  viewType: AppViewType;
175
166
  };
@@ -202,7 +193,7 @@ export function useAnyNoyaStateContext() {
202
193
 
203
194
  export function useAnyNoyaManager() {
204
195
  const { noyaManager } = useAnyNoyaStateContext();
205
- return noyaManager as NoyaManager<any, any, any, any>;
196
+ return noyaManager as NoyaManager<any, any, any, any, any>;
206
197
  }
207
198
 
208
199
  export function useColorScheme() {
@@ -327,24 +318,24 @@ export function usePipelineManager() {
327
318
  return noyaManager.pipelineManager;
328
319
  }
329
320
 
330
- export function useConnectedUsersManager() {
321
+ export function useUserManager() {
331
322
  const { noyaManager } = useAnyNoyaStateContext();
332
- return noyaManager.connectedUsersManager;
323
+ return noyaManager.userManager;
333
324
  }
334
325
 
335
326
  export function useConnectedUsers() {
336
- const connectedUsersManager = useConnectedUsersManager();
337
- return useObservable(connectedUsersManager?.connectedUsers$);
327
+ const userManager = useUserManager();
328
+ return useObservable(userManager?.connectedUsers$);
338
329
  }
339
330
 
340
331
  export function useConnectedUser(userId?: string | null) {
341
- const connectedUsersManager = useConnectedUsersManager();
332
+ const userManager = useUserManager();
342
333
  return useObservable(
343
- connectedUsersManager?.connectedUsers$,
334
+ userManager?.connectedUsers$,
344
335
  useCallback(
345
336
  (users) => {
346
337
  return users.find((user) => {
347
- const prefix = user.id.slice(0, 8);
338
+ const prefix = user.connectionId.slice(0, 8);
348
339
  return userId?.includes(prefix);
349
340
  });
350
341
  },
@@ -353,14 +344,47 @@ export function useConnectedUser(userId?: string | null) {
353
344
  );
354
345
  }
355
346
 
356
- export function useAnyEphemeralUserData() {
347
+ export function useAnySharedConnectionData() {
357
348
  const { noyaManager } = useAnyNoyaStateContext();
358
- return noyaManager.ephemeralUserDataManager;
349
+ return noyaManager.sharedConnectionDataManager;
350
+ }
351
+
352
+ export function useEnqueueInput<
353
+ I extends Record<string, any> = Record<string, unknown>
354
+ >() {
355
+ const { noyaManager } = useAnyNoyaStateContext();
356
+ return noyaManager.enqueueInput as NoyaManager<
357
+ any,
358
+ any,
359
+ any,
360
+ any,
361
+ I
362
+ >["enqueueInput"];
363
+ }
364
+
365
+ export function useNoyaUser() {
366
+ const userManager = useUserManager();
367
+ return useObservable(userManager?.currentUser$);
368
+ }
369
+
370
+ export function useNoyaId() {
371
+ const userManager = useUserManager();
372
+ return useObservable(userManager?.currentId$);
373
+ }
374
+
375
+ export function useNoyaUserId() {
376
+ const userManager = useUserManager();
377
+ return useObservable(userManager?.currentUserId$);
359
378
  }
360
379
 
361
- export function useCurrentUserId() {
362
- const connectedUsersManager = useConnectedUsersManager();
363
- return useObservable(connectedUsersManager?.currentUserId$);
380
+ export function useNoyaConnectionId() {
381
+ const userManager = useUserManager();
382
+ return useObservable(userManager?.currentConnectionId$);
383
+ }
384
+
385
+ export function useNoyaClientId() {
386
+ const userManager = useUserManager();
387
+ return useObservable(userManager?.currentClientId$);
364
388
  }
365
389
 
366
390
  export function useIsProcessing() {
@@ -389,6 +413,50 @@ export function useResourceManager(): ResourceManager {
389
413
  return noyaManager.resourceManager;
390
414
  }
391
415
 
416
+ export function useNoyaTranscriptionManager(): TranscriptionManager {
417
+ const { noyaManager } = useAnyNoyaStateContext();
418
+ return noyaManager.transcriptionManager;
419
+ }
420
+
421
+ type UseNoyaAssetTranscriptionOptions = {
422
+ endpoint?: string;
423
+ apiKey?: string;
424
+ };
425
+
426
+ /**
427
+ * Returns a text transcription for the given assetId.
428
+ *
429
+ * - This API is available when running your app on noya.io, not when running it locally.
430
+ * - An asset must first be uploaded via the `useAssetManager().create()` method before
431
+ * it can be transcribed via this API.
432
+ * - The asset must be a valid audio or video file.
433
+ */
434
+ export function useNoyaAssetTranscription(
435
+ assetId: string,
436
+ options?: UseNoyaAssetTranscriptionOptions
437
+ ): AssetTranscriptionResult;
438
+ export function useNoyaAssetTranscription(
439
+ assetId?: string,
440
+ options?: UseNoyaAssetTranscriptionOptions
441
+ ): AssetTranscriptionResult | undefined;
442
+ export function useNoyaAssetTranscription(
443
+ assetId?: string,
444
+ options?: UseNoyaAssetTranscriptionOptions
445
+ ): AssetTranscriptionResult | undefined {
446
+ const transcriptionManager = useNoyaTranscriptionManager();
447
+
448
+ const observable = useMemo(() => {
449
+ if (!assetId) return undefined;
450
+
451
+ return transcriptionManager.getAssetTranscription$(assetId, {
452
+ endpoint: options?.endpoint,
453
+ apiKey: options?.apiKey,
454
+ });
455
+ }, [transcriptionManager, assetId, options?.endpoint, options?.apiKey]);
456
+
457
+ return useObservable(observable);
458
+ }
459
+
392
460
  // export function useResources(): Resource[] {
393
461
  // const { noyaManager } = useAnyNoyaStateContext();
394
462
  // const resources = useObservable(noyaManager.resourceManager.resources$);
@@ -431,29 +499,20 @@ export function useResources({
431
499
  const { addedResources, removedResources, modifiedResources } =
432
500
  diffResourceMaps(resourceMap, newResourceMap, "id");
433
501
 
434
- const { payload: createPayload, consumedModifiedResourceIds } =
435
- buildCreateResourcePayload({
436
- addedResources,
437
- modifiedResources,
438
- newResourceMap,
439
- resourceMap,
440
- });
441
-
442
- if (createPayload.length > 0) {
443
- await resourceManager.createResource(createPayload);
444
- }
445
-
446
- const consumedModifiedResourceIdsSet = new Set(
447
- consumedModifiedResourceIds
448
- );
502
+ const patchPayload = buildResourcePatchPayload({
503
+ addedResources,
504
+ modifiedResources,
505
+ removedResources,
506
+ });
449
507
 
450
- const remainingModifiedResources = modifiedResources.filter(
451
- (resource) => !consumedModifiedResourceIdsSet.has(resource.id)
452
- );
508
+ const hasPatchOperations =
509
+ (patchPayload.create?.length ?? 0) > 0 ||
510
+ (patchPayload.update?.length ?? 0) > 0 ||
511
+ (patchPayload.delete?.length ?? 0) > 0;
453
512
 
454
- await resourceManager.deleteResource(
455
- removedResources.map((resource) => ({ id: resource.id }))
456
- );
513
+ if (hasPatchOperations) {
514
+ await resourceManager.patchResources(patchPayload);
515
+ }
457
516
 
458
517
  if (shouldDeleteAssets) {
459
518
  for (const resource of removedResources) {
@@ -462,17 +521,6 @@ export function useResources({
462
521
  }
463
522
  }
464
523
  }
465
-
466
- await Promise.all(
467
- remainingModifiedResources.map(({ id, path, assetId, asset }) =>
468
- resourceManager.updateResource({
469
- id,
470
- path,
471
- assetId,
472
- ...(assetId ? {} : asset ? { asset } : {}),
473
- })
474
- )
475
- );
476
524
  },
477
525
  [resourceMap, resourceManager, shouldDeleteAssets, assetManager]
478
526
  );
@@ -535,6 +583,7 @@ export function createNoyaContext<
535
583
  M extends object = object,
536
584
  E extends object = object,
537
585
  MenuT extends string = string,
586
+ I extends Record<string, any> = Record<string, unknown>,
538
587
  >({
539
588
  schema,
540
589
  mergeHistoryEntries,
@@ -557,7 +606,7 @@ export function createNoyaContext<
557
606
  >;
558
607
 
559
608
  const NoyaStateContext = AnyNoyaStateContext as React.Context<
560
- NoyaManager<S, M, E> | undefined
609
+ NoyaManager<S, M, E, MenuT, I> | undefined
561
610
  >;
562
611
 
563
612
  function NoyaContextProvider({
@@ -578,12 +627,13 @@ export function createNoyaContext<
578
627
  children,
579
628
  initialState,
580
629
  ...options
581
- }: NoyaStateProviderProps<S, M, E, MenuT>) {
630
+ }: NoyaStateProviderProps<S, M, E, MenuT, I>) {
582
631
  const [, , { noyaManager, theme, viewType }] = useNoyaStateInternal<
583
632
  S,
584
633
  M,
585
634
  E,
586
- MenuT
635
+ MenuT,
636
+ I
587
637
  >(initialState, {
588
638
  schema,
589
639
  mergeHistoryEntries,
@@ -730,9 +780,9 @@ export function createNoyaContext<
730
780
  >;
731
781
  }
732
782
 
733
- function useEphemeralUserDataManager() {
783
+ function useSharedConnectionDataManager() {
734
784
  const { noyaManager } = useAnyNoyaStateContext();
735
- return noyaManager.ephemeralUserDataManager as EphemeralUserDataManager<E>;
785
+ return noyaManager.sharedConnectionDataManager as SharedConnectionDataManager<E>;
736
786
  }
737
787
 
738
788
  function useNoyaManager() {
@@ -740,7 +790,8 @@ export function createNoyaContext<
740
790
  S,
741
791
  PartialM,
742
792
  E,
743
- MenuT
793
+ MenuT,
794
+ I
744
795
  >;
745
796
  }
746
797
 
@@ -812,7 +863,7 @@ export function createNoyaContext<
812
863
  useSetValue,
813
864
  useValueState,
814
865
  useStateManager,
815
- useEphemeralUserDataManager,
866
+ useSharedConnectionDataManager,
816
867
  useLeftMenuItems,
817
868
  useSetLeftMenuItems,
818
869
  useRightMenuItems,
@@ -13,6 +13,8 @@ it("gets AppData with no data", () => {
13
13
  viewType: "editable",
14
14
  initialState,
15
15
  inspector: false,
16
+ isDemo: false,
17
+ access: "write",
16
18
  });
17
19
  });
18
20
 
@@ -37,6 +39,8 @@ it("gets AppData with data param", () => {
37
39
  viewType: "editable",
38
40
  initialState: { foo: "bar" },
39
41
  inspector: false,
42
+ isDemo: false,
43
+ access: "write",
40
44
  });
41
45
  });
42
46
 
@@ -50,6 +54,8 @@ it("uses initial state if data doesn't parse", () => {
50
54
  viewType: "editable",
51
55
  initialState: { foo: "bar" },
52
56
  inspector: false,
57
+ isDemo: false,
58
+ access: "write",
53
59
  });
54
60
  });
55
61
 
@@ -67,9 +73,33 @@ it("gets AppData with schema", () => {
67
73
  viewType: "editable",
68
74
  initialState: { foo: "bar" },
69
75
  inspector: false,
76
+ isDemo: false,
77
+ access: "write",
70
78
  });
71
79
  });
72
80
 
81
+ it("preserves userId from data param", () => {
82
+ const existing = {
83
+ theme: "light",
84
+ viewType: "editable",
85
+ initialState: { foo: "bar" },
86
+ inspector: false,
87
+ userId: "user-123",
88
+ isDemo: true,
89
+ };
90
+
91
+ const data = getAppData(null as unknown, undefined, {
92
+ window: {
93
+ location: {
94
+ hash: `#data=${encodeURIComponent(JSON.stringify(existing))}`,
95
+ },
96
+ },
97
+ });
98
+
99
+ expect(data.userId).toBe("user-123");
100
+ expect(data.isDemo).toBe(true);
101
+ });
102
+
73
103
  // it("enforces schema on data param", () => {
74
104
  // const schema = Type.Object({
75
105
  // foo: Type.String({ default: "bar" }),
@@ -103,6 +133,8 @@ it("gets AppData with function initial state", () => {
103
133
  viewType: "editable",
104
134
  initialState: { foo: "bar" },
105
135
  inspector: false,
136
+ isDemo: false,
137
+ access: "write",
106
138
  });
107
139
  });
108
140
 
@@ -130,6 +162,8 @@ it("even if initial state does not match schema, make sure we use multiplayer ur
130
162
  initialState: { foo: "" },
131
163
  inspector: false,
132
164
  multiplayerUrl: "https://example.com",
165
+ isDemo: false,
166
+ access: "write",
133
167
  });
134
168
  });
135
169