@peers-app/peers-ui 0.25.20 → 0.25.21

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.
@@ -167,7 +167,7 @@ const UIRouter = (props) => {
167
167
  if (!matchingRoute) {
168
168
  return undefined;
169
169
  }
170
- return UILoader({ peersUIId: matchingRoute.peersUIId, props: props.props });
170
+ return (0, jsx_runtime_1.jsx)(UILoader, { peersUIId: matchingRoute.peersUIId, props: props.props });
171
171
  };
172
172
  exports.UIRouter = UIRouter;
173
173
  const UILoader = (args) => {
@@ -177,22 +177,9 @@ const UILoader = (args) => {
177
177
  const { props, peersUIId } = args;
178
178
  const [lastReloadTime] = (0, hooks_1.useObservable)(globals_1.packageReloaded);
179
179
  props.__lastPackageReload = lastReloadTime;
180
- // check if peersUIId maps to an in-memory react component
181
- const peersUI = peersUIs[peersUIId];
182
- if (peersUI?.content) {
183
- const parseResult = peersUI.propsSchema.safeParse(props);
184
- if (!parseResult.success) {
185
- console.error("props did not match schema", {
186
- props,
187
- schema: peersUI.propsSchema,
188
- errors: parseResult.error,
189
- });
190
- return ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsxs)("pre", { children: ["Peers UI could not be loaded ", (0, jsx_runtime_1.jsx)("br", {}), "peersUIId: ", peersUI.peersUIId, " ", (0, jsx_runtime_1.jsx)("br", {}), "Component name: ", peersUI.content?.name, " ", (0, jsx_runtime_1.jsx)("br", {}), "Props did not match schema ", (0, jsx_runtime_1.jsx)("br", {}), "Errors: ", JSON.stringify(parseResult.error, null, 2), " ", (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("br", {}), "Props ", JSON.stringify((0, peers_sdk_1.toJSON)(props), null, 2), (0, jsx_runtime_1.jsx)("br", {})] }) }));
191
- }
192
- const Component = peersUI.content;
193
- return ((0, jsx_runtime_1.jsx)(UIErrorBoundary, { peersUIId: peersUIId, componentName: peersUI.content?.name, children: (0, jsx_runtime_1.jsx)(Component, { ...args.props }) }));
194
- }
195
- return (0, jsx_runtime_1.jsx)(UIAsyncLoader, { peersUIId: peersUIId, props: props });
180
+ // Keep the same loader ancestor after the bundle enters the cache. Replacing it
181
+ // on a path change would unmount the screen before its navigation guard can run.
182
+ return (0, jsx_runtime_1.jsx)(UIAsyncLoader, { peersUIId: peersUIId, props: props }, peersUIId);
196
183
  };
197
184
  const uiLoadingPromises = {};
198
185
  // Check if we're running in a React Native WebView (has injectUIBundle available)
@@ -273,6 +260,7 @@ function loadUIBundle(pkg, forceRefresh) {
273
260
  }
274
261
  const UIAsyncLoader = (props) => {
275
262
  const { peersUIId } = props;
263
+ const peersUI = peersUIs[peersUIId];
276
264
  const [loaded, setLoaded] = react_1.default.useState(false);
277
265
  const [loadError, setLoadError] = react_1.default.useState(null);
278
266
  const allRoutes = _window.getPeersUIRoutes();
@@ -280,7 +268,7 @@ const UIAsyncLoader = (props) => {
280
268
  (0, hooks_1.useObservable)(routes_loader_1.allPackages);
281
269
  const pkg = (0, routes_loader_1.allPackages)().find((p) => p.packageId === uiPackageId);
282
270
  (0, react_1.useEffect)(() => {
283
- if (!pkg)
271
+ if (!pkg || peersUIs[peersUIId]?.content)
284
272
  return;
285
273
  const pkgRef = pkg;
286
274
  setLoadError(null);
@@ -290,11 +278,11 @@ const UIAsyncLoader = (props) => {
290
278
  console.error(`Failed to load UI bundle for package ${pkgRef.name}:`, error);
291
279
  setLoadError(error);
292
280
  });
293
- }, [pkg?.packageId, pkg?.name, pkg]);
294
- if (!pkg) {
281
+ }, [peersUIId, pkg?.packageId, pkg?.name, pkg]);
282
+ if (!peersUI?.content && !pkg) {
295
283
  return (0, jsx_runtime_1.jsxs)("div", { children: ["Could not find package that owns peersUIId ", peersUIId] });
296
284
  }
297
- if (loadError) {
285
+ if (!peersUI?.content && loadError) {
298
286
  return ((0, jsx_runtime_1.jsxs)("div", { style: {
299
287
  padding: "20px",
300
288
  border: "2px solid #ff6b6b",
@@ -303,7 +291,7 @@ const UIAsyncLoader = (props) => {
303
291
  color: "#721c24",
304
292
  fontFamily: "monospace",
305
293
  margin: "10px 0",
306
- }, children: [(0, jsx_runtime_1.jsx)("h3", { style: { color: "#c53030", marginTop: 0 }, children: "Bundle Loading Error" }), (0, jsx_runtime_1.jsxs)("div", { style: { marginBottom: "15px" }, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Peers UI ID:" }), " ", peersUIId] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Package:" }), " ", pkg.name] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Package ID:" }), " ", pkg.packageId] })] }), (0, jsx_runtime_1.jsxs)("details", { style: { marginBottom: "15px" }, children: [(0, jsx_runtime_1.jsx)("summary", { style: { cursor: "pointer", fontWeight: "bold", marginBottom: "10px" }, children: "Error Details" }), (0, jsx_runtime_1.jsxs)("div", { style: {
294
+ }, children: [(0, jsx_runtime_1.jsx)("h3", { style: { color: "#c53030", marginTop: 0 }, children: "Bundle Loading Error" }), (0, jsx_runtime_1.jsxs)("div", { style: { marginBottom: "15px" }, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Peers UI ID:" }), " ", peersUIId] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Package:" }), " ", pkg?.name] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Package ID:" }), " ", pkg?.packageId] })] }), (0, jsx_runtime_1.jsxs)("details", { style: { marginBottom: "15px" }, children: [(0, jsx_runtime_1.jsx)("summary", { style: { cursor: "pointer", fontWeight: "bold", marginBottom: "10px" }, children: "Error Details" }), (0, jsx_runtime_1.jsxs)("div", { style: {
307
295
  backgroundColor: "#fed7d7",
308
296
  padding: "10px",
309
297
  borderRadius: "4px",
@@ -328,16 +316,15 @@ const UIAsyncLoader = (props) => {
328
316
  cursor: "pointer",
329
317
  }, children: "Retry Loading" })] }));
330
318
  }
331
- if (!loaded) {
319
+ if (!peersUI?.content && !loaded) {
332
320
  return (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {});
333
321
  }
334
- const peersUI = peersUIs[peersUIId];
335
322
  if (peersUI?.content) {
336
- const _props = { ...props, uiVar: undefined };
323
+ const _props = props.props;
337
324
  try {
338
325
  peersUI.propsSchema.parse(_props);
339
326
  const Component = peersUI.content;
340
- return ((0, jsx_runtime_1.jsx)(UIErrorBoundary, { peersUIId: peersUIId, packageName: pkg.name, componentName: peersUI.content?.name, children: (0, jsx_runtime_1.jsx)(Component, { ..._props }) }));
327
+ return ((0, jsx_runtime_1.jsx)(UIErrorBoundary, { peersUIId: peersUIId, packageName: pkg?.name, componentName: peersUI.content?.name, children: (0, jsx_runtime_1.jsx)(Component, { ..._props }) }));
341
328
  }
342
329
  catch (parseError) {
343
330
  return ((0, jsx_runtime_1.jsxs)("div", { style: {
@@ -348,7 +335,7 @@ const UIAsyncLoader = (props) => {
348
335
  color: "#721c24",
349
336
  fontFamily: "monospace",
350
337
  margin: "10px 0",
351
- }, children: [(0, jsx_runtime_1.jsx)("h3", { style: { color: "#c53030", marginTop: 0 }, children: "Props Validation Error" }), (0, jsx_runtime_1.jsxs)("div", { style: { marginBottom: "15px" }, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Peers UI ID:" }), " ", peersUIId] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Package:" }), " ", pkg.name] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Component:" }), " ", peersUI.content?.name] })] }), (0, jsx_runtime_1.jsxs)("div", { style: {
338
+ }, children: [(0, jsx_runtime_1.jsx)("h3", { style: { color: "#c53030", marginTop: 0 }, children: "Props Validation Error" }), (0, jsx_runtime_1.jsxs)("div", { style: { marginBottom: "15px" }, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Peers UI ID:" }), " ", peersUIId] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Package:" }), " ", pkg?.name] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Component:" }), " ", peersUI.content?.name] })] }), (0, jsx_runtime_1.jsxs)("div", { style: {
352
339
  backgroundColor: "#fed7d7",
353
340
  padding: "10px",
354
341
  borderRadius: "4px",
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const jsx_runtime_1 = require("react/jsx-runtime");
4
+ const react_1 = require("@testing-library/react");
5
+ const react_2 = require("react");
6
+ const mockPackages = [];
7
+ const mockGetFileContents = jest.fn();
8
+ jest.mock("@peers-app/peers-sdk", () => ({
9
+ debounceByArgs: (fn) => fn,
10
+ getUserContext: () => new Promise(() => { }),
11
+ packagePrefsVar: () => Object.assign(() => ({}), { loadingPromise: Promise.resolve() }),
12
+ PackageVersions: () => ({ get: async () => ({ uiBundleFileId: "bundle" }) }),
13
+ rpcServerCalls: { getFileContents: (...args) => mockGetFileContents(...args) },
14
+ }));
15
+ jest.mock("../globals", () => ({ mainContentPath: () => "", packageReloaded: () => 0 }));
16
+ jest.mock("../hooks", () => ({ useObservable: (value) => [value()] }));
17
+ jest.mock("../ui-defaults", () => ({}));
18
+ jest.mock("../components/loading-indicator", () => ({
19
+ LoadingIndicator: () => (0, jsx_runtime_1.jsx)("p", { children: "Loading bundle" }),
20
+ }));
21
+ jest.mock("./routes-loader", () => ({ allPackages: () => mockPackages }));
22
+ const ui_loader_1 = require("./ui-loader");
23
+ test.each([
24
+ false,
25
+ true,
26
+ ])("query changes retain screen state and props (preloaded: %s)", async (preloaded) => {
27
+ const packageId = `package-${preloaded}`;
28
+ const peersUIId = `ui-${preloaded}`;
29
+ const base = `package-nav/${packageId}/app`;
30
+ let path = `${base}?jobId=first`;
31
+ const listeners = new Set();
32
+ let mounts = 0;
33
+ let unmounts = 0;
34
+ function Content({ caller }) {
35
+ const [draft, setDraft] = (0, react_2.useState)("");
36
+ const currentPath = (0, react_2.useSyncExternalStore)((listener) => {
37
+ listeners.add(listener);
38
+ return () => {
39
+ listeners.delete(listener);
40
+ };
41
+ }, () => path);
42
+ (0, react_2.useEffect)(() => {
43
+ mounts++;
44
+ return () => {
45
+ unmounts++;
46
+ };
47
+ }, []);
48
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("p", { children: caller }), (0, jsx_runtime_1.jsx)("p", { children: currentPath }), (0, jsx_runtime_1.jsx)("input", { "aria-label": "Draft", value: draft, onChange: (event) => setDraft(event.target.value) })] }));
49
+ }
50
+ const schema = { parse: (value) => value, safeParse: () => ({ success: true }) };
51
+ const ui = { peersUIId, content: Content, propsSchema: schema };
52
+ const routeWindow = window;
53
+ routeWindow.registerPeersUIRoute({ packageId, peersUIId, path: base });
54
+ mockPackages.length = 0;
55
+ mockGetFileContents.mockReset();
56
+ let releaseBundle;
57
+ if (preloaded) {
58
+ await (0, ui_loader_1.registerInternalPeersUI)({ component: Content, peersUIId, propsSchema: schema });
59
+ }
60
+ else {
61
+ mockPackages.push({ packageId, name: "Test package", activePackageVersionId: "version" });
62
+ routeWindow.testUI = ui;
63
+ mockGetFileContents.mockImplementation(() => new Promise((resolve) => {
64
+ releaseBundle = resolve;
65
+ }));
66
+ }
67
+ const view = (0, react_1.render)((0, jsx_runtime_1.jsx)(ui_loader_1.UIRouter, { path: path, props: { caller: "Caller props" } }));
68
+ if (!preloaded) {
69
+ expect(await react_1.screen.findByText("Loading bundle")).toBeInTheDocument();
70
+ await (0, react_1.act)(async () => {
71
+ releaseBundle("exportUIs({uis: [window.testUI]});");
72
+ });
73
+ }
74
+ expect(await react_1.screen.findByText("Caller props")).toBeInTheDocument();
75
+ react_1.fireEvent.change(react_1.screen.getByLabelText("Draft"), { target: { value: "Unsaved edits" } });
76
+ await (0, react_1.act)(async () => {
77
+ path = `${base}?jobId=second`;
78
+ for (const listener of listeners)
79
+ listener();
80
+ view.rerender((0, jsx_runtime_1.jsx)(ui_loader_1.UIRouter, { path: path, props: { caller: "Caller props" } }));
81
+ });
82
+ expect(react_1.screen.getByLabelText("Draft")).toHaveValue("Unsaved edits");
83
+ expect(react_1.screen.getByText(path)).toBeInTheDocument();
84
+ expect({ mounts, unmounts, loads: mockGetFileContents.mock.calls.length }).toEqual({
85
+ mounts: 1,
86
+ unmounts: 0,
87
+ loads: preloaded ? 0 : 1,
88
+ });
89
+ view.unmount();
90
+ delete routeWindow.testUI;
91
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.25.20",
3
+ "version": "0.25.21",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-ui.git"
@@ -30,7 +30,7 @@
30
30
  "lint:fix": "biome check --write ."
31
31
  },
32
32
  "peerDependencies": {
33
- "@peers-app/peers-sdk": "^0.25.20",
33
+ "@peers-app/peers-sdk": "^0.25.21",
34
34
  "bootstrap": "^5.3.3",
35
35
  "react": "^18.0.0",
36
36
  "react-dom": "^18.0.0"
@@ -41,7 +41,7 @@
41
41
  "@babel/preset-react": "^7.24.1",
42
42
  "@babel/preset-typescript": "^7.27.1",
43
43
  "@biomejs/biome": "2.3.11",
44
- "@peers-app/peers-sdk": "0.25.20",
44
+ "@peers-app/peers-sdk": "0.25.21",
45
45
  "@testing-library/dom": "^10.4.0",
46
46
  "@testing-library/jest-dom": "^6.6.3",
47
47
  "@testing-library/react": "^16.3.0",
@@ -0,0 +1,108 @@
1
+ import { act, fireEvent, render, screen } from "@testing-library/react";
2
+ import { useEffect, useState, useSyncExternalStore } from "react";
3
+
4
+ const mockPackages: Record<string, unknown>[] = [];
5
+ const mockGetFileContents = jest.fn();
6
+ jest.mock("@peers-app/peers-sdk", () => ({
7
+ debounceByArgs: (fn: unknown) => fn,
8
+ getUserContext: () => new Promise(() => {}),
9
+ packagePrefsVar: () => Object.assign(() => ({}), { loadingPromise: Promise.resolve() }),
10
+ PackageVersions: () => ({ get: async () => ({ uiBundleFileId: "bundle" }) }),
11
+ rpcServerCalls: { getFileContents: (...args: unknown[]) => mockGetFileContents(...args) },
12
+ }));
13
+ jest.mock("../globals", () => ({ mainContentPath: () => "", packageReloaded: () => 0 }));
14
+ jest.mock("../hooks", () => ({ useObservable: (value: () => unknown) => [value()] }));
15
+ jest.mock("../ui-defaults", () => ({}));
16
+ jest.mock("../components/loading-indicator", () => ({
17
+ LoadingIndicator: () => <p>Loading bundle</p>,
18
+ }));
19
+ jest.mock("./routes-loader", () => ({ allPackages: () => mockPackages }));
20
+
21
+ import { registerInternalPeersUI, UIRouter } from "./ui-loader";
22
+
23
+ test.each([
24
+ false,
25
+ true,
26
+ ])("query changes retain screen state and props (preloaded: %s)", async (preloaded) => {
27
+ const packageId = `package-${preloaded}`;
28
+ const peersUIId = `ui-${preloaded}`;
29
+ const base = `package-nav/${packageId}/app`;
30
+ let path = `${base}?jobId=first`;
31
+ const listeners = new Set<() => void>();
32
+ let mounts = 0;
33
+ let unmounts = 0;
34
+ function Content({ caller }: { caller: string }) {
35
+ const [draft, setDraft] = useState("");
36
+ const currentPath = useSyncExternalStore(
37
+ (listener) => {
38
+ listeners.add(listener);
39
+ return () => {
40
+ listeners.delete(listener);
41
+ };
42
+ },
43
+ () => path,
44
+ );
45
+ useEffect(() => {
46
+ mounts++;
47
+ return () => {
48
+ unmounts++;
49
+ };
50
+ }, []);
51
+ return (
52
+ <>
53
+ <p>{caller}</p>
54
+ <p>{currentPath}</p>
55
+ <input
56
+ aria-label="Draft"
57
+ value={draft}
58
+ onChange={(event) => setDraft(event.target.value)}
59
+ />
60
+ </>
61
+ );
62
+ }
63
+ const schema = { parse: (value: unknown) => value, safeParse: () => ({ success: true }) };
64
+ const ui = { peersUIId, content: Content, propsSchema: schema };
65
+ const routeWindow = window as unknown as {
66
+ registerPeersUIRoute: (route: object) => void;
67
+ testUI?: object;
68
+ };
69
+ routeWindow.registerPeersUIRoute({ packageId, peersUIId, path: base });
70
+ mockPackages.length = 0;
71
+ mockGetFileContents.mockReset();
72
+ let releaseBundle!: (code: string) => void;
73
+ if (preloaded) {
74
+ await registerInternalPeersUI({ component: Content, peersUIId, propsSchema: schema });
75
+ } else {
76
+ mockPackages.push({ packageId, name: "Test package", activePackageVersionId: "version" });
77
+ routeWindow.testUI = ui;
78
+ mockGetFileContents.mockImplementation(
79
+ () =>
80
+ new Promise<string>((resolve) => {
81
+ releaseBundle = resolve;
82
+ }),
83
+ );
84
+ }
85
+ const view = render(<UIRouter path={path} props={{ caller: "Caller props" }} />);
86
+ if (!preloaded) {
87
+ expect(await screen.findByText("Loading bundle")).toBeInTheDocument();
88
+ await act(async () => {
89
+ releaseBundle("exportUIs({uis: [window.testUI]});");
90
+ });
91
+ }
92
+ expect(await screen.findByText("Caller props")).toBeInTheDocument();
93
+ fireEvent.change(screen.getByLabelText("Draft"), { target: { value: "Unsaved edits" } });
94
+ await act(async () => {
95
+ path = `${base}?jobId=second`;
96
+ for (const listener of listeners) listener();
97
+ view.rerender(<UIRouter path={path} props={{ caller: "Caller props" }} />);
98
+ });
99
+ expect(screen.getByLabelText("Draft")).toHaveValue("Unsaved edits");
100
+ expect(screen.getByText(path)).toBeInTheDocument();
101
+ expect({ mounts, unmounts, loads: mockGetFileContents.mock.calls.length }).toEqual({
102
+ mounts: 1,
103
+ unmounts: 0,
104
+ loads: preloaded ? 0 : 1,
105
+ });
106
+ view.unmount();
107
+ delete routeWindow.testUI;
108
+ });
@@ -239,7 +239,7 @@ export const UIRouter = (props: IProps) => {
239
239
  return undefined;
240
240
  }
241
241
 
242
- return UILoader({ peersUIId: matchingRoute.peersUIId, props: props.props });
242
+ return <UILoader peersUIId={matchingRoute.peersUIId} props={props.props} />;
243
243
  };
244
244
 
245
245
  const UILoader = (args: { peersUIId: string; props?: Record<string, unknown> }) => {
@@ -249,40 +249,9 @@ const UILoader = (args: { peersUIId: string; props?: Record<string, unknown> })
249
249
  const { props, peersUIId } = args;
250
250
  const [lastReloadTime] = useObservable(packageReloaded);
251
251
  props.__lastPackageReload = lastReloadTime;
252
- // check if peersUIId maps to an in-memory react component
253
- const peersUI = peersUIs[peersUIId];
254
- if (peersUI?.content) {
255
- const parseResult = peersUI.propsSchema.safeParse(props);
256
- if (!parseResult.success) {
257
- console.error("props did not match schema", {
258
- props,
259
- schema: peersUI.propsSchema,
260
- errors: parseResult.error,
261
- });
262
- return (
263
- <div>
264
- <pre>
265
- Peers UI could not be loaded <br />
266
- peersUIId: {peersUI.peersUIId} <br />
267
- Component name: {peersUI.content?.name} <br />
268
- Props did not match schema <br />
269
- Errors: {JSON.stringify(parseResult.error, null, 2)} <br />
270
- <br />
271
- Props {JSON.stringify(toJSON(props), null, 2)}
272
- <br />
273
- </pre>
274
- </div>
275
- );
276
- }
277
- const Component = peersUI.content as React.ComponentType<Record<string, unknown>>;
278
- return (
279
- <UIErrorBoundary peersUIId={peersUIId} componentName={peersUI.content?.name}>
280
- <Component {...args.props} />
281
- </UIErrorBoundary>
282
- );
283
- }
284
-
285
- return <UIAsyncLoader peersUIId={peersUIId} props={props} />;
252
+ // Keep the same loader ancestor after the bundle enters the cache. Replacing it
253
+ // on a path change would unmount the screen before its navigation guard can run.
254
+ return <UIAsyncLoader key={peersUIId} peersUIId={peersUIId} props={props} />;
286
255
  };
287
256
 
288
257
  const uiLoadingPromises: Record<string, Promise<void>> = {};
@@ -379,6 +348,7 @@ function loadUIBundle(pkg: IPackage, forceRefresh?: boolean) {
379
348
 
380
349
  const UIAsyncLoader = (props: { peersUIId: string; props: Record<string, unknown> }) => {
381
350
  const { peersUIId } = props;
351
+ const peersUI = peersUIs[peersUIId];
382
352
  const [loaded, setLoaded] = React.useState(false);
383
353
  const [loadError, setLoadError] = React.useState<Error | null>(null);
384
354
 
@@ -389,7 +359,7 @@ const UIAsyncLoader = (props: { peersUIId: string; props: Record<string, unknown
389
359
  const pkg = allPackages().find((p) => p.packageId === uiPackageId);
390
360
 
391
361
  useEffect(() => {
392
- if (!pkg) return;
362
+ if (!pkg || peersUIs[peersUIId]?.content) return;
393
363
  const pkgRef = pkg;
394
364
  setLoadError(null);
395
365
  loadUIBundle(pkgRef)
@@ -398,13 +368,13 @@ const UIAsyncLoader = (props: { peersUIId: string; props: Record<string, unknown
398
368
  console.error(`Failed to load UI bundle for package ${pkgRef.name}:`, error);
399
369
  setLoadError(error);
400
370
  });
401
- }, [pkg?.packageId, pkg?.name, pkg]);
371
+ }, [peersUIId, pkg?.packageId, pkg?.name, pkg]);
402
372
 
403
- if (!pkg) {
373
+ if (!peersUI?.content && !pkg) {
404
374
  return <div>Could not find package that owns peersUIId {peersUIId}</div>;
405
375
  }
406
376
 
407
- if (loadError) {
377
+ if (!peersUI?.content && loadError) {
408
378
  return (
409
379
  <div
410
380
  style={{
@@ -423,10 +393,10 @@ const UIAsyncLoader = (props: { peersUIId: string; props: Record<string, unknown
423
393
  <strong>Peers UI ID:</strong> {peersUIId}
424
394
  </div>
425
395
  <div>
426
- <strong>Package:</strong> {pkg.name}
396
+ <strong>Package:</strong> {pkg?.name}
427
397
  </div>
428
398
  <div>
429
- <strong>Package ID:</strong> {pkg.packageId}
399
+ <strong>Package ID:</strong> {pkg?.packageId}
430
400
  </div>
431
401
  </div>
432
402
 
@@ -484,20 +454,19 @@ const UIAsyncLoader = (props: { peersUIId: string; props: Record<string, unknown
484
454
  );
485
455
  }
486
456
 
487
- if (!loaded) {
457
+ if (!peersUI?.content && !loaded) {
488
458
  return <LoadingIndicator />;
489
459
  }
490
460
 
491
- const peersUI = peersUIs[peersUIId];
492
461
  if (peersUI?.content) {
493
- const _props = { ...props, uiVar: undefined };
462
+ const _props = props.props;
494
463
  try {
495
464
  peersUI.propsSchema.parse(_props);
496
465
  const Component = peersUI.content as React.ComponentType<Record<string, unknown>>;
497
466
  return (
498
467
  <UIErrorBoundary
499
468
  peersUIId={peersUIId}
500
- packageName={pkg.name}
469
+ packageName={pkg?.name}
501
470
  componentName={peersUI.content?.name}
502
471
  >
503
472
  <Component {..._props} />
@@ -522,7 +491,7 @@ const UIAsyncLoader = (props: { peersUIId: string; props: Record<string, unknown
522
491
  <strong>Peers UI ID:</strong> {peersUIId}
523
492
  </div>
524
493
  <div>
525
- <strong>Package:</strong> {pkg.name}
494
+ <strong>Package:</strong> {pkg?.name}
526
495
  </div>
527
496
  <div>
528
497
  <strong>Component:</strong> {peersUI.content?.name}