@dxos/app-framework 0.8.4-main.d05539e30a → 0.8.4-main.d9fc60f731

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.
Files changed (177) hide show
  1. package/dist/lib/browser/{capability-K5XIVCQU.mjs → capability-JCUNWZ66.mjs} +7 -7
  2. package/dist/lib/browser/{chunk-5AHASNDW.mjs → chunk-2ZIYRDAF.mjs} +1 -1
  3. package/dist/lib/browser/{chunk-5AHASNDW.mjs.map → chunk-2ZIYRDAF.mjs.map} +1 -1
  4. package/dist/lib/browser/{chunk-QLML5QFJ.mjs → chunk-7MBI66M7.mjs} +12 -6
  5. package/dist/lib/browser/chunk-7MBI66M7.mjs.map +7 -0
  6. package/dist/lib/browser/{chunk-VJ5PFAWC.mjs → chunk-BMLCIFZW.mjs} +37 -16
  7. package/dist/lib/browser/chunk-BMLCIFZW.mjs.map +7 -0
  8. package/dist/lib/browser/{chunk-SLX73WRZ.mjs → chunk-DC3WRPBV.mjs} +14 -5
  9. package/dist/lib/browser/chunk-DC3WRPBV.mjs.map +7 -0
  10. package/dist/lib/browser/{chunk-BRK6GYNB.mjs → chunk-E5DTDNDY.mjs} +20 -19
  11. package/dist/lib/browser/chunk-E5DTDNDY.mjs.map +7 -0
  12. package/dist/lib/browser/{chunk-KLHQNYJ2.mjs → chunk-KZUDO43J.mjs} +2 -2
  13. package/dist/lib/browser/chunk-Q2GLJTVV.mjs +12 -0
  14. package/dist/lib/browser/chunk-Q2GLJTVV.mjs.map +7 -0
  15. package/dist/lib/browser/{chunk-UVTGHZQF.mjs → chunk-VBG5F5UF.mjs} +51 -88
  16. package/dist/lib/browser/chunk-VBG5F5UF.mjs.map +7 -0
  17. package/dist/lib/browser/cli/index.mjs +5 -5
  18. package/dist/lib/browser/common/activation-events.mjs +3 -3
  19. package/dist/lib/browser/common/capabilities.mjs +4 -4
  20. package/dist/lib/browser/core/plugin-manager.mjs +2 -2
  21. package/dist/lib/browser/core/plugin.mjs +3 -1
  22. package/dist/lib/browser/core/url-loader.mjs +2 -2
  23. package/dist/lib/browser/index.mjs +13 -14
  24. package/dist/lib/browser/index.mjs.map +3 -3
  25. package/dist/lib/browser/meta.json +1 -1
  26. package/dist/lib/browser/{process-manager-capability-JIWLN7SU.mjs → process-manager-capability-4FUA4TRP.mjs} +6 -6
  27. package/dist/lib/browser/testing/index.mjs +11 -10
  28. package/dist/lib/browser/testing/index.mjs.map +3 -3
  29. package/dist/lib/browser/testing/react.mjs +6 -6
  30. package/dist/lib/browser/ui/index.mjs +7 -9
  31. package/dist/lib/node-esm/{capability-RLKFFLTB.mjs → capability-7TCQK7ZP.mjs} +7 -7
  32. package/dist/lib/node-esm/{chunk-SFYCO3PT.mjs → chunk-4BX2AE47.mjs} +37 -16
  33. package/dist/lib/node-esm/chunk-4BX2AE47.mjs.map +7 -0
  34. package/dist/lib/node-esm/chunk-FPW45EZH.mjs +14 -0
  35. package/dist/lib/node-esm/chunk-FPW45EZH.mjs.map +7 -0
  36. package/dist/lib/node-esm/{chunk-LJNUFNDO.mjs → chunk-GEWHMHM7.mjs} +12 -6
  37. package/dist/lib/node-esm/chunk-GEWHMHM7.mjs.map +7 -0
  38. package/dist/lib/node-esm/{chunk-BYHYYJZH.mjs → chunk-KSH4OVMY.mjs} +20 -19
  39. package/dist/lib/node-esm/chunk-KSH4OVMY.mjs.map +7 -0
  40. package/dist/lib/node-esm/{chunk-PW2VYGOS.mjs → chunk-QBUGLR4H.mjs} +1 -1
  41. package/dist/lib/node-esm/{chunk-PW2VYGOS.mjs.map → chunk-QBUGLR4H.mjs.map} +1 -1
  42. package/dist/lib/node-esm/{chunk-42UNAKYO.mjs → chunk-XGBUKRCK.mjs} +2 -2
  43. package/dist/lib/node-esm/{chunk-6S45OMUP.mjs → chunk-XIGPWXCQ.mjs} +14 -5
  44. package/dist/lib/node-esm/chunk-XIGPWXCQ.mjs.map +7 -0
  45. package/dist/lib/node-esm/{chunk-JNT72ZCN.mjs → chunk-Y7JKXNDU.mjs} +51 -88
  46. package/dist/lib/node-esm/chunk-Y7JKXNDU.mjs.map +7 -0
  47. package/dist/lib/node-esm/cli/index.mjs +5 -5
  48. package/dist/lib/node-esm/common/activation-events.mjs +3 -3
  49. package/dist/lib/node-esm/common/capabilities.mjs +4 -4
  50. package/dist/lib/node-esm/core/plugin-manager.mjs +2 -2
  51. package/dist/lib/node-esm/core/plugin.mjs +3 -1
  52. package/dist/lib/node-esm/core/url-loader.mjs +2 -2
  53. package/dist/lib/node-esm/index.mjs +13 -14
  54. package/dist/lib/node-esm/index.mjs.map +3 -3
  55. package/dist/lib/node-esm/meta.json +1 -1
  56. package/dist/lib/node-esm/{process-manager-capability-PHKLO2BL.mjs → process-manager-capability-F23D62RN.mjs} +6 -6
  57. package/dist/lib/node-esm/testing/index.mjs +11 -10
  58. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  59. package/dist/lib/node-esm/testing/react.mjs +6 -6
  60. package/dist/lib/node-esm/ui/index.mjs +7 -9
  61. package/dist/plugin/node-esm/index.mjs +155 -77
  62. package/dist/plugin/node-esm/index.mjs.map +4 -4
  63. package/dist/plugin/node-esm/meta.json +1 -1
  64. package/dist/types/src/common/capabilities.d.ts +1 -1
  65. package/dist/types/src/common/index.d.ts +0 -1
  66. package/dist/types/src/common/index.d.ts.map +1 -1
  67. package/dist/types/src/common/translations.d.ts +1 -1
  68. package/dist/types/src/core/capability-manager.d.ts +4 -0
  69. package/dist/types/src/core/capability-manager.d.ts.map +1 -1
  70. package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
  71. package/dist/types/src/core/plugin.d.ts +33 -6
  72. package/dist/types/src/core/plugin.d.ts.map +1 -1
  73. package/dist/types/src/plugin-process-manager/history/history-tracker.d.ts +15 -1
  74. package/dist/types/src/plugin-process-manager/history/history-tracker.d.ts.map +1 -1
  75. package/dist/types/src/plugin-process-manager/history/undo-mapping.d.ts +1 -1
  76. package/dist/types/src/plugin-process-manager/history/undo-mapping.d.ts.map +1 -1
  77. package/dist/types/src/plugin-process-manager/meta.d.ts +1 -1
  78. package/dist/types/src/plugin-process-manager/meta.d.ts.map +1 -1
  79. package/dist/types/src/plugin-process-manager/testing.d.ts.map +1 -1
  80. package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
  81. package/dist/types/src/ui/components/App/App.d.ts +34 -2
  82. package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
  83. package/dist/types/src/ui/components/App/App.stories.d.ts.map +1 -1
  84. package/dist/types/src/ui/components/PluginManager/PluginManagerContext.stories.d.ts.map +1 -1
  85. package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
  86. package/dist/types/src/ui/components/index.d.ts +0 -1
  87. package/dist/types/src/ui/components/index.d.ts.map +1 -1
  88. package/dist/types/src/ui/hooks/useApp.d.ts +1 -7
  89. package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
  90. package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
  91. package/dist/types/src/ui/hooks/useProcessManagerRuntime.d.ts +6 -0
  92. package/dist/types/src/ui/hooks/useProcessManagerRuntime.d.ts.map +1 -1
  93. package/dist/types/src/vite-plugin/boot-loader/loader.d.ts +26 -23
  94. package/dist/types/src/vite-plugin/boot-loader/loader.d.ts.map +1 -1
  95. package/dist/types/src/vite-plugin/manifest.d.ts.map +1 -1
  96. package/dist/types/tsconfig.tsbuildinfo +1 -1
  97. package/package.json +40 -35
  98. package/src/common/capabilities.ts +1 -1
  99. package/src/common/index.ts +0 -1
  100. package/src/common/translations.ts +1 -1
  101. package/src/core/capability-manager.test.ts +3 -0
  102. package/src/core/capability-manager.ts +22 -1
  103. package/src/core/plugin-manager.test.ts +51 -21
  104. package/src/core/plugin-manager.ts +10 -5
  105. package/src/core/plugin.ts +44 -8
  106. package/src/core/url-loader.test.ts +2 -1
  107. package/src/plugin-process-manager/history/history-tracker.test.ts +39 -60
  108. package/src/plugin-process-manager/history/history-tracker.ts +32 -16
  109. package/src/plugin-process-manager/history/undo-mapping.ts +1 -1
  110. package/src/plugin-process-manager/history/undo-registry.test.ts +3 -1
  111. package/src/plugin-process-manager/meta.ts +6 -4
  112. package/src/plugin-process-manager/testing.ts +6 -5
  113. package/src/testing/react.test.tsx +3 -1
  114. package/src/testing/withPluginManager.tsx +4 -3
  115. package/src/ui/components/App/App.stories.tsx +9 -11
  116. package/src/ui/components/App/App.tsx +80 -17
  117. package/src/ui/components/PluginManager/PluginManagerContext.stories.tsx +9 -14
  118. package/src/ui/components/Surface/SurfaceComponent.stories.tsx +1 -1
  119. package/src/ui/components/Surface/SurfaceComponent.tsx +1 -1
  120. package/src/ui/components/Surface/types.test.ts +2 -2
  121. package/src/ui/components/Surface/types.ts +21 -0
  122. package/src/ui/components/index.ts +0 -1
  123. package/src/ui/hooks/useApp.test.tsx +3 -1
  124. package/src/ui/hooks/useApp.tsx +4 -18
  125. package/src/ui/hooks/useCapabilities.ts +1 -0
  126. package/src/ui/hooks/useProcessManagerRuntime.ts +12 -2
  127. package/src/vite-plugin/boot-loader/BootLoader.solid-stories.tsx +198 -0
  128. package/src/vite-plugin/boot-loader/loader-app/Loader.tsx +166 -0
  129. package/src/vite-plugin/boot-loader/loader-app/boot-loader.css +311 -0
  130. package/src/vite-plugin/boot-loader/loader-app/bridge.ts +25 -0
  131. package/src/vite-plugin/boot-loader/loader-app/entry.tsx +21 -0
  132. package/src/vite-plugin/boot-loader/loader-app/mount.tsx +70 -0
  133. package/src/vite-plugin/boot-loader/loader-app/store.test.ts +137 -0
  134. package/src/vite-plugin/boot-loader/loader-app/store.ts +149 -0
  135. package/src/vite-plugin/boot-loader/loader-app/tsconfig.json +11 -0
  136. package/src/vite-plugin/boot-loader/loader-app/types.ts +78 -0
  137. package/src/vite-plugin/boot-loader/loader.ts +133 -63
  138. package/src/vite-plugin/manifest.test.ts +19 -10
  139. package/src/vite-plugin/manifest.ts +9 -3
  140. package/tsconfig.json +6 -7
  141. package/dist/lib/browser/chunk-BRK6GYNB.mjs.map +0 -7
  142. package/dist/lib/browser/chunk-KFDF7KR3.mjs +0 -42
  143. package/dist/lib/browser/chunk-KFDF7KR3.mjs.map +0 -7
  144. package/dist/lib/browser/chunk-QLML5QFJ.mjs.map +0 -7
  145. package/dist/lib/browser/chunk-SLX73WRZ.mjs.map +0 -7
  146. package/dist/lib/browser/chunk-UVTGHZQF.mjs.map +0 -7
  147. package/dist/lib/browser/chunk-VJ5PFAWC.mjs.map +0 -7
  148. package/dist/lib/node-esm/chunk-6S45OMUP.mjs.map +0 -7
  149. package/dist/lib/node-esm/chunk-BYHYYJZH.mjs.map +0 -7
  150. package/dist/lib/node-esm/chunk-JNT72ZCN.mjs.map +0 -7
  151. package/dist/lib/node-esm/chunk-LJNUFNDO.mjs.map +0 -7
  152. package/dist/lib/node-esm/chunk-OUEMWPIW.mjs +0 -43
  153. package/dist/lib/node-esm/chunk-OUEMWPIW.mjs.map +0 -7
  154. package/dist/lib/node-esm/chunk-SFYCO3PT.mjs.map +0 -7
  155. package/dist/types/src/common/operations.d.ts +0 -19
  156. package/dist/types/src/common/operations.d.ts.map +0 -1
  157. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts +0 -64
  158. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts.map +0 -1
  159. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts +0 -19
  160. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts.map +0 -1
  161. package/dist/types/src/ui/components/Placeholder/index.d.ts +0 -2
  162. package/dist/types/src/ui/components/Placeholder/index.d.ts.map +0 -1
  163. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts +0 -34
  164. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts.map +0 -1
  165. package/src/common/operations.ts +0 -35
  166. package/src/ui/components/Placeholder/Placeholder.stories.tsx +0 -77
  167. package/src/ui/components/Placeholder/Placeholder.tsx +0 -155
  168. package/src/ui/components/Placeholder/index.ts +0 -5
  169. package/src/vite-plugin/boot-loader/BootLoader.stories.tsx +0 -270
  170. package/src/vite-plugin/boot-loader/boot-loader.css +0 -320
  171. package/src/vite-plugin/boot-loader/boot-loader.js +0 -325
  172. /package/dist/lib/browser/{capability-K5XIVCQU.mjs.map → capability-JCUNWZ66.mjs.map} +0 -0
  173. /package/dist/lib/browser/{chunk-KLHQNYJ2.mjs.map → chunk-KZUDO43J.mjs.map} +0 -0
  174. /package/dist/lib/browser/{process-manager-capability-JIWLN7SU.mjs.map → process-manager-capability-4FUA4TRP.mjs.map} +0 -0
  175. /package/dist/lib/node-esm/{capability-RLKFFLTB.mjs.map → capability-7TCQK7ZP.mjs.map} +0 -0
  176. /package/dist/lib/node-esm/{chunk-42UNAKYO.mjs.map → chunk-XGBUKRCK.mjs.map} +0 -0
  177. /package/dist/lib/node-esm/{process-manager-capability-PHKLO2BL.mjs.map → process-manager-capability-F23D62RN.mjs.map} +0 -0
@@ -0,0 +1,149 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Accessor, createSignal } from 'solid-js';
6
+
7
+ import { type StatusPayload } from './types';
8
+
9
+ //
10
+ // Creep tuning. The ring auto-creeps toward a moving ceiling so a long
11
+ // activation silence never reads as a frozen disc. See the original
12
+ // hand-written driver for the rationale behind each constant.
13
+ //
14
+
15
+ /** Creep timer cadence, in milliseconds. */
16
+ export const CREEP_TICK_MS = 100;
17
+ /** State-1 ease rate — a gentle "we're alive" hint before real progress lands. */
18
+ export const STATE_1_RATE = 0.05;
19
+ /** State-1 asymptote (percent) the creep eases toward before host progress. */
20
+ export const STATE_1_ASYMPTOTE = 40;
21
+ /** State-2 ease rate — bridges the gap between sparse host `progress()` calls. */
22
+ export const STATE_2_RATE = 0.05;
23
+ /** State-2 lead (percent) the ceiling sits ahead of the last host value. */
24
+ export const STATE_2_BUMP = 15;
25
+ /** Hard ceiling the auto-creep never crosses (host must drive the rest). */
26
+ export const ABSOLUTE_CEILING = 90;
27
+
28
+ /** Loader lifecycle phase. */
29
+ export type Phase = 'creep' | 'host' | 'dismissing';
30
+
31
+ /** A single rendered status line; `id` keys the `<For>` and survives in-place range ticks. */
32
+ export type StatusLine = {
33
+ id: number;
34
+ text: string;
35
+ event?: string;
36
+ module?: string;
37
+ };
38
+
39
+ /**
40
+ * Asymptotic ease — `next = raw + (ceiling - raw) * rate`, clamped so it never
41
+ * overshoots (or regresses past) the ceiling. Pure; unit-tested.
42
+ */
43
+ export const easeToward = (raw: number, ceiling: number, rate: number): number =>
44
+ raw >= ceiling - 0.1 ? raw : raw + (ceiling - raw) * rate;
45
+
46
+ /**
47
+ * Clamp a host-supplied fraction to a percent in [0, 100]. Invalid / negative /
48
+ * non-finite values collapse to 0 rather than poisoning the CSS var. Pure;
49
+ * unit-tested.
50
+ */
51
+ export const clampPercent = (fraction?: number): number =>
52
+ typeof fraction !== 'number' || !Number.isFinite(fraction) || fraction < 0 ? 0 : Math.min(1, fraction) * 100;
53
+
54
+ /** Display text for a payload — appends the `(index/total)` suffix when a range is present. */
55
+ export const displayText = (payload: StatusPayload): string =>
56
+ payload.range ? `${payload.humanized} (${payload.range.index}/${payload.range.total})` : payload.humanized;
57
+
58
+ /**
59
+ * Reactive loader store — owns the progress percent, the status-line log, and
60
+ * the lifecycle phase, plus the auto-creep timer. All DOM-free, so the creep
61
+ * math and status reduction are testable as plain functions; `Loader.tsx` binds
62
+ * the accessors to the DOM and `bridge.ts` wraps the mutators as the imperative
63
+ * `window.__bootLoader` facade.
64
+ */
65
+ export type LoaderStore = {
66
+ /** Progress as a percent in [0, 100] (the CSS `--boot-loader-bar-progress` value). */
67
+ progress: Accessor<number>;
68
+ /** Appended status lines, newest last. */
69
+ lines: Accessor<StatusLine[]>;
70
+ /** Current lifecycle phase. */
71
+ phase: Accessor<Phase>;
72
+ /** Apply a status update (append, or replace-in-place for range ticks). */
73
+ pushStatus: (payload: StatusPayload) => void;
74
+ /** Enter host-driven progress with `fraction` ∈ [0, 1]; never regresses. */
75
+ setProgress: (fraction?: number) => void;
76
+ /** Snap to 100%, stop the creep, and enter the dismissing phase. */
77
+ ready: () => void;
78
+ /** Stop the creep timer (call on teardown). */
79
+ dispose: () => void;
80
+ };
81
+
82
+ export const createLoaderStore = (initialStatus?: string): LoaderStore => {
83
+ const [progress, setProgressPct] = createSignal(0);
84
+ const [lines, setLines] = createSignal<StatusLine[]>(initialStatus ? [{ id: 0, text: initialStatus }] : []);
85
+ const [phase, setPhase] = createSignal<Phase>('creep');
86
+
87
+ let creepCeiling = STATE_1_ASYMPTOTE;
88
+ let creepRate = STATE_1_RATE;
89
+ let nextId = initialStatus ? 1 : 0;
90
+ let timer: ReturnType<typeof setInterval> | null = null;
91
+
92
+ const startCreep = (): void => {
93
+ if (timer == null) {
94
+ timer = setInterval(() => setProgressPct((raw) => easeToward(raw, creepCeiling, creepRate)), CREEP_TICK_MS);
95
+ }
96
+ };
97
+
98
+ const stopCreep = (): void => {
99
+ if (timer != null) {
100
+ clearInterval(timer);
101
+ timer = null;
102
+ }
103
+ };
104
+
105
+ const pushStatus = (payload: StatusPayload): void => {
106
+ const text = displayText(payload);
107
+ setLines((current) => {
108
+ const previous = current.at(-1);
109
+ // Dedup back-to-back identical transitions (same text + structured fields).
110
+ if (
111
+ previous &&
112
+ previous.text === text &&
113
+ (previous.event ?? null) === (payload.event ?? null) &&
114
+ (previous.module ?? null) === (payload.module ?? null)
115
+ ) {
116
+ return current;
117
+ }
118
+ // Range tick — replace the current line in place rather than appending,
119
+ // so a counted phase produces one entry with an updating suffix.
120
+ if (payload.range && previous) {
121
+ return [...current.slice(0, -1), { ...previous, text }];
122
+ }
123
+ return [...current, { id: nextId++, text, event: payload.event, module: payload.module }];
124
+ });
125
+ };
126
+
127
+ const setProgress = (fraction?: number): void => {
128
+ setPhase('host');
129
+ const pct = clampPercent(fraction);
130
+ // The ring never regresses: hold the current value if the host reports lower.
131
+ setProgressPct((current) => Math.max(current, pct));
132
+ // Switch the creep to its state-2 cadence and lead the host value by a bump.
133
+ creepRate = STATE_2_RATE;
134
+ creepCeiling = Math.min(Math.max(creepCeiling, pct + STATE_2_BUMP), ABSOLUTE_CEILING);
135
+ startCreep();
136
+ };
137
+
138
+ const ready = (): void => {
139
+ stopCreep();
140
+ setProgressPct(100);
141
+ setPhase('dismissing');
142
+ };
143
+
144
+ const dispose = (): void => stopCreep();
145
+
146
+ startCreep();
147
+
148
+ return { progress, lines, phase, pushStatus, setProgress, ready, dispose };
149
+ };
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../../../../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "jsx": "preserve",
5
+ "jsxImportSource": "solid-js",
6
+ "strict": false,
7
+ "noEmit": true,
8
+ "composite": false
9
+ },
10
+ "include": ["."]
11
+ }
@@ -0,0 +1,78 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ /**
6
+ * Status payload accepted by `window.__bootLoader.status(...)`. The caller owns
7
+ * formatting — `humanized` is the exact text rendered — while `event` / `module`
8
+ * carry the structured activation ids for the boot trace, and `range` drives an
9
+ * in-place `(index/total)` counter for long counted phases.
10
+ */
11
+ export type StatusPayload = {
12
+ /**
13
+ * Raw activation event key (e.g. `dxos.org/plugin/observability/activate`)
14
+ * when the transition is event-level.
15
+ */
16
+ event?: string;
17
+
18
+ /**
19
+ * Raw module id (e.g. `org.dxos.plugin.observability.module.ReactSurface`)
20
+ * when the transition is module-level.
21
+ */
22
+ module?: string;
23
+
24
+ /** Exact text to display (e.g. "Activating Observability: react-surface"). */
25
+ humanized: string;
26
+
27
+ /**
28
+ * Optional `(index/total)` tick. When present the loader replaces the current
29
+ * line in place ("Loading plugins (12/80)") instead of appending a new entry —
30
+ * keeps the visible log compact during long counted phases.
31
+ */
32
+ range?: { index: number; total: number };
33
+ };
34
+
35
+ /**
36
+ * Imperative facade exposed on `window.__bootLoader`, installed by the inlined
37
+ * loader bundle and driven by the host app (the React relay forwards `useApp`'s
38
+ * startup progress through it).
39
+ */
40
+ export type BootLoaderApi = {
41
+ /** Update the visible status line. */
42
+ status: (payload: StatusPayload) => void;
43
+ /** Enter host-driven progress — `fraction` ∈ [0, 1]. */
44
+ progress: (fraction?: number) => void;
45
+ /** Play the dismissal outro, then remove the loader DOM (graceful path). */
46
+ ready: () => void;
47
+ /** Remove the loader DOM immediately (fast-load backstop / terminal). */
48
+ dismiss: () => void;
49
+ };
50
+
51
+ /**
52
+ * Config baked into `index.html` by `bootLoaderPlugin` ahead of the loader
53
+ * bundle, so the compiled app stays static (compiled once, independent of the
54
+ * host's brand mark / initial status).
55
+ */
56
+ export type BootLoaderConfig = {
57
+ /**
58
+ * Id of the static backdrop element the plugin injects and the loader mounts
59
+ * into — the single coupling between `transformIndexHtml` and the app. The
60
+ * plugin owns the authoritative value and passes it here so `entry.tsx` reads
61
+ * it rather than hardcoding (the CSS selector mirrors it — see `boot-loader.css`).
62
+ */
63
+ rootId?: string;
64
+ /** Inline SVG markup for the brand mark rendered inside the ring. */
65
+ markSvg?: string;
66
+ /** Initial status text rendered before the host fires its first `status(...)`. */
67
+ status?: string;
68
+ };
69
+
70
+ /** Fallback backdrop id when no config is present (kept in sync with `loader.ts` + the CSS). */
71
+ export const DEFAULT_ROOT_ID = 'boot-loader';
72
+
73
+ declare global {
74
+ interface Window {
75
+ __bootLoader?: BootLoaderApi;
76
+ __BOOT_LOADER_CONFIG__?: BootLoaderConfig;
77
+ }
78
+ }
@@ -2,10 +2,28 @@
2
2
  // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
+ // @ts-ignore — @types/babel__core present transitively but not declared here.
6
+ import * as babel from '@babel/core';
7
+ // @ts-ignore — @babel/preset-typescript ships no .d.ts.
8
+ import babelPresetTypeScript from '@babel/preset-typescript';
9
+ // @ts-ignore — babel-preset-solid ships no .d.ts.
10
+ import babelPresetSolid from 'babel-preset-solid';
11
+ import { build, type Plugin as EsbuildPlugin } from 'esbuild';
12
+ import { dirname } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
5
14
  import { type Plugin } from 'vite';
6
15
 
7
- import css from './boot-loader.css?raw';
8
- import driverScript from './boot-loader.js?raw';
16
+ import css from './loader-app/boot-loader.css?raw';
17
+ // The loader-app Solid sources are inlined as raw text (never parsed by
18
+ // dx-compile — app-framework is a React package, so its build can't compile
19
+ // Solid JSX). They are compiled to a self-contained browser IIFE at the
20
+ // consuming app's build time by {@link compileLoaderBundle} below.
21
+ import bridgeSrc from './loader-app/bridge.ts?raw';
22
+ import entrySrc from './loader-app/entry.tsx?raw';
23
+ import loaderSrc from './loader-app/Loader.tsx?raw';
24
+ import mountSrc from './loader-app/mount.tsx?raw';
25
+ import storeSrc from './loader-app/store.ts?raw';
26
+ import typesSrc from './loader-app/types.ts?raw';
9
27
 
10
28
  /**
11
29
  * Options for {@link bootLoaderPlugin}.
@@ -18,56 +36,130 @@ export type BootLoaderOptions = {
18
36
  status?: string;
19
37
 
20
38
  /**
21
- * Inline SVG markup for an optional brand mark rendered above the progress bar.
22
- * The mark renders at full opacity with whatever fills the SVG declares, so
23
- * pass a colour-palette logo for the strongest visual identity. SVGs that use
24
- * `fill="currentColor"` still inherit the loader's `prefers-color-scheme`
25
- * text colour. Leave empty/undefined to render only the bar.
39
+ * Inline SVG markup for an optional brand mark rendered inside the progress
40
+ * ring. The mark renders at full opacity with whatever fills the SVG declares,
41
+ * so pass a colour-palette logo for the strongest visual identity. SVGs that
42
+ * use `fill="currentColor"` still inherit the loader's `prefers-color-scheme`
43
+ * text colour. Leave empty/undefined to render only the ring.
26
44
  */
27
45
  markSvg?: string;
28
46
  };
29
47
 
48
+ const VIRTUAL_NS = 'boot-loader-app';
49
+
50
+ // Relative-import graph of the loader-app, keyed by module name (no extension).
51
+ // `tsx` modules carry JSX (compiled via `babel-preset-solid`); `ts` modules are
52
+ // type-stripped only. `solid-js` imports introduced by the Solid transform
53
+ // resolve from `node_modules` against `resolveDir`.
54
+ const MODULES: Record<string, { contents: string; tsx: boolean }> = {
55
+ entry: { contents: entrySrc, tsx: true },
56
+ mount: { contents: mountSrc, tsx: true },
57
+ Loader: { contents: loaderSrc, tsx: true },
58
+ bridge: { contents: bridgeSrc, tsx: false },
59
+ store: { contents: storeSrc, tsx: false },
60
+ types: { contents: typesSrc, tsx: false },
61
+ };
62
+
63
+ // Walk up from the compiled plugin to find a `node_modules` containing
64
+ // `solid-js` (a dependency of this package). Used as esbuild's `resolveDir`.
65
+ const resolveDir = dirname(fileURLToPath(import.meta.url));
66
+
67
+ /**
68
+ * Compile the loader-app from its inlined raw sources into a single
69
+ * self-contained IIFE (Solid runtime bundled in) suitable for inlining into
70
+ * `index.html`. Mirrors dx-compile's Solid pipeline: `@babel/preset-typescript`
71
+ * strips types, then `babel-preset-solid` compiles JSX into reactive primitives.
72
+ */
73
+ const compileLoaderBundle = async (): Promise<string> => {
74
+ const virtualSolid = (): EsbuildPlugin => ({
75
+ name: 'boot-loader-app-virtual',
76
+ setup: (esbuild) => {
77
+ esbuild.onResolve({ filter: /^\.\// }, (args) => {
78
+ const name = args.path.replace(/^\.\//, '').replace(/\.(tsx?|jsx?)$/, '');
79
+ return name in MODULES ? { path: name, namespace: VIRTUAL_NS } : undefined;
80
+ });
81
+
82
+ esbuild.onLoad({ filter: /.*/, namespace: VIRTUAL_NS }, async (args) => {
83
+ const module = MODULES[args.path];
84
+ const result = await babel.transformAsync(module.contents, {
85
+ filename: `${args.path}.${module.tsx ? 'tsx' : 'ts'}`,
86
+ babelrc: false,
87
+ configFile: false,
88
+ // Presets run last-to-first: TS strips first, then Solid sees plain JSX.
89
+ presets: [
90
+ [babelPresetSolid, { generate: 'dom', hydratable: false }],
91
+ [babelPresetTypeScript, { allowDeclareFields: true }],
92
+ ],
93
+ });
94
+ if (!result?.code) {
95
+ throw new Error(`babel-preset-solid returned no code for ${args.path}`);
96
+ }
97
+ return { contents: result.code, loader: 'js', resolveDir };
98
+ });
99
+ },
100
+ });
101
+
102
+ const result = await build({
103
+ stdin: { contents: "import './entry';", loader: 'ts', resolveDir, sourcefile: 'boot-loader-entry.ts' },
104
+ bundle: true,
105
+ format: 'iife',
106
+ platform: 'browser',
107
+ target: 'es2020',
108
+ minify: true,
109
+ write: false,
110
+ legalComments: 'none',
111
+ define: { 'process.env.NODE_ENV': '"production"' },
112
+ plugins: [virtualSolid()],
113
+ });
114
+
115
+ return result.outputFiles[0].text;
116
+ };
117
+
118
+ let bundlePromise: Promise<string> | undefined;
119
+ const getLoaderBundle = (): Promise<string> => (bundlePromise ??= compileLoaderBundle());
120
+
30
121
  /**
31
- * Vite plugin that injects a tiny native-DOM "boot loader" into the host app's
122
+ * Vite plugin that injects a Solid "boot loader" app into the host app's
32
123
  * `index.html`.
33
124
  *
34
- * The loader paints on the very first frame, before any JS bundle is fetched —
35
- * so the user sees something immediately on cold load instead of staring at a
36
- * blank document while the compiled module graph is parsed. The brand mark
37
- * sits centred inside a thin determinate progress ring; both layers are
38
- * composited so the ring's fill stays visually smooth even while the JS main
39
- * thread is busy.
125
+ * The full-screen backdrop paints on the very first frame from an inline
126
+ * `<style>` (before any JS bundle is fetched), so the user sees the loading
127
+ * surface immediately on cold load instead of a blank document. A compiled,
128
+ * self-contained Solid IIFE inlined ahead of the app bundle — then renders
129
+ * the determinate progress ring + status log into that backdrop and exposes the
130
+ * host-facing driver on `window.__bootLoader`.
40
131
  *
41
- * The host app drives the loader from `main.tsx`:
42
- * - `window.__bootLoader.status(text)` updates the status line per phase.
132
+ * The host app drives the loader (the React relay forwards `useApp`'s startup
133
+ * progress):
134
+ * - `window.__bootLoader.status(payload)` updates the status line per phase.
43
135
  * - `window.__bootLoader.progress(fraction)` grows the determinate ring (0–1).
44
- * - `window.__bootLoader.dismiss()` removes the loader once React has mounted.
136
+ * - `window.__bootLoader.ready()` plays the dismissal outro, then self-removes.
137
+ * - `window.__bootLoader.dismiss()` removes the loader immediately (fast path).
45
138
  *
46
- * Inject order:
139
+ * Inject order (all at the start of `<body>`, except the stylesheet):
47
140
  * - `<style>` → `<head>` (parses before any bundled stylesheet).
48
- * - `<div id="boot-loader">…</div>` → start of `<body>` (sibling of `#root`).
49
- * - inline `<script>` defining `window.__bootLoader` → start of `<body>`.
141
+ * - `<div id="boot-loader">` → the static backdrop the CSS styles instantly.
142
+ * - inline `<script>` config (`window.__BOOT_LOADER_CONFIG__`)before the bundle.
143
+ * - inline `<script>` loader bundle → renders the Solid app into the backdrop.
50
144
  *
51
- * Keeping the loader as a sibling of `#root` (rather than a child of it) means
145
+ * Keeping the loader a sibling of `#root` (rather than a child) means
52
146
  * `createRoot(document.getElementById('root')).render(...)` does not fight the
53
- * loader for ownership; the host explicitly calls `dismiss()` after the first
54
- * React commit, which gives a deterministic handoff.
147
+ * loader for ownership; the framework explicitly dismisses after the first
148
+ * React commit for a deterministic handoff.
55
149
  *
56
150
  * Color tokens are exposed as CSS custom properties (`--boot-loader-bg-light`,
57
- * `--boot-loader-bg-dark`, etc.) defined in `boot-loader.css`, so consumers can
58
- * override them at the document level without us re-parameterizing this plugin.
151
+ * etc.) in `boot-loader.css`, so consumers can override them at the document
152
+ * level without re-parameterizing this plugin.
59
153
  */
60
154
  export const bootLoaderPlugin = ({ status, markSvg }: BootLoaderOptions = {}): Plugin => {
61
155
  return {
62
156
  name: 'app-framework:boot-loader',
63
- transformIndexHtml() {
64
- // The mark slot uses `children: <raw-svg>` (vite emits string children as
65
- // raw HTML, not text-escaped) so the host can pass an SVG file's contents
66
- // straight through. The mark and ring are siblings inside `#boot-loader-disc`
67
- // so CSS grid-stacking can centre the mark inside the ring without the ring's
68
- // radial-gradient mask clipping the SVG.
69
- const markChildren = markSvg ? [{ tag: 'div', attrs: { id: 'boot-loader-mark' }, children: markSvg }] : [];
70
-
157
+ async transformIndexHtml() {
158
+ const bundle = await getLoaderBundle();
159
+ // Authoritative backdrop id: injected here, passed to the bundle via
160
+ // config (so `entry.tsx` reads it), and mirrored by the CSS selector in
161
+ // `boot-loader.css`. The inner DOM CSS contract is guarded by the story.
162
+ const rootId = 'boot-loader';
71
163
  return [
72
164
  {
73
165
  tag: 'style',
@@ -78,44 +170,22 @@ export const bootLoaderPlugin = ({ status, markSvg }: BootLoaderOptions = {}): P
78
170
  tag: 'div',
79
171
  injectTo: 'body-prepend',
80
172
  attrs: {
81
- id: 'boot-loader',
173
+ id: rootId,
82
174
  role: 'status',
83
175
  'aria-live': 'polite',
84
176
  'aria-label': 'Initializing',
85
177
  },
86
- children: [
87
- {
88
- tag: 'div',
89
- attrs: { id: 'boot-loader-disc' },
90
- children: [
91
- { tag: 'div', attrs: { id: 'boot-loader-bar' }, children: '' },
92
- { tag: 'div', attrs: { id: 'boot-loader-dot' }, children: '' },
93
- ...markChildren,
94
- ],
95
- },
96
- {
97
- tag: 'div',
98
- attrs: { id: 'boot-loader-status' },
99
- children: [
100
- {
101
- tag: 'div',
102
- attrs: { id: 'boot-loader-status-fade' },
103
- },
104
- {
105
- tag: 'div',
106
- attrs: { id: 'boot-loader-status-track' },
107
- children: status
108
- ? [{ tag: 'div', attrs: { class: 'boot-loader-status-line' }, children: status }]
109
- : [],
110
- },
111
- ],
112
- },
113
- ],
178
+ children: '',
179
+ },
180
+ {
181
+ tag: 'script',
182
+ injectTo: 'body-prepend',
183
+ children: `window.__BOOT_LOADER_CONFIG__=${JSON.stringify({ rootId, markSvg, status })};`,
114
184
  },
115
185
  {
116
186
  tag: 'script',
117
187
  injectTo: 'body-prepend',
118
- children: driverScript,
188
+ children: bundle,
119
189
  },
120
190
  ];
121
191
  },
@@ -4,12 +4,22 @@
4
4
 
5
5
  import { describe, test } from 'vitest';
6
6
 
7
- import { serializeManifest } from './manifest';
7
+ import { DXN } from '@dxos/keys';
8
+
9
+ import { Plugin } from '../core';
10
+ import { type BuildMeta, serializeManifest } from './manifest';
11
+
12
+ // Build a `BuildMeta` (runtime `Meta` plus the build-time `version`, which may be a
13
+ // non-semver dev tag and so lives outside the `key` DXN).
14
+ const buildMeta = (options: Plugin.MakeMetaOptions, version: string): BuildMeta => ({
15
+ ...Plugin.makeMeta(options),
16
+ version,
17
+ });
8
18
 
9
19
  describe('serializeManifest', () => {
10
20
  test('serializes plugin meta plus assets', ({ expect }) => {
11
21
  const json = serializeManifest(
12
- { id: 'org.example.plugin', name: 'Example', description: 'Demo', tags: ['new'], version: '1.2.3' },
22
+ buildMeta({ key: DXN.make('org.example.plugin'), name: 'Example', description: 'Demo', tags: ['new'] }, '1.2.3'),
13
23
  { assets: ['index.mjs', 'style.css', 'chunks/lib-abc.js'] },
14
24
  );
15
25
  expect(JSON.parse(json)).toEqual({
@@ -23,10 +33,10 @@ describe('serializeManifest', () => {
23
33
  });
24
34
 
25
35
  test('includes devEntry when supplied (dev-server manifest)', ({ expect }) => {
26
- const json = serializeManifest(
27
- { id: 'org.example.plugin', name: 'Example', version: '0.0.0-dev' },
28
- { assets: [], devEntry: 'src/plugin.tsx' },
29
- );
36
+ const json = serializeManifest(buildMeta({ key: DXN.make('org.example.plugin'), name: 'Example' }, '0.0.0-dev'), {
37
+ assets: [],
38
+ devEntry: 'src/plugin.tsx',
39
+ });
30
40
  expect(JSON.parse(json)).toEqual({
31
41
  id: 'org.example.plugin',
32
42
  name: 'Example',
@@ -37,10 +47,9 @@ describe('serializeManifest', () => {
37
47
  });
38
48
 
39
49
  test('omits devEntry from the output when undefined', ({ expect }) => {
40
- const json = serializeManifest(
41
- { id: 'org.example.plugin', name: 'Example', version: '1.0.0' },
42
- { assets: ['index.mjs'] },
43
- );
50
+ const json = serializeManifest(buildMeta({ key: DXN.make('org.example.plugin'), name: 'Example' }, '1.0.0'), {
51
+ assets: ['index.mjs'],
52
+ });
44
53
  expect(Object.keys(JSON.parse(json))).not.toContain('devEntry');
45
54
  });
46
55
  });
@@ -45,13 +45,19 @@ export type BuildMeta = Plugin.Meta & { version: string };
45
45
  export const serializeManifest = (
46
46
  meta: BuildMeta,
47
47
  { assets, devEntry }: { assets: readonly string[]; devEntry?: string },
48
- ): string =>
49
- JSON.stringify(
48
+ ): string => {
49
+ // The published manifest is keyed by the bare NSID `id`; the `key` DXN is an
50
+ // in-process convenience and is intentionally omitted to keep the registry
51
+ // wire-format stable. `version` is the build-time package version (may be a
52
+ // non-semver dev tag), carried by `BuildMeta` rather than derived from `key`.
53
+ const { key: _key, ...rest } = meta;
54
+ return JSON.stringify(
50
55
  {
51
- ...meta,
56
+ ...rest,
52
57
  assets,
53
58
  ...(devEntry !== undefined ? { devEntry } : {}),
54
59
  },
55
60
  null,
56
61
  2,
57
62
  );
63
+ };
package/tsconfig.json CHANGED
@@ -2,7 +2,9 @@
2
2
  "extends": "../../../tsconfig.base.json",
3
3
  "compilerOptions": {},
4
4
  "exclude": [
5
- "/node_modules/"
5
+ "/node_modules/",
6
+ "src/**/*.solid-stories.tsx",
7
+ "src/vite-plugin/boot-loader/loader-app"
6
8
  ],
7
9
  "include": [
8
10
  "src"
@@ -26,6 +28,9 @@
26
28
  {
27
29
  "path": "../../common/invariant"
28
30
  },
31
+ {
32
+ "path": "../../common/keys"
33
+ },
29
34
  {
30
35
  "path": "../../common/log"
31
36
  },
@@ -53,15 +58,9 @@
53
58
  {
54
59
  "path": "../../core/compute/compute"
55
60
  },
56
- {
57
- "path": "../../core/compute/functions"
58
- },
59
61
  {
60
62
  "path": "../../core/compute/operation"
61
63
  },
62
- {
63
- "path": "../../core/echo/echo"
64
- },
65
64
  {
66
65
  "path": "../../core/echo/echo-db"
67
66
  },
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/plugin-process-manager/history/errors.ts", "../../../src/plugin-process-manager/history/undo-mapping.ts", "../../../src/plugin-process-manager/history/undo-registry.ts", "../../../src/plugin-process-manager/history/history-tracker.ts"],
4
- "sourcesContent": ["//\n// Copyright 2025 DXOS.org\n//\n\nimport { BaseError } from '@dxos/errors';\n\nexport class EmptyHistoryError extends BaseError.extend('EmptyHistoryError', 'Cannot undo: history is empty.') {}\n", "//\n// Copyright 2025 DXOS.org\n//\n\nimport type { Operation } from '@dxos/compute';\n\n/**\n * Label type for translatable text (canonical definition in @dxos/app-toolkit).\n */\ntype Label = string | [string, { ns: string; count?: number; defaultValue?: string }];\n\n/**\n * Extract the input type from an OperationDefinition.\n */\nexport type InputOf<T> = T extends Operation.Definition<infer I, any> ? I : never;\n\n/**\n * Extract the output type from an OperationDefinition.\n */\nexport type OutputOf<T> = T extends Operation.Definition<any, infer O> ? O : never;\n\n/**\n * Message provider for undo toast.\n * Can be a static Label or a function that derives the message from input/output.\n */\nexport type MessageProvider<Op extends Operation.Definition<any, any>> =\n | Label\n | ((input: InputOf<Op>, output: OutputOf<Op>) => Label);\n\n/**\n * Undo mapping that links a forward operation to its inverse.\n * Type parameters ensure deriveContext has correctly typed arguments.\n *\n * @template Op - The forward operation definition type.\n * @template Inv - The inverse operation definition type.\n */\nexport interface UndoMapping<\n Op extends Operation.Definition<any, any> = Operation.Definition<any, any>,\n Inv extends Operation.Definition<any, any> = Operation.Definition<any, any>,\n> {\n /** The forward operation. */\n readonly operation: Op;\n\n /** The inverse operation to invoke for undo. */\n readonly inverse: Inv;\n\n /**\n * Derives the input for the inverse operation from the forward operation's input and output.\n * @param input - The input that was passed to the forward operation.\n * @param output - The output that was returned by the forward operation.\n * @returns The input to pass to the inverse operation, or undefined to indicate the operation is not undoable.\n */\n readonly deriveContext: (input: InputOf<Op>, output: OutputOf<Op>) => InputOf<Inv> | undefined;\n\n /**\n * Optional message to show in the undo toast.\n * Can be a static Label or a function that derives the message from input/output.\n */\n readonly message?: MessageProvider<Op>;\n}\n\n/**\n * Props for creating an UndoMapping.\n */\nexport interface UndoMappingProps<\n Op extends Operation.Definition<any, any>,\n Inv extends Operation.Definition<any, any>,\n> {\n /** The forward operation. */\n operation: Op;\n\n /** The inverse operation to invoke for undo. */\n inverse: Inv;\n\n /**\n * Derives the input for the inverse operation from the forward operation's input and output.\n * Return undefined to indicate the operation is not undoable.\n */\n deriveContext: (input: InputOf<Op>, output: OutputOf<Op>) => InputOf<Inv> | undefined;\n\n /**\n * Optional message to show in the undo toast.\n * Can be a static Label or a function that derives the message from input/output.\n */\n message?: MessageProvider<Op>;\n}\n\n/**\n * Creates a type-safe undo mapping.\n *\n * @example\n * ```ts\n * // Static message\n * const mapping = UndoMapping.make({\n * operation: DeleteThread,\n * inverse: RestoreThread,\n * deriveContext: (input, output) => ({\n * thread: output.thread,\n * anchor: output.anchor,\n * }),\n * message: ['thread deleted label', { ns: '@dxos/plugin-thread' }],\n * });\n *\n * // Dynamic message based on input/output\n * const mapping = UndoMapping.make({\n * operation: RemoveObjects,\n * inverse: RestoreObjects,\n * deriveContext: (_input, output) => output,\n * message: (input, _output) =>\n * input.objects.length === 1\n * ? ['object deleted label', { ns: getTypename(input.objects[0]) }]\n * : ['objects deleted label', { ns: '@dxos/plugin-space' }],\n * });\n * ```\n */\nexport const make = <Op extends Operation.Definition<any, any>, Inv extends Operation.Definition<any, any>>(\n props: UndoMappingProps<Op, Inv>,\n): UndoMapping<Op, Inv> => props;\n\n/**\n * Resolves a message provider to a Label.\n */\nexport const resolveMessage = <Op extends Operation.Definition<any, any>>(\n message: MessageProvider<Op> | undefined,\n input: InputOf<Op>,\n output: OutputOf<Op>,\n): Label | undefined => {\n if (message === undefined) {\n return undefined;\n }\n if (typeof message === 'function') {\n return message(input, output);\n }\n return message;\n};\n", "//\n// Copyright 2025 DXOS.org\n//\n\nimport type { Operation } from '@dxos/compute';\n\nimport type * as UndoMapping from './undo-mapping';\n\n//\n// Public Interface\n//\n\n/**\n * Lookup result from UndoRegistry.\n */\nexport type UndoMappingResult = {\n inverse: Operation.Definition<any, any>;\n /** Returns undefined to indicate the operation is not undoable. */\n deriveContext: (input: any, output: any) => any | undefined;\n /** Message provider - may be a static Label or a function. */\n message?: UndoMapping.MessageProvider<Operation.Definition<any, any>>;\n};\n\n/**\n * UndoRegistry interface - looks up inverse operations.\n */\nexport interface UndoRegistry {\n lookup: (operation: Operation.Definition<any, any>) => UndoMappingResult | undefined;\n}\n\n//\n// Factory\n//\n\n/**\n * Creates an UndoRegistry that looks up inverse operations.\n */\nexport const make = (getMappings: () => UndoMapping.UndoMapping[]): UndoRegistry => {\n const lookup = (operation: Operation.Definition<any, any>): UndoMappingResult | undefined => {\n const mappings = getMappings();\n const mapping = mappings.find((m) => m.operation.meta.key === operation.meta.key);\n if (!mapping) {\n return undefined;\n }\n\n return {\n inverse: mapping.inverse,\n deriveContext: mapping.deriveContext,\n message: mapping.message,\n };\n };\n\n return { lookup };\n};\n", "//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport * as Stream from 'effect/Stream';\n\nimport { runAndForwardErrors } from '@dxos/effect';\nimport { log } from '@dxos/log';\nimport { OperationInvoker } from '@dxos/operation';\n\nimport { UndoOperation } from '../../common';\nimport { EmptyHistoryError } from './errors';\nimport type { HistoryEntry } from './types';\nimport { resolveMessage } from './undo-mapping';\nimport type { UndoRegistry } from './undo-registry';\n\nconst HISTORY_LIMIT = 100;\n\n//\n// Public Interface\n//\n\n/**\n * HistoryTracker interface - tracks operation history and provides undo.\n */\nexport interface HistoryTracker {\n undo: () => Effect.Effect<void, Error>;\n undoPromise: () => Promise<{ error?: Error }>;\n canUndo: () => boolean;\n}\n\n//\n// Factory\n//\n\n/**\n * Creates a HistoryTracker that subscribes to invocation events and provides undo.\n */\nexport const make = (\n invoker: OperationInvoker.OperationInvokerInternal,\n undoRegistry: UndoRegistry,\n): HistoryTracker => {\n const history: HistoryEntry[] = [];\n\n // Subscribe to invocation stream.\n const handleInvocation = (event: OperationInvoker.InvocationEvent) => {\n const mapping = undoRegistry.lookup(event.operation);\n if (!mapping) {\n // Operation is not undoable, skip.\n return;\n }\n\n const inverseInput = mapping.deriveContext(event.input, event.output);\n if (inverseInput === undefined) {\n // Operation is conditionally not undoable (deriveContext returned undefined).\n log('operation not undoable', { key: event.operation.meta.key });\n return;\n }\n\n const entry: HistoryEntry = {\n operation: event.operation,\n input: event.input,\n output: event.output,\n inverse: mapping.inverse,\n inverseInput,\n timestamp: event.timestamp,\n };\n\n history.push(entry);\n log('history entry added', { key: event.operation.meta.key, historyLength: history.length });\n\n // Trim history if it exceeds limit.\n if (history.length > HISTORY_LIMIT) {\n history.splice(0, history.length - HISTORY_LIMIT);\n }\n\n // Show undo toast (resolve message if it's a function).\n const resolvedMessage = resolveMessage(mapping.message, event.input, event.output);\n Effect.runFork(\n invoker.invoke(UndoOperation.ShowUndo, {\n message: resolvedMessage,\n }),\n );\n };\n\n // Fork a fiber to consume the invocation stream.\n Effect.runFork(\n Stream.fromPubSub(invoker.invocations).pipe(\n Stream.runForEach((event) => Effect.sync(() => handleInvocation(event))),\n ),\n );\n\n const undo = (): Effect.Effect<void, Error> => {\n return Effect.gen(function* () {\n const entry = history.pop();\n if (!entry) {\n return yield* Effect.fail(new EmptyHistoryError());\n }\n\n log('undoing operation', { key: entry.operation.meta.key, inverseKey: entry.inverse.meta.key });\n\n // Use _invokeCore to skip event emission (avoid undo-of-undo loops).\n yield* invoker._invokeCore(entry.inverse, entry.inverseInput);\n\n log('undo completed', { key: entry.operation.meta.key });\n });\n };\n\n const undoPromise = async (): Promise<{ error?: Error }> => {\n return runAndForwardErrors(undo())\n .then(() => ({}))\n .catch((error) => {\n log.catch(error);\n return { error };\n });\n };\n\n const canUndo = (): boolean => {\n return history.length > 0;\n };\n\n return {\n undo,\n undoPromise,\n canUndo,\n };\n};\n"],
5
- "mappings": "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",
6
- "names": ["BaseError", "EmptyHistoryError", "extend", "make", "props", "resolveMessage", "message", "input", "output", "undefined", "make", "getMappings", "lookup", "operation", "mappings", "mapping", "find", "m", "meta", "key", "undefined", "inverse", "deriveContext", "message", "make", "Effect", "Stream", "runAndForwardErrors", "log", "HISTORY_LIMIT", "make", "handleInvocation", "log", "input", "event", "output", "inverse", "mapping", "inverseInput", "timestamp", "history", "entry", "key", "operation", "historyLength", "length", "message", "resolvedMessage", "Effect", "fail", "inverseKey", "invoker", "_invokeCore", "meta", "error", "undoPromise", "canUndo"]
7
- }