@noya-app/noya-multiplayer-react 0.1.84 → 0.1.86

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.84",
3
+ "version": "0.1.86",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -11,7 +11,7 @@
11
11
  "dev": "npm run build -- --watch"
12
12
  },
13
13
  "dependencies": {
14
- "@noya-app/state-manager": "0.1.66",
14
+ "@noya-app/state-manager": "0.1.68",
15
15
  "@noya-app/emitter": "0.1.0",
16
16
  "@noya-app/observable": "0.1.12",
17
17
  "@noya-app/task-runner": "0.1.7",
@@ -170,26 +170,28 @@ export const AnyNoyaStateContext = createContext<
170
170
  AnyNoyaStateContextValue | undefined
171
171
  >(undefined);
172
172
 
173
- export function useAnyNoyaStateContext() {
173
+ export function useOptionalAnyNoyaStateContext() {
174
174
  const value = useContext(AnyNoyaStateContext);
175
+ const bindings = getNoyaSingletonBindings();
175
176
 
176
- if (!value) {
177
- const bindings = getNoyaSingletonBindings();
177
+ if (value) return value;
178
+ if (!bindings) return undefined;
178
179
 
179
- if (bindings) {
180
- return {
181
- noyaManager: getNoyaManagerSingleton(),
182
- theme: bindings.theme,
183
- viewType: bindings.viewType,
184
- } as AnyNoyaStateContextValue;
185
- }
186
-
187
- throw new Error(
188
- "useNoyaStateContext must be used within a NoyaStateProvider"
189
- );
190
- }
180
+ return {
181
+ noyaManager: getNoyaManagerSingleton(),
182
+ theme: bindings.theme,
183
+ viewType: bindings.viewType,
184
+ } as AnyNoyaStateContextValue;
185
+ }
186
+
187
+ export function useAnyNoyaStateContext() {
188
+ const value = useOptionalAnyNoyaStateContext();
191
189
 
192
- return value;
190
+ if (value) return value;
191
+
192
+ throw new Error(
193
+ "useNoyaStateContext must be used within a NoyaStateProvider"
194
+ );
193
195
  }
194
196
 
195
197
  export function useAnyNoyaManager() {
@@ -0,0 +1,92 @@
1
+ import { Asset, Resource } from "@noya-app/noya-schemas";
2
+ import { NoyaManager } from "@noya-app/state-manager";
3
+ import { act, cleanup, renderHook } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it } from "bun:test";
5
+ import { PropsWithChildren } from "react";
6
+ import { AnyNoyaStateContext } from "../NoyaStateContext";
7
+ import { useResourceUrl } from "../useResourceUrl";
8
+
9
+ const asset = {
10
+ id: "asset-id",
11
+ stableId: "asset-stable-id",
12
+ url: "blob:http://localhost/image",
13
+ } as Asset;
14
+
15
+ const resource = {
16
+ accessibleByFileId: null,
17
+ accessibleByFileVersionId: null,
18
+ createdAt: "2026-01-01T00:00:00.000Z",
19
+ updatedAt: "2026-01-01T00:00:00.000Z",
20
+ id: "resource-id",
21
+ stableId: "resource-stable-id",
22
+ path: "image.png",
23
+ type: "asset",
24
+ assetId: asset.id,
25
+ url: "/api/resources/resource-id",
26
+ } satisfies Resource;
27
+
28
+ afterEach(cleanup);
29
+
30
+ describe("useResourceUrl", () => {
31
+ it("resolves IDs and stable IDs and responds to data updates", () => {
32
+ const noyaManager = new NoyaManager(
33
+ {},
34
+ {
35
+ initialAssets: [asset],
36
+ initialResources: [resource],
37
+ }
38
+ );
39
+ const wrapper = ({ children }: PropsWithChildren) => (
40
+ <AnyNoyaStateContext.Provider
41
+ value={{ noyaManager, theme: "light", viewType: "editable" }}
42
+ >
43
+ {children}
44
+ </AnyNoyaStateContext.Provider>
45
+ );
46
+ const { result, rerender } = renderHook(
47
+ ({ id }: { id: string | undefined }) => useResourceUrl(id),
48
+ {
49
+ initialProps: { id: resource.id },
50
+ wrapper,
51
+ }
52
+ );
53
+
54
+ expect(result.current).toBe(asset.url);
55
+
56
+ rerender({ id: resource.stableId });
57
+ expect(result.current).toBe(asset.url);
58
+
59
+ act(() => {
60
+ noyaManager.assetManager.set([
61
+ { ...asset, url: "blob:http://localhost/updated-image" },
62
+ ]);
63
+ });
64
+ expect(result.current).toBe("blob:http://localhost/updated-image");
65
+
66
+ act(() => {
67
+ noyaManager.assetManager.set([]);
68
+ });
69
+ expect(result.current).toBe(resource.url);
70
+
71
+ act(() => {
72
+ noyaManager.resourceManager.setResources([
73
+ { ...resource, url: "/api/resources/updated-resource-id" },
74
+ ]);
75
+ });
76
+ expect(result.current).toBe("/api/resources/updated-resource-id");
77
+ });
78
+
79
+ it("returns undefined when no resource ID is provided", () => {
80
+ const noyaManager = new NoyaManager({});
81
+ const wrapper = ({ children }: PropsWithChildren) => (
82
+ <AnyNoyaStateContext.Provider
83
+ value={{ noyaManager, theme: "light", viewType: "editable" }}
84
+ >
85
+ {children}
86
+ </AnyNoyaStateContext.Provider>
87
+ );
88
+ const { result } = renderHook(() => useResourceUrl(undefined), { wrapper });
89
+
90
+ expect(result.current).toBeUndefined();
91
+ });
92
+ });
package/src/index.ts CHANGED
@@ -9,5 +9,6 @@ export * from "./noyaApp";
9
9
  export * from "./NoyaStateContext";
10
10
  export * from "./singleton";
11
11
  export * from "./useObservable";
12
+ export * from "./useResourceUrl";
12
13
  export { WebSocketConnection } from "./WebSocketConnection";
13
14
  export type { ConnectionEvent, ConnectionOptions } from "./WebSocketConnection";
package/src/noyaApp.ts CHANGED
@@ -40,6 +40,8 @@ export type AppData<State> = {
40
40
  clientId?: string;
41
41
  clientName?: string;
42
42
  clientImage?: string;
43
+ parentOrigin?: string;
44
+ enableDocumentAI?: boolean;
43
45
  };
44
46
 
45
47
  export function createDefaultAppData<State>(
@@ -150,6 +152,8 @@ export function getSyncAdapter<
150
152
  policyAuthContext?: PolicyAuthContext;
151
153
  serverScripts?: LocalStorageSyncOptions["serverScripts"];
152
154
  git?: LocalStorageSyncOptions["git"];
155
+ parentOrigin?: string;
156
+ enableDocumentAI?: boolean;
153
157
  }): SyncAdapter<S, M, E, MenuT, I> {
154
158
  const {
155
159
  multiplayerUrl,
@@ -158,10 +162,17 @@ export function getSyncAdapter<
158
162
  policyAuthContext,
159
163
  serverScripts,
160
164
  git,
165
+ parentOrigin,
166
+ enableDocumentAI,
161
167
  } = params ?? {};
162
168
 
163
169
  return isEmbeddedApp()
164
- ? parentFrameSync<S, M, E, MenuT, I>({ debug, policyAuthContext })
170
+ ? parentFrameSync<S, M, E, MenuT, I>({
171
+ debug,
172
+ policyAuthContext,
173
+ origin: parentOrigin,
174
+ enableDocumentAI,
175
+ })
165
176
  : multiplayerUrl
166
177
  ? webSocketSync<S, M, E, MenuT, I>({ url: multiplayerUrl, debug })
167
178
  : offlineStorageKey
@@ -258,6 +269,8 @@ export function useNoyaStateInternal<
258
269
  userId,
259
270
  isDemo,
260
271
  access,
272
+ parentOrigin,
273
+ enableDocumentAI,
261
274
  },
262
275
  ] = useState(() => {
263
276
  return getAppData(initialState, options?.schema, {
@@ -289,6 +302,8 @@ export function useNoyaStateInternal<
289
302
  policyAuthContext: basePolicyAuthContext,
290
303
  serverScripts: options?.serverScripts,
291
304
  git: options?.git,
305
+ parentOrigin,
306
+ enableDocumentAI,
292
307
  });
293
308
  }, [
294
309
  multiplayerUrl,
@@ -297,10 +312,23 @@ export function useNoyaStateInternal<
297
312
  basePolicyAuthContext,
298
313
  options?.serverScripts,
299
314
  options?.git,
315
+ parentOrigin,
316
+ enableDocumentAI,
300
317
  ]);
301
318
 
302
319
  const [noyaManager] = useState(
303
- () => new NoyaManager<S, M, E, MenuT, I>(noyaAppInitialState as S, options)
320
+ () =>
321
+ new NoyaManager<S, M, E, MenuT, I>(noyaAppInitialState as S, {
322
+ ...options,
323
+ resourceUrlBase:
324
+ options?.resourceUrlBase ??
325
+ parentOrigin ??
326
+ (typeof globalThis.location === "undefined"
327
+ ? undefined
328
+ : globalThis.location.origin === "null"
329
+ ? undefined
330
+ : globalThis.location.origin),
331
+ })
304
332
  );
305
333
 
306
334
  useEffect(() => {
package/src/singleton.tsx CHANGED
@@ -89,7 +89,17 @@ export function initializeNoyaSingleton<
89
89
 
90
90
  noyaManagerSingleton = new NoyaManager<S, M, E, MenuT, I>(
91
91
  appData.initialState as S,
92
- managerOptions
92
+ {
93
+ ...managerOptions,
94
+ resourceUrlBase:
95
+ managerOptions.resourceUrlBase ??
96
+ appData.parentOrigin ??
97
+ (typeof globalThis.location === "undefined"
98
+ ? undefined
99
+ : globalThis.location.origin === "null"
100
+ ? undefined
101
+ : globalThis.location.origin),
102
+ }
93
103
  );
94
104
 
95
105
  if (appData.userId) {
@@ -109,6 +119,8 @@ export function initializeNoyaSingleton<
109
119
  serverScripts: managerOptions?.serverScripts,
110
120
  git: managerOptions?.git,
111
121
  policyAuthContext,
122
+ parentOrigin: appData.parentOrigin,
123
+ enableDocumentAI: appData.enableDocumentAI,
112
124
  });
113
125
 
114
126
  // Activate sync immediately for the singleton
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import { getResourceUrl } from "@noya-app/state-manager";
4
+ import { useAnyNoyaStateContext } from "./NoyaStateContext";
5
+ import { useObservable } from "./useObservable";
6
+
7
+ export function useResourceUrl(
8
+ idOrStableId: string | undefined
9
+ ): string | undefined {
10
+ const { noyaManager } = useAnyNoyaStateContext();
11
+ const resources = useObservable(
12
+ noyaManager.resourceManager.optimisticResources$
13
+ );
14
+ const assets = useObservable(noyaManager.assetManager.assets$);
15
+
16
+ if (!idOrStableId) return undefined;
17
+
18
+ return getResourceUrl(idOrStableId, resources, assets);
19
+ }