@ai-matrx/summon 0.1.2 → 0.1.3

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/CHANGELOG.md CHANGED
@@ -1,9 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.3
4
+ ### Unreleased
5
+
6
+ - **New: the render map can live on the host.** `render` is now optional on `defineOverlay`, and `<SummonHost renders={…}>` takes an id → `() => import("./<id>.render")` map (`OverlayRenderMap`, data type erased, no `any`) for definitions declared without one. A definition's own `render` still wins. Why: every file that imports a definition to open it also inherited an `import()` edge to that overlay's window subtree; with ~220 overlays and ~450 opener files in AI Matrx that multiplied the build graph (a sub-app build went from ~10 s to an out-of-memory kill). An opened overlay with no render source shows its error card (`OverlayRenderMissingError`, `MISSING_RENDER_ERROR`) and logs in development. Guard: `src/react/__tests__/hostRenders.test.tsx`. Consumer action: none for existing users.
7
+
3
8
  ## 0.1.2
4
9
 
5
- UNVERIFIED: this release has no Unreleased entry and the source comparison could not be made (the caller supplied no source comparison). Whether source changed is unknown; consumer action is unknown.
6
- Check the package's gitHead on npm against this version's gitHead: `git diff <previous gitHead> <this gitHead> -- apps/shared/summon/src`.
10
+ SOURCE CHANGED WITH NO RELEASE NOTE. This release shipped source changes that nobody described in an Unreleased entry,
11
+ so they may include breaking API changes. Consumer action is UNKNOWN: read the diff before upgrading.
12
+ Diff: `git diff b7bc6ffc86 19d4c1f55d -- apps/shared/summon/src`
13
+
14
+ Changed shipped source files (4):
15
+ - `apps/shared/summon/src/persistence/controller.ts`
16
+ - `apps/shared/summon/src/persistence/data/createDataPersistence.ts`
17
+ - `apps/shared/summon/src/sheet/OverlaySheets.tsx`
18
+ - `apps/shared/summon/src/window/useWindowPanel.ts`
7
19
 
8
20
  ## 0.1.1
9
21
  ### Unreleased
package/README.md CHANGED
@@ -121,6 +121,7 @@ const Always = () => <notesWindow.Controller noteId="42" />;
121
121
  | `window` | `{ mobile, mobileSidebarAs, ephemeral, preservation, restoreDefaults, windowId, deprecated, unbiddenHome }` for window overlays |
122
122
  | `link` | `{ key, aliases?, hydrate }`, see Links |
123
123
  | `parse` | narrow raw data once; `null` refuses |
124
+ | `render` | `() => import("./x.render")`; optional when the host supplies it (see below), and wins over the host map when present |
124
125
 
125
126
  Minimizing a window always keeps its component mounted, whatever `keepAlive` says.
126
127
 
@@ -159,6 +160,26 @@ emit("uploaded", { url });
159
160
 
160
161
  Closing the instance disposes the channel. Emitting after that is a silent no-op.
161
162
 
163
+ ### Large apps: the render map lives on the host
164
+
165
+ Every file that imports a definition to open it also imports that definition's `render: () => import(…)`. To a bundler, each such file gains an edge to the overlay's whole window subtree. With a few overlays that is free. With hundreds of overlays and hundreds of opener files it multiplies the build graph: in AI Matrx, about 450 opener files each reached about 220 window subtrees, and a sub-app build went from about 10 s to an out-of-memory kill. This is the host-build law: never fan out `import()` edges.
166
+
167
+ The fix keeps "declare once" at two files plus one line. Leave `render` off the definitions, and give the host one id → import map, generated from the file names, that only the host imports:
168
+
169
+ ```ts
170
+ // definitions/notes.overlay.ts — no render: importing it costs nothing
171
+ export const notes = defineOverlay<{ noteId?: string }>()({ id: "notes", label: "Notes" });
172
+
173
+ // definitions/render-map.ts — generated, imported ONLY by the host (behind its own lazy boundary)
174
+ import type { OverlayRenderMap } from "@ai-matrx/summon/react";
175
+ export const RENDER_MAP: OverlayRenderMap = { notes: () => import("./notes.render") };
176
+
177
+ // the host
178
+ <SummonHost definitions={APP_OVERLAYS} renders={RENDER_MAP} />
179
+ ```
180
+
181
+ `OverlayRenderMap` erases each module's data type (`OverlayRenderProps<never>`), so differently typed render modules share one map without `any`. An overlay that is opened with no render source at all (no `render`, no map entry) shows its error card (`OverlayRenderMissingError`, exported as `MISSING_RENDER_ERROR`) and logs in development. It is never a silent blank.
182
+
162
183
  ## Mounting into an existing Redux store
163
184
 
164
185
  ```ts
package/dist/react.cjs CHANGED
@@ -25,6 +25,7 @@ __export(react_exports, {
25
25
  DEFAULT_IMPORT_TIMEOUT_MS: () => DEFAULT_IMPORT_TIMEOUT_MS,
26
26
  DEFAULT_KEEP_ALIVE_LIMIT: () => DEFAULT_KEEP_ALIVE_LIMIT,
27
27
  DEFAULT_STALL_WARN_MS: () => DEFAULT_STALL_WARN_MS,
28
+ MISSING_RENDER_ERROR: () => MISSING_RENDER_ERROR,
28
29
  OverlayErrorBoundary: () => OverlayErrorBoundary,
29
30
  OverlayErrorFallback: () => OverlayErrorFallback,
30
31
  OverlayLoadingFallback: () => OverlayLoadingFallback,
@@ -310,7 +311,7 @@ function SummonProvider({
310
311
  }
311
312
 
312
313
  // src/react/defineOverlay.tsx
313
- var import_react9 = require("react");
314
+ var import_react10 = require("react");
314
315
  var import_summon7 = require("@ai-matrx/summon");
315
316
  var import_summon8 = require("@ai-matrx/summon");
316
317
 
@@ -348,7 +349,7 @@ function emitOverlayEvent(channelId, type, payload) {
348
349
  }
349
350
 
350
351
  // src/react/instanceBody.tsx
351
- var import_react8 = require("react");
352
+ var import_react9 = require("react");
352
353
 
353
354
  // src/react/OverlayErrorBoundary.tsx
354
355
  var import_react6 = require("react");
@@ -433,6 +434,26 @@ function useOverlayState(name, initial) {
433
434
  return [value, setValue];
434
435
  }
435
436
 
437
+ // src/react/hostRenders.ts
438
+ var import_react8 = require("react");
439
+ var HostRendersContext = (0, import_react8.createContext)(null);
440
+ var MISSING_RENDER_ERROR = "OverlayRenderMissingError";
441
+ var missing = /* @__PURE__ */ new Map();
442
+ function missingRenderLoader(overlayId) {
443
+ let loader = missing.get(overlayId);
444
+ if (!loader) {
445
+ const message = `Overlay "${overlayId}" is open but has no render module: its definition has no \`render\` and <SummonHost renders> has no "${overlayId}" entry.`;
446
+ loader = () => {
447
+ if (process.env.NODE_ENV !== "production") console.error(`[summon] ${message}`);
448
+ const error = new Error(message);
449
+ error.name = MISSING_RENDER_ERROR;
450
+ return Promise.reject(error);
451
+ };
452
+ missing.set(overlayId, loader);
453
+ }
454
+ return loader;
455
+ }
456
+
436
457
  // src/react/instanceBody.tsx
437
458
  var import_jsx_runtime4 = require("react/jsx-runtime");
438
459
  function LazyRender({ overlayId, instanceId, data, hidden, close, render, attemptRef }) {
@@ -442,14 +463,17 @@ function LazyRender({ overlayId, instanceId, data, hidden, close, render, attemp
442
463
  const Render = entry.Component;
443
464
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Render, { overlayId, instanceId, data, isOpen: true, close, hidden });
444
465
  }
445
- function createInstanceBody(overlayId, label, render, parse, refusable) {
466
+ function createInstanceBody(overlayId, label, ownRender, parse, refusable) {
446
467
  function OverlayInstanceBody({ instanceId, instanceKey, data, hidden, close }) {
447
468
  const config = useSummonConfig();
448
469
  const ports = useSummonPorts();
449
- const attemptRef = (0, import_react8.useRef)(0);
450
- const parsed = (0, import_react8.useMemo)(() => parse(data), [data]);
470
+ const hostRenders = (0, import_react9.useContext)(HostRendersContext);
471
+ const hostRender = ownRender ? void 0 : hostRenders?.[overlayId];
472
+ const render = ownRender ?? hostRender ?? missingRenderLoader(overlayId);
473
+ const attemptRef = (0, import_react9.useRef)(0);
474
+ const parsed = (0, import_react9.useMemo)(() => parse(data), [data]);
451
475
  const refused = refusable && parsed === null;
452
- (0, import_react8.useEffect)(() => {
476
+ (0, import_react9.useEffect)(() => {
453
477
  if (!refused) return;
454
478
  if (process.env.NODE_ENV !== "production") {
455
479
  ports.notify.warn(`Overlay "${overlayId}" was opened with data its parse() refused; closing it.`, {
@@ -458,7 +482,7 @@ function createInstanceBody(overlayId, label, render, parse, refusable) {
458
482
  }
459
483
  close();
460
484
  }, [refused, ports, instanceKey, close]);
461
- const instance = (0, import_react8.useMemo)(
485
+ const instance = (0, import_react9.useMemo)(
462
486
  () => ({ overlayId, instanceId, instanceKey, close, hidden, rawData: data }),
463
487
  [instanceId, instanceKey, close, hidden, data]
464
488
  );
@@ -476,7 +500,7 @@ function createInstanceBody(overlayId, label, render, parse, refusable) {
476
500
  extras: config.errorReportExtras,
477
501
  onRetry: () => config.loaders.retry(overlayId, render, attemptRef.current),
478
502
  onClose: close,
479
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react8.Suspense, { fallback: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(config.loadingFallback, { overlayId, label }), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
503
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react9.Suspense, { fallback: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(config.loadingFallback, { overlayId, label }), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
480
504
  LazyRender,
481
505
  {
482
506
  overlayId,
@@ -493,7 +517,7 @@ function createInstanceBody(overlayId, label, render, parse, refusable) {
493
517
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(OverlayInstanceContext.Provider, { value: instance, children: config.renderWrapper ? config.renderWrapper(body, context) : body });
494
518
  }
495
519
  OverlayInstanceBody.displayName = `Overlay(${overlayId})`;
496
- return (0, import_react8.memo)(OverlayInstanceBody);
520
+ return (0, import_react9.memo)(OverlayInstanceBody);
497
521
  }
498
522
 
499
523
  // src/react/defineOverlay.tsx
@@ -525,19 +549,19 @@ function defineOverlay() {
525
549
  const meta = spec;
526
550
  function useSummon() {
527
551
  const dispatch = useSummonDispatch();
528
- return (0, import_react9.useCallback)((options) => openWith(dispatch, id, multi, options), [dispatch]);
552
+ return (0, import_react10.useCallback)((options) => openWith(dispatch, id, multi, options), [dispatch]);
529
553
  }
530
554
  function useIsOpen(instanceId = import_summon7.DEFAULT_INSTANCE_ID) {
531
555
  return useSummonSelector((state) => (0, import_summon8.selectIsOverlayOpen)(state, id, instanceId));
532
556
  }
533
557
  function useClose() {
534
558
  const dispatch = useSummonDispatch();
535
- return (0, import_react9.useCallback)((instanceId) => dispatchClose(dispatch, id, instanceId ?? import_summon7.DEFAULT_INSTANCE_ID), [dispatch]);
559
+ return (0, import_react10.useCallback)((instanceId) => dispatchClose(dispatch, id, instanceId ?? import_summon7.DEFAULT_INSTANCE_ID), [dispatch]);
536
560
  }
537
561
  function useEmit() {
538
562
  const instance = useOptionalOverlayInstance();
539
563
  const channelId = instance?.overlayId === id ? instance.rawData?.[CALLBACK_GROUP_KEY] : void 0;
540
- return (0, import_react9.useCallback)(
564
+ return (0, import_react10.useCallback)(
541
565
  (event, payload) => {
542
566
  if (typeof channelId === "string") emitOverlayEvent(channelId, event, payload);
543
567
  },
@@ -546,12 +570,12 @@ function defineOverlay() {
546
570
  }
547
571
  function Controller(props) {
548
572
  const dispatch = useSummonDispatch();
549
- const [ownInstanceId] = (0, import_react9.useState)(() => multi ? newInstanceId(id) : import_summon7.DEFAULT_INSTANCE_ID);
573
+ const [ownInstanceId] = (0, import_react10.useState)(() => multi ? newInstanceId(id) : import_summon7.DEFAULT_INSTANCE_ID);
550
574
  const instanceId = props.instanceId ?? ownInstanceId;
551
- const handlersRef = (0, import_react9.useRef)(props.handlers);
575
+ const handlersRef = (0, import_react10.useRef)(props.handlers);
552
576
  handlersRef.current = props.handlers;
553
577
  const hasHandlers = props.handlers !== void 0;
554
- const data = (0, import_react9.useMemo)(() => {
578
+ const data = (0, import_react10.useMemo)(() => {
555
579
  const out = {};
556
580
  for (const [key, value] of Object.entries(props)) {
557
581
  if (key !== "instanceId" && key !== "handlers") out[key] = value;
@@ -559,9 +583,9 @@ function defineOverlay() {
559
583
  return out;
560
584
  }, [props]);
561
585
  const signature = dataSignature(data);
562
- const dataRef = (0, import_react9.useRef)(data);
586
+ const dataRef = (0, import_react10.useRef)(data);
563
587
  dataRef.current = data;
564
- (0, import_react9.useEffect)(() => {
588
+ (0, import_react10.useEffect)(() => {
565
589
  const key = (0, import_summon7.instanceKey)(id, instanceId);
566
590
  const open = { ...dataRef.current };
567
591
  if (hasHandlers) open[CALLBACK_GROUP_KEY] = openOverlayEventChannel(key, () => handlersRef.current);
@@ -589,7 +613,7 @@ function defineOverlay() {
589
613
  }
590
614
 
591
615
  // src/react/SummonHost.tsx
592
- var import_react10 = require("react");
616
+ var import_react11 = require("react");
593
617
  var import_summon9 = require("@ai-matrx/summon");
594
618
  var import_summon10 = require("@ai-matrx/summon");
595
619
  var import_summon11 = require("@ai-matrx/summon");
@@ -612,7 +636,7 @@ var NO_RETAINED = /* @__PURE__ */ new Map();
612
636
  function useCloseOnNavigation() {
613
637
  const ports = useSummonPorts();
614
638
  const dispatch = useSummonDispatch();
615
- (0, import_react10.useEffect)(() => {
639
+ (0, import_react11.useEffect)(() => {
616
640
  const router = ports.router;
617
641
  let previous = router.pathname();
618
642
  return router.subscribe(() => {
@@ -624,19 +648,19 @@ function useCloseOnNavigation() {
624
648
  });
625
649
  }, [ports, dispatch]);
626
650
  }
627
- var InstanceSlot = (0, import_react10.memo)(function InstanceSlot2({ definition, instanceId, data, hidden }) {
651
+ var InstanceSlot = (0, import_react11.memo)(function InstanceSlot2({ definition, instanceId, data, hidden }) {
628
652
  const dispatch = useSummonDispatch();
629
- const close = (0, import_react10.useCallback)(() => dispatchClose(dispatch, definition.id, instanceId), [dispatch, definition.id, instanceId]);
653
+ const close = (0, import_react11.useCallback)(() => dispatchClose(dispatch, definition.id, instanceId), [dispatch, definition.id, instanceId]);
630
654
  const Body = definition.Body;
631
655
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Body, { instanceId, instanceKey: (0, import_summon12.instanceKey)(definition.id, instanceId), data, hidden, close });
632
656
  });
633
- var DefinitionSlot = (0, import_react10.memo)(function DefinitionSlot2({ definition }) {
657
+ var DefinitionSlot = (0, import_react11.memo)(function DefinitionSlot2({ definition }) {
634
658
  const { keepAlive } = useSummonConfig();
635
659
  const dispatch = useSummonDispatch();
636
660
  const open = useSummonSelector((state) => (0, import_summon11.selectOpenInstances)(state, definition.id), sameInstances);
637
661
  const keepHidden = definition.keepAlive === "hidden";
638
- const [retained, setRetained] = (0, import_react10.useState)(NO_RETAINED);
639
- const [previousOpen, setPreviousOpen] = (0, import_react10.useState)(open);
662
+ const [retained, setRetained] = (0, import_react11.useState)(NO_RETAINED);
663
+ const [previousOpen, setPreviousOpen] = (0, import_react11.useState)(open);
640
664
  if (previousOpen !== open) {
641
665
  setPreviousOpen(open);
642
666
  if (keepHidden) {
@@ -649,8 +673,8 @@ var DefinitionSlot = (0, import_react10.memo)(function DefinitionSlot2({ definit
649
673
  setRetained(next);
650
674
  }
651
675
  }
652
- const registered = (0, import_react10.useRef)(/* @__PURE__ */ new Set());
653
- (0, import_react10.useEffect)(() => {
676
+ const registered = (0, import_react11.useRef)(/* @__PURE__ */ new Set());
677
+ (0, import_react11.useEffect)(() => {
654
678
  const seen = registered.current;
655
679
  for (const instanceId of retained.keys()) {
656
680
  if (seen.has(instanceId)) continue;
@@ -673,7 +697,7 @@ var DefinitionSlot = (0, import_react10.memo)(function DefinitionSlot2({ definit
673
697
  keepAlive.release((0, import_summon12.instanceKey)(definition.id, instanceId));
674
698
  }
675
699
  }, [retained, keepAlive, definition.id, dispatch]);
676
- (0, import_react10.useEffect)(() => {
700
+ (0, import_react11.useEffect)(() => {
677
701
  const seen = registered.current;
678
702
  return () => {
679
703
  for (const instanceId of seen) {
@@ -689,30 +713,30 @@ var DefinitionSlot = (0, import_react10.memo)(function DefinitionSlot2({ definit
689
713
  for (const instance of open) {
690
714
  const node = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(InstanceSlot, { definition, instanceId: instance.instanceId, data: asRawData(instance.data), hidden: false }, instance.instanceId);
691
715
  nodes.push(
692
- keepHidden ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react10.Activity, { mode: "visible", children: node }, instance.instanceId) : node
716
+ keepHidden ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react11.Activity, { mode: "visible", children: node }, instance.instanceId) : node
693
717
  );
694
718
  }
695
719
  if (keepHidden) {
696
720
  for (const [instanceId, data] of retained) {
697
721
  nodes.push(
698
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react10.Activity, { mode: "hidden", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(InstanceSlot, { definition, instanceId, data, hidden: true }, instanceId) }, instanceId)
722
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react11.Activity, { mode: "hidden", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(InstanceSlot, { definition, instanceId, data, hidden: true }, instanceId) }, instanceId)
699
723
  );
700
724
  }
701
725
  }
702
726
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: nodes });
703
727
  });
704
- function SummonHost({ definitions }) {
705
- const registeredFor = (0, import_react10.useRef)(null);
728
+ function SummonHost({ definitions, renders }) {
729
+ const registeredFor = (0, import_react11.useRef)(null);
706
730
  if (registeredFor.current !== definitions) {
707
731
  (0, import_summon10.registerOverlayMeta)(definitions.map((definition) => definition.meta));
708
732
  registeredFor.current = definitions;
709
733
  }
710
734
  useCloseOnNavigation();
711
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: definitions.map((definition) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DefinitionSlot, { definition }, definition.id)) });
735
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(HostRendersContext.Provider, { value: renders ?? null, children: definitions.map((definition) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DefinitionSlot, { definition }, definition.id)) });
712
736
  }
713
737
 
714
738
  // src/react/hooks.ts
715
- var import_react11 = require("react");
739
+ var import_react12 = require("react");
716
740
  var import_summon13 = require("@ai-matrx/summon");
717
741
  var import_summon14 = require("@ai-matrx/summon");
718
742
  var import_summon15 = require("@ai-matrx/summon");
@@ -721,7 +745,7 @@ function useIsOverlayOpen(overlayId, instanceId = import_summon15.DEFAULT_INSTAN
721
745
  }
722
746
  function useOpenOverlay(overlayId) {
723
747
  const dispatch = useSummonDispatch();
724
- return (0, import_react11.useCallback)(
748
+ return (0, import_react12.useCallback)(
725
749
  (options) => {
726
750
  const multi = (0, import_summon13.getOverlayMeta)(overlayId)?.instance === "multi";
727
751
  const instanceId = options?.instanceId ?? (multi ? newInstanceId(overlayId) : import_summon15.DEFAULT_INSTANCE_ID);
@@ -733,6 +757,6 @@ function useOpenOverlay(overlayId) {
733
757
  }
734
758
  function useCloseOverlay(overlayId, instanceId = import_summon15.DEFAULT_INSTANCE_ID) {
735
759
  const dispatch = useSummonDispatch();
736
- return (0, import_react11.useCallback)(() => dispatchClose(dispatch, overlayId, instanceId), [dispatch, overlayId, instanceId]);
760
+ return (0, import_react12.useCallback)(() => dispatchClose(dispatch, overlayId, instanceId), [dispatch, overlayId, instanceId]);
737
761
  }
738
762
  //# sourceMappingURL=react.cjs.map