@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.
- package/dist/lib/browser/{capability-K5XIVCQU.mjs → capability-JCUNWZ66.mjs} +7 -7
- package/dist/lib/browser/{chunk-5AHASNDW.mjs → chunk-2ZIYRDAF.mjs} +1 -1
- package/dist/lib/browser/{chunk-5AHASNDW.mjs.map → chunk-2ZIYRDAF.mjs.map} +1 -1
- package/dist/lib/browser/{chunk-QLML5QFJ.mjs → chunk-7MBI66M7.mjs} +12 -6
- package/dist/lib/browser/chunk-7MBI66M7.mjs.map +7 -0
- package/dist/lib/browser/{chunk-VJ5PFAWC.mjs → chunk-BMLCIFZW.mjs} +37 -16
- package/dist/lib/browser/chunk-BMLCIFZW.mjs.map +7 -0
- package/dist/lib/browser/{chunk-SLX73WRZ.mjs → chunk-DC3WRPBV.mjs} +14 -5
- package/dist/lib/browser/chunk-DC3WRPBV.mjs.map +7 -0
- package/dist/lib/browser/{chunk-BRK6GYNB.mjs → chunk-E5DTDNDY.mjs} +20 -19
- package/dist/lib/browser/chunk-E5DTDNDY.mjs.map +7 -0
- package/dist/lib/browser/{chunk-KLHQNYJ2.mjs → chunk-KZUDO43J.mjs} +2 -2
- package/dist/lib/browser/chunk-Q2GLJTVV.mjs +12 -0
- package/dist/lib/browser/chunk-Q2GLJTVV.mjs.map +7 -0
- package/dist/lib/browser/{chunk-UVTGHZQF.mjs → chunk-VBG5F5UF.mjs} +51 -88
- package/dist/lib/browser/chunk-VBG5F5UF.mjs.map +7 -0
- package/dist/lib/browser/cli/index.mjs +5 -5
- package/dist/lib/browser/common/activation-events.mjs +3 -3
- package/dist/lib/browser/common/capabilities.mjs +4 -4
- package/dist/lib/browser/core/plugin-manager.mjs +2 -2
- package/dist/lib/browser/core/plugin.mjs +3 -1
- package/dist/lib/browser/core/url-loader.mjs +2 -2
- package/dist/lib/browser/index.mjs +13 -14
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/{process-manager-capability-JIWLN7SU.mjs → process-manager-capability-4FUA4TRP.mjs} +6 -6
- package/dist/lib/browser/testing/index.mjs +11 -10
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/testing/react.mjs +6 -6
- package/dist/lib/browser/ui/index.mjs +7 -9
- package/dist/lib/node-esm/{capability-RLKFFLTB.mjs → capability-7TCQK7ZP.mjs} +7 -7
- package/dist/lib/node-esm/{chunk-SFYCO3PT.mjs → chunk-4BX2AE47.mjs} +37 -16
- package/dist/lib/node-esm/chunk-4BX2AE47.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-FPW45EZH.mjs +14 -0
- package/dist/lib/node-esm/chunk-FPW45EZH.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-LJNUFNDO.mjs → chunk-GEWHMHM7.mjs} +12 -6
- package/dist/lib/node-esm/chunk-GEWHMHM7.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-BYHYYJZH.mjs → chunk-KSH4OVMY.mjs} +20 -19
- package/dist/lib/node-esm/chunk-KSH4OVMY.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-PW2VYGOS.mjs → chunk-QBUGLR4H.mjs} +1 -1
- package/dist/lib/node-esm/{chunk-PW2VYGOS.mjs.map → chunk-QBUGLR4H.mjs.map} +1 -1
- package/dist/lib/node-esm/{chunk-42UNAKYO.mjs → chunk-XGBUKRCK.mjs} +2 -2
- package/dist/lib/node-esm/{chunk-6S45OMUP.mjs → chunk-XIGPWXCQ.mjs} +14 -5
- package/dist/lib/node-esm/chunk-XIGPWXCQ.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-JNT72ZCN.mjs → chunk-Y7JKXNDU.mjs} +51 -88
- package/dist/lib/node-esm/chunk-Y7JKXNDU.mjs.map +7 -0
- package/dist/lib/node-esm/cli/index.mjs +5 -5
- package/dist/lib/node-esm/common/activation-events.mjs +3 -3
- package/dist/lib/node-esm/common/capabilities.mjs +4 -4
- package/dist/lib/node-esm/core/plugin-manager.mjs +2 -2
- package/dist/lib/node-esm/core/plugin.mjs +3 -1
- package/dist/lib/node-esm/core/url-loader.mjs +2 -2
- package/dist/lib/node-esm/index.mjs +13 -14
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/{process-manager-capability-PHKLO2BL.mjs → process-manager-capability-F23D62RN.mjs} +6 -6
- package/dist/lib/node-esm/testing/index.mjs +11 -10
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/testing/react.mjs +6 -6
- package/dist/lib/node-esm/ui/index.mjs +7 -9
- package/dist/plugin/node-esm/index.mjs +155 -77
- package/dist/plugin/node-esm/index.mjs.map +4 -4
- package/dist/plugin/node-esm/meta.json +1 -1
- package/dist/types/src/common/capabilities.d.ts +1 -1
- package/dist/types/src/common/index.d.ts +0 -1
- package/dist/types/src/common/index.d.ts.map +1 -1
- package/dist/types/src/common/translations.d.ts +1 -1
- package/dist/types/src/core/capability-manager.d.ts +4 -0
- package/dist/types/src/core/capability-manager.d.ts.map +1 -1
- package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
- package/dist/types/src/core/plugin.d.ts +33 -6
- package/dist/types/src/core/plugin.d.ts.map +1 -1
- package/dist/types/src/plugin-process-manager/history/history-tracker.d.ts +15 -1
- package/dist/types/src/plugin-process-manager/history/history-tracker.d.ts.map +1 -1
- package/dist/types/src/plugin-process-manager/history/undo-mapping.d.ts +1 -1
- package/dist/types/src/plugin-process-manager/history/undo-mapping.d.ts.map +1 -1
- package/dist/types/src/plugin-process-manager/meta.d.ts +1 -1
- package/dist/types/src/plugin-process-manager/meta.d.ts.map +1 -1
- package/dist/types/src/plugin-process-manager/testing.d.ts.map +1 -1
- package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
- package/dist/types/src/ui/components/App/App.d.ts +34 -2
- package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
- package/dist/types/src/ui/components/App/App.stories.d.ts.map +1 -1
- package/dist/types/src/ui/components/PluginManager/PluginManagerContext.stories.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
- package/dist/types/src/ui/components/index.d.ts +0 -1
- package/dist/types/src/ui/components/index.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useApp.d.ts +1 -7
- package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useProcessManagerRuntime.d.ts +6 -0
- package/dist/types/src/ui/hooks/useProcessManagerRuntime.d.ts.map +1 -1
- package/dist/types/src/vite-plugin/boot-loader/loader.d.ts +26 -23
- package/dist/types/src/vite-plugin/boot-loader/loader.d.ts.map +1 -1
- package/dist/types/src/vite-plugin/manifest.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +40 -35
- package/src/common/capabilities.ts +1 -1
- package/src/common/index.ts +0 -1
- package/src/common/translations.ts +1 -1
- package/src/core/capability-manager.test.ts +3 -0
- package/src/core/capability-manager.ts +22 -1
- package/src/core/plugin-manager.test.ts +51 -21
- package/src/core/plugin-manager.ts +10 -5
- package/src/core/plugin.ts +44 -8
- package/src/core/url-loader.test.ts +2 -1
- package/src/plugin-process-manager/history/history-tracker.test.ts +39 -60
- package/src/plugin-process-manager/history/history-tracker.ts +32 -16
- package/src/plugin-process-manager/history/undo-mapping.ts +1 -1
- package/src/plugin-process-manager/history/undo-registry.test.ts +3 -1
- package/src/plugin-process-manager/meta.ts +6 -4
- package/src/plugin-process-manager/testing.ts +6 -5
- package/src/testing/react.test.tsx +3 -1
- package/src/testing/withPluginManager.tsx +4 -3
- package/src/ui/components/App/App.stories.tsx +9 -11
- package/src/ui/components/App/App.tsx +80 -17
- package/src/ui/components/PluginManager/PluginManagerContext.stories.tsx +9 -14
- package/src/ui/components/Surface/SurfaceComponent.stories.tsx +1 -1
- package/src/ui/components/Surface/SurfaceComponent.tsx +1 -1
- package/src/ui/components/Surface/types.test.ts +2 -2
- package/src/ui/components/Surface/types.ts +21 -0
- package/src/ui/components/index.ts +0 -1
- package/src/ui/hooks/useApp.test.tsx +3 -1
- package/src/ui/hooks/useApp.tsx +4 -18
- package/src/ui/hooks/useCapabilities.ts +1 -0
- package/src/ui/hooks/useProcessManagerRuntime.ts +12 -2
- package/src/vite-plugin/boot-loader/BootLoader.solid-stories.tsx +198 -0
- package/src/vite-plugin/boot-loader/loader-app/Loader.tsx +166 -0
- package/src/vite-plugin/boot-loader/loader-app/boot-loader.css +311 -0
- package/src/vite-plugin/boot-loader/loader-app/bridge.ts +25 -0
- package/src/vite-plugin/boot-loader/loader-app/entry.tsx +21 -0
- package/src/vite-plugin/boot-loader/loader-app/mount.tsx +70 -0
- package/src/vite-plugin/boot-loader/loader-app/store.test.ts +137 -0
- package/src/vite-plugin/boot-loader/loader-app/store.ts +149 -0
- package/src/vite-plugin/boot-loader/loader-app/tsconfig.json +11 -0
- package/src/vite-plugin/boot-loader/loader-app/types.ts +78 -0
- package/src/vite-plugin/boot-loader/loader.ts +133 -63
- package/src/vite-plugin/manifest.test.ts +19 -10
- package/src/vite-plugin/manifest.ts +9 -3
- package/tsconfig.json +6 -7
- package/dist/lib/browser/chunk-BRK6GYNB.mjs.map +0 -7
- package/dist/lib/browser/chunk-KFDF7KR3.mjs +0 -42
- package/dist/lib/browser/chunk-KFDF7KR3.mjs.map +0 -7
- package/dist/lib/browser/chunk-QLML5QFJ.mjs.map +0 -7
- package/dist/lib/browser/chunk-SLX73WRZ.mjs.map +0 -7
- package/dist/lib/browser/chunk-UVTGHZQF.mjs.map +0 -7
- package/dist/lib/browser/chunk-VJ5PFAWC.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-6S45OMUP.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-BYHYYJZH.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-JNT72ZCN.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-LJNUFNDO.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-OUEMWPIW.mjs +0 -43
- package/dist/lib/node-esm/chunk-OUEMWPIW.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-SFYCO3PT.mjs.map +0 -7
- package/dist/types/src/common/operations.d.ts +0 -19
- package/dist/types/src/common/operations.d.ts.map +0 -1
- package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts +0 -64
- package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts.map +0 -1
- package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts +0 -19
- package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts.map +0 -1
- package/dist/types/src/ui/components/Placeholder/index.d.ts +0 -2
- package/dist/types/src/ui/components/Placeholder/index.d.ts.map +0 -1
- package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts +0 -34
- package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts.map +0 -1
- package/src/common/operations.ts +0 -35
- package/src/ui/components/Placeholder/Placeholder.stories.tsx +0 -77
- package/src/ui/components/Placeholder/Placeholder.tsx +0 -155
- package/src/ui/components/Placeholder/index.ts +0 -5
- package/src/vite-plugin/boot-loader/BootLoader.stories.tsx +0 -270
- package/src/vite-plugin/boot-loader/boot-loader.css +0 -320
- package/src/vite-plugin/boot-loader/boot-loader.js +0 -325
- /package/dist/lib/browser/{capability-K5XIVCQU.mjs.map → capability-JCUNWZ66.mjs.map} +0 -0
- /package/dist/lib/browser/{chunk-KLHQNYJ2.mjs.map → chunk-KZUDO43J.mjs.map} +0 -0
- /package/dist/lib/browser/{process-manager-capability-JIWLN7SU.mjs.map → process-manager-capability-4FUA4TRP.mjs.map} +0 -0
- /package/dist/lib/node-esm/{capability-RLKFFLTB.mjs.map → capability-7TCQK7ZP.mjs.map} +0 -0
- /package/dist/lib/node-esm/{chunk-42UNAKYO.mjs.map → chunk-XGBUKRCK.mjs.map} +0 -0
- /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,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
|
-
|
|
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
|
|
22
|
-
* The mark renders at full opacity with whatever fills the SVG declares,
|
|
23
|
-
* pass a colour-palette logo for the strongest visual identity. SVGs that
|
|
24
|
-
* `fill="currentColor"` still inherit the loader's `prefers-color-scheme`
|
|
25
|
-
* text colour. Leave empty/undefined to render only the
|
|
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
|
|
122
|
+
* Vite plugin that injects a Solid "boot loader" app into the host app's
|
|
32
123
|
* `index.html`.
|
|
33
124
|
*
|
|
34
|
-
* The
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
|
42
|
-
*
|
|
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.
|
|
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"
|
|
49
|
-
* - inline `<script>`
|
|
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
|
|
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
|
|
54
|
-
* React commit
|
|
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
|
-
*
|
|
58
|
-
*
|
|
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
|
-
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
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:
|
|
173
|
+
id: rootId,
|
|
82
174
|
role: 'status',
|
|
83
175
|
'aria-live': 'polite',
|
|
84
176
|
'aria-label': 'Initializing',
|
|
85
177
|
},
|
|
86
|
-
children:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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:
|
|
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 {
|
|
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
|
-
{
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
}
|