@buoy-gg/time-machine 7.0.41 → 7.0.42
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/lib/commonjs/components/HeaderActions.js +1 -91
- package/lib/commonjs/components/HeaderReloadButton.js +1 -58
- package/lib/commonjs/components/TimeMachineBody.js +1 -282
- package/lib/commonjs/components/TimeMachineModal.js +1 -74
- package/lib/commonjs/components/confirm.js +2 -38
- package/lib/commonjs/components/floating/NamePromptModal.js +1 -193
- package/lib/commonjs/components/floating/TimeMachineFloatingTool.js +1 -715
- package/lib/commonjs/components/floating/armedPoint.js +1 -92
- package/lib/commonjs/components/floating/barSettings.js +1 -66
- package/lib/commonjs/components/format.js +1 -24
- package/lib/commonjs/components/list/CaptureBar.js +1 -97
- package/lib/commonjs/components/list/RestoreOutcomeCard.js +1 -175
- package/lib/commonjs/components/list/SnapshotRow.js +1 -170
- package/lib/commonjs/components/list/SourcesStrip.js +1 -172
- package/lib/commonjs/components/list/UndoDeleteBar.js +1 -85
- package/lib/commonjs/components/list/WipeAllRow.js +1 -46
- package/lib/commonjs/components/preview/ItemDiffPanel.js +1 -79
- package/lib/commonjs/components/preview/PreviewFlow.js +1 -51
- package/lib/commonjs/components/preview/RestorePointNameRow.js +1 -83
- package/lib/commonjs/components/preview/RestorePreviewScreen.js +1 -1010
- package/lib/commonjs/components/preview/verdictMeta.js +1 -58
- package/lib/commonjs/components/sources.js +1 -83
- package/lib/commonjs/hooks/useTimeMachine.js +1 -24
- package/lib/commonjs/index.js +1 -161
- package/lib/commonjs/preset.js +1 -56
- package/lib/commonjs/store/routeRestore.js +1 -188
- package/lib/commonjs/store/snapshotVault.js +1 -186
- package/lib/commonjs/store/sourceIds.js +1 -9
- package/lib/commonjs/store/sourceSettings.js +1 -79
- package/lib/commonjs/store/timeMachineStore.js +1 -848
- package/lib/commonjs/sync/timeMachineSyncAdapter.js +1 -258
- package/lib/commonjs/ui/SwipeActionsRow.js +1 -178
- package/lib/commonjs/ui/primitives.js +1 -172
- package/lib/commonjs/ui/tmTheme.js +1 -118
- package/lib/module/components/HeaderActions.js +1 -85
- package/lib/module/components/HeaderReloadButton.js +1 -53
- package/lib/module/components/TimeMachineBody.js +1 -277
- package/lib/module/components/TimeMachineModal.js +1 -68
- package/lib/module/components/confirm.js +2 -33
- package/lib/module/components/floating/NamePromptModal.js +1 -188
- package/lib/module/components/floating/TimeMachineFloatingTool.js +1 -708
- package/lib/module/components/floating/armedPoint.js +1 -82
- package/lib/module/components/floating/barSettings.js +1 -57
- package/lib/module/components/format.js +1 -19
- package/lib/module/components/list/CaptureBar.js +1 -92
- package/lib/module/components/list/RestoreOutcomeCard.js +1 -169
- package/lib/module/components/list/SnapshotRow.js +1 -167
- package/lib/module/components/list/SourcesStrip.js +1 -169
- package/lib/module/components/list/UndoDeleteBar.js +1 -82
- package/lib/module/components/list/WipeAllRow.js +1 -41
- package/lib/module/components/preview/ItemDiffPanel.js +1 -74
- package/lib/module/components/preview/PreviewFlow.js +1 -46
- package/lib/module/components/preview/RestorePointNameRow.js +1 -78
- package/lib/module/components/preview/RestorePreviewScreen.js +1 -1005
- package/lib/module/components/preview/verdictMeta.js +1 -54
- package/lib/module/components/sources.js +1 -73
- package/lib/module/hooks/useTimeMachine.js +1 -19
- package/lib/module/index.js +1 -46
- package/lib/module/preset.js +1 -51
- package/lib/module/store/routeRestore.js +1 -176
- package/lib/module/store/snapshotVault.js +1 -170
- package/lib/module/store/sourceIds.js +1 -5
- package/lib/module/store/sourceSettings.js +1 -69
- package/lib/module/store/timeMachineStore.js +1 -839
- package/lib/module/sync/timeMachineSyncAdapter.js +1 -253
- package/lib/module/ui/SwipeActionsRow.js +1 -173
- package/lib/module/ui/primitives.js +1 -164
- package/lib/module/ui/tmTheme.js +1 -114
- package/lib/web/index.mjs +2 -3
- package/package.json +7 -7
- package/lib/commonjs/components/HeaderActions.js.map +0 -1
- package/lib/commonjs/components/HeaderReloadButton.js.map +0 -1
- package/lib/commonjs/components/TimeMachineBody.js.map +0 -1
- package/lib/commonjs/components/TimeMachineModal.js.map +0 -1
- package/lib/commonjs/components/confirm.js.map +0 -1
- package/lib/commonjs/components/floating/NamePromptModal.js.map +0 -1
- package/lib/commonjs/components/floating/TimeMachineFloatingTool.js.map +0 -1
- package/lib/commonjs/components/floating/armedPoint.js.map +0 -1
- package/lib/commonjs/components/floating/barSettings.js.map +0 -1
- package/lib/commonjs/components/format.js.map +0 -1
- package/lib/commonjs/components/list/CaptureBar.js.map +0 -1
- package/lib/commonjs/components/list/RestoreOutcomeCard.js.map +0 -1
- package/lib/commonjs/components/list/SnapshotRow.js.map +0 -1
- package/lib/commonjs/components/list/SourcesStrip.js.map +0 -1
- package/lib/commonjs/components/list/UndoDeleteBar.js.map +0 -1
- package/lib/commonjs/components/list/WipeAllRow.js.map +0 -1
- package/lib/commonjs/components/preview/ItemDiffPanel.js.map +0 -1
- package/lib/commonjs/components/preview/PreviewFlow.js.map +0 -1
- package/lib/commonjs/components/preview/RestorePointNameRow.js.map +0 -1
- package/lib/commonjs/components/preview/RestorePreviewScreen.js.map +0 -1
- package/lib/commonjs/components/preview/verdictMeta.js.map +0 -1
- package/lib/commonjs/components/sources.js.map +0 -1
- package/lib/commonjs/hooks/useTimeMachine.js.map +0 -1
- package/lib/commonjs/index.js.map +0 -1
- package/lib/commonjs/preset.js.map +0 -1
- package/lib/commonjs/store/routeRestore.js.map +0 -1
- package/lib/commonjs/store/snapshotVault.js.map +0 -1
- package/lib/commonjs/store/sourceIds.js.map +0 -1
- package/lib/commonjs/store/sourceSettings.js.map +0 -1
- package/lib/commonjs/store/timeMachineStore.js.map +0 -1
- package/lib/commonjs/sync/timeMachineSyncAdapter.js.map +0 -1
- package/lib/commonjs/ui/SwipeActionsRow.js.map +0 -1
- package/lib/commonjs/ui/primitives.js.map +0 -1
- package/lib/commonjs/ui/tmTheme.js.map +0 -1
- package/lib/module/components/HeaderActions.js.map +0 -1
- package/lib/module/components/HeaderReloadButton.js.map +0 -1
- package/lib/module/components/TimeMachineBody.js.map +0 -1
- package/lib/module/components/TimeMachineModal.js.map +0 -1
- package/lib/module/components/confirm.js.map +0 -1
- package/lib/module/components/floating/NamePromptModal.js.map +0 -1
- package/lib/module/components/floating/TimeMachineFloatingTool.js.map +0 -1
- package/lib/module/components/floating/armedPoint.js.map +0 -1
- package/lib/module/components/floating/barSettings.js.map +0 -1
- package/lib/module/components/format.js.map +0 -1
- package/lib/module/components/list/CaptureBar.js.map +0 -1
- package/lib/module/components/list/RestoreOutcomeCard.js.map +0 -1
- package/lib/module/components/list/SnapshotRow.js.map +0 -1
- package/lib/module/components/list/SourcesStrip.js.map +0 -1
- package/lib/module/components/list/UndoDeleteBar.js.map +0 -1
- package/lib/module/components/list/WipeAllRow.js.map +0 -1
- package/lib/module/components/preview/ItemDiffPanel.js.map +0 -1
- package/lib/module/components/preview/PreviewFlow.js.map +0 -1
- package/lib/module/components/preview/RestorePointNameRow.js.map +0 -1
- package/lib/module/components/preview/RestorePreviewScreen.js.map +0 -1
- package/lib/module/components/preview/verdictMeta.js.map +0 -1
- package/lib/module/components/sources.js.map +0 -1
- package/lib/module/hooks/useTimeMachine.js.map +0 -1
- package/lib/module/index.js.map +0 -1
- package/lib/module/preset.js.map +0 -1
- package/lib/module/store/routeRestore.js.map +0 -1
- package/lib/module/store/snapshotVault.js.map +0 -1
- package/lib/module/store/sourceIds.js.map +0 -1
- package/lib/module/store/sourceSettings.js.map +0 -1
- package/lib/module/store/timeMachineStore.js.map +0 -1
- package/lib/module/sync/timeMachineSyncAdapter.js.map +0 -1
- package/lib/module/ui/SwipeActionsRow.js.map +0 -1
- package/lib/module/ui/primitives.js.map +0 -1
- package/lib/module/ui/tmTheme.js.map +0 -1
- package/lib/typescript/components/HeaderActions.d.ts.map +0 -1
- package/lib/typescript/components/HeaderReloadButton.d.ts.map +0 -1
- package/lib/typescript/components/TimeMachineBody.d.ts.map +0 -1
- package/lib/typescript/components/TimeMachineModal.d.ts.map +0 -1
- package/lib/typescript/components/confirm.d.ts.map +0 -1
- package/lib/typescript/components/floating/NamePromptModal.d.ts.map +0 -1
- package/lib/typescript/components/floating/TimeMachineFloatingTool.d.ts.map +0 -1
- package/lib/typescript/components/floating/armedPoint.d.ts.map +0 -1
- package/lib/typescript/components/floating/barSettings.d.ts.map +0 -1
- package/lib/typescript/components/format.d.ts.map +0 -1
- package/lib/typescript/components/list/CaptureBar.d.ts.map +0 -1
- package/lib/typescript/components/list/RestoreOutcomeCard.d.ts.map +0 -1
- package/lib/typescript/components/list/SnapshotRow.d.ts.map +0 -1
- package/lib/typescript/components/list/SourcesStrip.d.ts.map +0 -1
- package/lib/typescript/components/list/UndoDeleteBar.d.ts.map +0 -1
- package/lib/typescript/components/list/WipeAllRow.d.ts.map +0 -1
- package/lib/typescript/components/preview/ItemDiffPanel.d.ts.map +0 -1
- package/lib/typescript/components/preview/PreviewFlow.d.ts.map +0 -1
- package/lib/typescript/components/preview/RestorePointNameRow.d.ts.map +0 -1
- package/lib/typescript/components/preview/RestorePreviewScreen.d.ts.map +0 -1
- package/lib/typescript/components/preview/verdictMeta.d.ts.map +0 -1
- package/lib/typescript/components/sources.d.ts.map +0 -1
- package/lib/typescript/hooks/useTimeMachine.d.ts.map +0 -1
- package/lib/typescript/index.d.ts.map +0 -1
- package/lib/typescript/preset.d.ts.map +0 -1
- package/lib/typescript/store/routeRestore.d.ts.map +0 -1
- package/lib/typescript/store/snapshotVault.d.ts.map +0 -1
- package/lib/typescript/store/sourceIds.d.ts.map +0 -1
- package/lib/typescript/store/sourceSettings.d.ts.map +0 -1
- package/lib/typescript/store/timeMachineStore.d.ts.map +0 -1
- package/lib/typescript/sync/timeMachineSyncAdapter.d.ts.map +0 -1
- package/lib/typescript/ui/SwipeActionsRow.d.ts.map +0 -1
- package/lib/typescript/ui/primitives.d.ts.map +0 -1
- package/lib/typescript/ui/tmTheme.d.ts.map +0 -1
|
@@ -1,839 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Time Machine orchestrator — snapshot CRUD + capture/restore across every
|
|
5
|
-
* registered StateSnapshotProvider (storage, redux, zustand, jotai, query,
|
|
6
|
-
* plus any custom providers the app registers).
|
|
7
|
-
*
|
|
8
|
-
* Restore ordering is fixed: persisted storage FIRST (so store persist
|
|
9
|
-
* middleware writing through after an in-memory restore converges with what
|
|
10
|
-
* we just wrote), then redux → zustand → jotai, query LAST (restored queries
|
|
11
|
-
* may derive from store state). Custom providers run after, in data order.
|
|
12
|
-
*
|
|
13
|
-
* Two restore modes:
|
|
14
|
-
* - "live": in-place state replacement, capability-gated per source.
|
|
15
|
-
* - "reload": restore ONLY persisted storage, then reload the JS bundle —
|
|
16
|
-
* in-memory stores rebuild themselves from the restored storage.
|
|
17
|
-
* Baseline snapshots always use this path (clear + reload).
|
|
18
|
-
*/
|
|
19
|
-
import { getStateSnapshotProvider, getStateSnapshotProviders, scheduleReloadApp, superJsonDeserialize, superJsonSerialize } from "@buoy-gg/shared-ui";
|
|
20
|
-
import { objectDiff } from "@buoy-gg/shared-ui/dataViewer";
|
|
21
|
-
import { deleteSnapshot, duplicateSnapshot, loadIndex, loadSnapshot, markSnapshotRestored, readSnapshot, renameSnapshot, restoreDeletedSnapshot, saveSnapshot, setSnapshotRestoreRoute, setSnapshotScope, setSnapshotExclusions } from "./snapshotVault";
|
|
22
|
-
import { captureCurrentRoute, navigateToRoute, savePendingRoute } from "./routeRestore";
|
|
23
|
-
import { ensureSourceSettingsLoaded, isSourceEnabled } from "./sourceSettings";
|
|
24
|
-
import { ROUTE_SOURCE_ID } from "./sourceIds";
|
|
25
|
-
export const RESTORE_ORDER = ["storage", "redux", "zustand", "jotai", "query"];
|
|
26
|
-
/**
|
|
27
|
-
* What to call a capture nobody named. The screen you were on plus the time
|
|
28
|
-
* is the pair that makes a restore point recognizable an hour later —
|
|
29
|
-
* "Untitled snapshot" never was, and typing mid-flow is the friction that
|
|
30
|
-
* produced it.
|
|
31
|
-
*/
|
|
32
|
-
function defaultSnapshotName(route) {
|
|
33
|
-
const now = new Date();
|
|
34
|
-
const clock = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
|
35
|
-
const screen = route ? route.pathname === "/" ? "Home" : route.pathname : null;
|
|
36
|
-
return screen ? `${screen} · ${clock}` : `Snapshot · ${clock}`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Two captures in the same minute on the same screen would get the same
|
|
41
|
-
* default name and be told apart by nothing. Number the repeats.
|
|
42
|
-
*/
|
|
43
|
-
function dedupeName(name, existing) {
|
|
44
|
-
const taken = new Set(existing.map(m => m.name));
|
|
45
|
-
if (!taken.has(name)) return name;
|
|
46
|
-
for (let n = 2; n < 1000; n++) {
|
|
47
|
-
const candidate = `${name} (${n})`;
|
|
48
|
-
if (!taken.has(candidate)) return candidate;
|
|
49
|
-
}
|
|
50
|
-
return name;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Will restoring this point also navigate back to its route? Having a route
|
|
55
|
-
* means wanting it back unless the point says otherwise — the same rule
|
|
56
|
-
* `restore` applies, exported so the UI can't drift from it.
|
|
57
|
-
*/
|
|
58
|
-
export function willRestoreRoute(meta) {
|
|
59
|
-
return !!meta.route && (meta.restoreRoute ?? true);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Extra knobs a restore accepts beyond mode + exclusions. */
|
|
63
|
-
|
|
64
|
-
export const SAFETY_SNAPSHOT_NAME = "Before last restore";
|
|
65
|
-
|
|
66
|
-
/** Provider capability row as mirrored to a remote dashboard. */
|
|
67
|
-
|
|
68
|
-
/** One side of a preview comparison. */
|
|
69
|
-
|
|
70
|
-
/** Whether this device can capture and return to routes, and where it is now. */
|
|
71
|
-
|
|
72
|
-
let remoteHandler = null;
|
|
73
|
-
function skippedResult(reason) {
|
|
74
|
-
return {
|
|
75
|
-
ok: false,
|
|
76
|
-
applied: 0,
|
|
77
|
-
skipped: [{
|
|
78
|
-
id: "*",
|
|
79
|
-
reason
|
|
80
|
-
}],
|
|
81
|
-
warnings: []
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
let idCounter = 0;
|
|
85
|
-
function nextId() {
|
|
86
|
-
idCounter += 1;
|
|
87
|
-
return `snap_${Date.now().toString(36)}_${idCounter.toString(36)}`;
|
|
88
|
-
}
|
|
89
|
-
class TimeMachineStore {
|
|
90
|
-
state = {
|
|
91
|
-
snapshots: [],
|
|
92
|
-
hasLoaded: false,
|
|
93
|
-
busy: null,
|
|
94
|
-
lastOutcome: null,
|
|
95
|
-
preview: null,
|
|
96
|
-
remoteProviders: null,
|
|
97
|
-
remoteRoute: null,
|
|
98
|
-
recentlyDeleted: null
|
|
99
|
-
};
|
|
100
|
-
/** Monotonic token — a preview build checks it after every await and
|
|
101
|
-
* abandons itself when a newer build (or clearPreview) supersedes it. */
|
|
102
|
-
previewRunId = 0;
|
|
103
|
-
listeners = new Set();
|
|
104
|
-
loadPromise = null;
|
|
105
|
-
getState() {
|
|
106
|
-
return this.state;
|
|
107
|
-
}
|
|
108
|
-
subscribe = listener => {
|
|
109
|
-
this.listeners.add(listener);
|
|
110
|
-
void this.ensureLoaded();
|
|
111
|
-
return () => {
|
|
112
|
-
this.listeners.delete(listener);
|
|
113
|
-
};
|
|
114
|
-
};
|
|
115
|
-
setState(patch) {
|
|
116
|
-
this.state = {
|
|
117
|
-
...this.state,
|
|
118
|
-
...patch
|
|
119
|
-
};
|
|
120
|
-
this.listeners.forEach(l => {
|
|
121
|
-
try {
|
|
122
|
-
l();
|
|
123
|
-
} catch {
|
|
124
|
-
// listener errors must not break the store
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
ensureLoaded() {
|
|
129
|
-
if (remoteHandler) return Promise.resolve(); // mirror mode — device owns the vault
|
|
130
|
-
if (!this.loadPromise) {
|
|
131
|
-
this.loadPromise = loadIndex().then(snapshots => {
|
|
132
|
-
if (remoteHandler) return;
|
|
133
|
-
this.setState({
|
|
134
|
-
snapshots,
|
|
135
|
-
hasLoaded: true
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
return this.loadPromise;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** @internal Buoy Desktop: mirror the device's synced metadata payload. */
|
|
143
|
-
replaceFromSnapshot(payload) {
|
|
144
|
-
this.setState({
|
|
145
|
-
snapshots: payload.snapshots ?? [],
|
|
146
|
-
busy: payload.busy ?? null,
|
|
147
|
-
lastOutcome: payload.lastOutcome ?? null,
|
|
148
|
-
remoteProviders: payload.providers ?? [],
|
|
149
|
-
remoteRoute: payload.route ?? {
|
|
150
|
-
available: false
|
|
151
|
-
},
|
|
152
|
-
hasLoaded: true
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Capture all capturable sources (or a subset) into a named snapshot. */
|
|
157
|
-
async capture(name, sourceIds, options) {
|
|
158
|
-
if (remoteHandler) {
|
|
159
|
-
const result = await remoteHandler("capture", {
|
|
160
|
-
name,
|
|
161
|
-
sourceIds
|
|
162
|
-
});
|
|
163
|
-
return result.snapshot;
|
|
164
|
-
}
|
|
165
|
-
await this.ensureLoaded();
|
|
166
|
-
await ensureSourceSettingsLoaded();
|
|
167
|
-
this.setState({
|
|
168
|
-
busy: "capture"
|
|
169
|
-
});
|
|
170
|
-
try {
|
|
171
|
-
const providers = getStateSnapshotProviders().filter(p => p.status().canCapture && isSourceEnabled(p.id) && (!sourceIds || sourceIds.includes(p.id)));
|
|
172
|
-
const data = {};
|
|
173
|
-
const sources = {};
|
|
174
|
-
for (const provider of providers) {
|
|
175
|
-
try {
|
|
176
|
-
const captured = await provider.capture();
|
|
177
|
-
data[provider.id] = superJsonSerialize(captured.data);
|
|
178
|
-
sources[provider.id] = {
|
|
179
|
-
warnings: captured.warnings
|
|
180
|
-
};
|
|
181
|
-
} catch (e) {
|
|
182
|
-
sources[provider.id] = {
|
|
183
|
-
warnings: [`capture failed: ${String(e)}`]
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
// Where the app was when this state was true — restoring can return here.
|
|
188
|
-
const route = isSourceEnabled(ROUTE_SOURCE_ID) ? captureCurrentRoute() : null;
|
|
189
|
-
// The safety slot is rolling: the previous one goes before this one lands.
|
|
190
|
-
let current = this.state.snapshots;
|
|
191
|
-
if (options?.safety) {
|
|
192
|
-
for (const stale of current.filter(m => m.safety)) {
|
|
193
|
-
current = await deleteSnapshot(stale.id, current);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
const snapshot = {
|
|
197
|
-
meta: {
|
|
198
|
-
id: nextId(),
|
|
199
|
-
name: options?.safety ? SAFETY_SNAPSHOT_NAME : dedupeName(name.trim() || defaultSnapshotName(route), current),
|
|
200
|
-
createdAt: Date.now(),
|
|
201
|
-
sizeBytes: 0,
|
|
202
|
-
...(route ? {
|
|
203
|
-
route
|
|
204
|
-
} : {}),
|
|
205
|
-
...(options?.safety ? {
|
|
206
|
-
safety: true
|
|
207
|
-
} : {}),
|
|
208
|
-
sources
|
|
209
|
-
},
|
|
210
|
-
data
|
|
211
|
-
};
|
|
212
|
-
const snapshots = await saveSnapshot(snapshot, current);
|
|
213
|
-
this.setState({
|
|
214
|
-
snapshots
|
|
215
|
-
});
|
|
216
|
-
return snapshot.meta;
|
|
217
|
-
} finally {
|
|
218
|
-
this.setState({
|
|
219
|
-
busy: null
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Capture the fresh-install baseline: an EMPTY snapshot whose restore
|
|
226
|
-
* wipes app state (clear + reload) — regardless of what state the app is
|
|
227
|
-
* in when the button is pressed.
|
|
228
|
-
*/
|
|
229
|
-
async captureBaseline() {
|
|
230
|
-
if (remoteHandler) {
|
|
231
|
-
const result = await remoteHandler("captureBaseline", {});
|
|
232
|
-
return result.snapshot;
|
|
233
|
-
}
|
|
234
|
-
await this.ensureLoaded();
|
|
235
|
-
await ensureSourceSettingsLoaded();
|
|
236
|
-
const snapshot = {
|
|
237
|
-
meta: {
|
|
238
|
-
id: nextId(),
|
|
239
|
-
name: "Fresh install",
|
|
240
|
-
createdAt: Date.now(),
|
|
241
|
-
sizeBytes: 0,
|
|
242
|
-
baseline: true,
|
|
243
|
-
sources: {}
|
|
244
|
-
},
|
|
245
|
-
data: {}
|
|
246
|
-
};
|
|
247
|
-
const snapshots = await saveSnapshot(snapshot, this.state.snapshots);
|
|
248
|
-
this.setState({
|
|
249
|
-
snapshots
|
|
250
|
-
});
|
|
251
|
-
return snapshot.meta;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* `excludeKeys` entries are compound `"sourceId::itemKey"` strings (the
|
|
256
|
-
* itemKey being what previewItems emitted) — split per source before each
|
|
257
|
-
* provider sees only its own keys.
|
|
258
|
-
*/
|
|
259
|
-
async restore(id, mode, excludeKeys, options) {
|
|
260
|
-
if (remoteHandler) {
|
|
261
|
-
const outcome = await remoteHandler("restore", {
|
|
262
|
-
id,
|
|
263
|
-
mode,
|
|
264
|
-
excludeKeys,
|
|
265
|
-
restoreRoute: options?.restoreRoute
|
|
266
|
-
});
|
|
267
|
-
this.setState({
|
|
268
|
-
lastOutcome: outcome
|
|
269
|
-
});
|
|
270
|
-
return outcome;
|
|
271
|
-
}
|
|
272
|
-
await this.ensureLoaded();
|
|
273
|
-
await ensureSourceSettingsLoaded();
|
|
274
|
-
// The way back from a mis-tap: capture what is about to be replaced.
|
|
275
|
-
// Restoring the safety slot itself never rolls it, or Undo would eat
|
|
276
|
-
// its own target.
|
|
277
|
-
let safetySnapshotId;
|
|
278
|
-
if (options?.safetyCapture && !this.state.snapshots.find(m => m.id === id)?.safety) {
|
|
279
|
-
safetySnapshotId = (await this.capture(SAFETY_SNAPSHOT_NAME, undefined, {
|
|
280
|
-
safety: true
|
|
281
|
-
})).id;
|
|
282
|
-
}
|
|
283
|
-
this.setState({
|
|
284
|
-
busy: "restore"
|
|
285
|
-
});
|
|
286
|
-
try {
|
|
287
|
-
const stored = await loadSnapshot(id);
|
|
288
|
-
if (!stored) throw new Error(`No snapshot with id "${id}"`);
|
|
289
|
-
const outcome = {
|
|
290
|
-
snapshotId: id,
|
|
291
|
-
snapshotName: stored.meta.name,
|
|
292
|
-
mode,
|
|
293
|
-
at: Date.now(),
|
|
294
|
-
willReload: false,
|
|
295
|
-
results: {},
|
|
296
|
-
...(safetySnapshotId ? {
|
|
297
|
-
safetySnapshotId
|
|
298
|
-
} : {})
|
|
299
|
-
};
|
|
300
|
-
if (stored.meta.baseline) {
|
|
301
|
-
// Fresh install: wipe every clearable source, then reload so all
|
|
302
|
-
// in-memory state rebuilds from nothing.
|
|
303
|
-
for (const provider of getStateSnapshotProviders()) {
|
|
304
|
-
if (provider.clear) {
|
|
305
|
-
try {
|
|
306
|
-
outcome.results[provider.id] = await provider.clear();
|
|
307
|
-
} catch (e) {
|
|
308
|
-
outcome.results[provider.id] = skippedResult(String(e));
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
outcome.willReload = this.scheduleReload("buoy time-machine baseline");
|
|
313
|
-
this.setState({
|
|
314
|
-
lastOutcome: outcome
|
|
315
|
-
});
|
|
316
|
-
return outcome;
|
|
317
|
-
}
|
|
318
|
-
const sourceIds = [...RESTORE_ORDER.filter(sid => sid in stored.data), ...Object.keys(stored.data).filter(sid => !RESTORE_ORDER.includes(sid))];
|
|
319
|
-
const splitCompound = keys => {
|
|
320
|
-
const bySource = new Map();
|
|
321
|
-
for (const compound of keys) {
|
|
322
|
-
const sep = compound.indexOf("::");
|
|
323
|
-
if (sep <= 0) continue;
|
|
324
|
-
const sid = compound.slice(0, sep);
|
|
325
|
-
const itemKey = compound.slice(sep + 2);
|
|
326
|
-
const list = bySource.get(sid) ?? [];
|
|
327
|
-
list.push(itemKey);
|
|
328
|
-
bySource.set(sid, list);
|
|
329
|
-
}
|
|
330
|
-
return bySource;
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
// No explicit list means "use what the user saved in the preview" —
|
|
334
|
-
// that is what makes an unchecked item stay unchecked forever, for the
|
|
335
|
-
// one-tap RESTORE on the list and for MCP alike.
|
|
336
|
-
const effectiveExcludes = excludeKeys ?? stored.meta.excludedKeys ?? [];
|
|
337
|
-
const excludeBySource = splitCompound(effectiveExcludes);
|
|
338
|
-
// A saved scope makes this a targeted restore point. An explicit
|
|
339
|
-
// exclusion list from the preview already reflects the scope (the
|
|
340
|
-
// preview seeds its selection from it), so it takes precedence.
|
|
341
|
-
const includeBySource = !excludeKeys && stored.meta.scope?.length ? splitCompound(stored.meta.scope) : null;
|
|
342
|
-
|
|
343
|
-
// No prompt any more: the point carries its own answer, and having a
|
|
344
|
-
// route means wanting it back unless you said otherwise. An explicit
|
|
345
|
-
// option still wins, so MCP and the desktop can force either way.
|
|
346
|
-
const wantsRoute = isSourceEnabled(ROUTE_SOURCE_ID) && (options?.restoreRoute ?? willRestoreRoute(stored.meta));
|
|
347
|
-
if (mode === "reload") {
|
|
348
|
-
// Storage only; everything in-memory rebuilds after the reload.
|
|
349
|
-
if (stored.data.storage && isSourceEnabled("storage") && (!includeBySource || includeBySource.has("storage"))) {
|
|
350
|
-
outcome.results.storage = await this.restoreSource("storage", stored.data.storage, excludeBySource.get("storage"), includeBySource?.get("storage"));
|
|
351
|
-
}
|
|
352
|
-
// Hand the route to the NEXT boot — this realm is about to die, so the
|
|
353
|
-
// slot must be written before the reload is armed.
|
|
354
|
-
if (wantsRoute) {
|
|
355
|
-
const route = stored.meta.route;
|
|
356
|
-
try {
|
|
357
|
-
await savePendingRoute(route, stored.meta.name);
|
|
358
|
-
outcome.route = {
|
|
359
|
-
...route,
|
|
360
|
-
status: "deferred"
|
|
361
|
-
};
|
|
362
|
-
} catch (e) {
|
|
363
|
-
outcome.route = {
|
|
364
|
-
...route,
|
|
365
|
-
status: "failed",
|
|
366
|
-
error: String(e)
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
outcome.willReload = this.scheduleReload("buoy time-machine restore");
|
|
371
|
-
// Stamped BEFORE the reload is armed — the JS realm is about to die.
|
|
372
|
-
this.setState({
|
|
373
|
-
lastOutcome: outcome,
|
|
374
|
-
snapshots: await markSnapshotRestored(id, outcome.at, this.state.snapshots)
|
|
375
|
-
});
|
|
376
|
-
return outcome;
|
|
377
|
-
}
|
|
378
|
-
for (const sid of sourceIds) {
|
|
379
|
-
// Sources entirely outside the scope are untouched and unreported.
|
|
380
|
-
if (includeBySource && !includeBySource.has(sid)) continue;
|
|
381
|
-
// A source switched off in Sources is left exactly as it is. Reported
|
|
382
|
-
// as a warning, not a failure: this is what you asked for.
|
|
383
|
-
if (!isSourceEnabled(sid)) {
|
|
384
|
-
outcome.results[sid] = {
|
|
385
|
-
ok: true,
|
|
386
|
-
applied: 0,
|
|
387
|
-
skipped: [],
|
|
388
|
-
warnings: ["Switched off in Sources; left as it is"]
|
|
389
|
-
};
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
outcome.results[sid] = await this.restoreSource(sid, stored.data[sid], excludeBySource.get(sid), includeBySource?.get(sid));
|
|
393
|
-
}
|
|
394
|
-
// Navigate LAST — the screen we land on should read the restored state.
|
|
395
|
-
if (wantsRoute) {
|
|
396
|
-
outcome.route = navigateToRoute(stored.meta.route);
|
|
397
|
-
}
|
|
398
|
-
this.setState({
|
|
399
|
-
lastOutcome: outcome,
|
|
400
|
-
snapshots: await markSnapshotRestored(id, outcome.at, this.state.snapshots)
|
|
401
|
-
});
|
|
402
|
-
return outcome;
|
|
403
|
-
} finally {
|
|
404
|
-
this.setState({
|
|
405
|
-
busy: null
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
async restoreSource(sourceId, payload, excludeKeys, includeKeys) {
|
|
410
|
-
const provider = getStateSnapshotProvider(sourceId);
|
|
411
|
-
if (!provider) {
|
|
412
|
-
return skippedResult("provider not available (package missing or not initialized)");
|
|
413
|
-
}
|
|
414
|
-
const status = provider.status();
|
|
415
|
-
if (!status.canRestore) {
|
|
416
|
-
return skippedResult(status.detail ?? "restore unavailable");
|
|
417
|
-
}
|
|
418
|
-
try {
|
|
419
|
-
const options = excludeKeys?.length || includeKeys?.length ? {
|
|
420
|
-
...(excludeKeys?.length ? {
|
|
421
|
-
excludeKeys
|
|
422
|
-
} : {}),
|
|
423
|
-
...(includeKeys?.length ? {
|
|
424
|
-
includeKeys
|
|
425
|
-
} : {})
|
|
426
|
-
} : undefined;
|
|
427
|
-
return await provider.restore(superJsonDeserialize(payload), options);
|
|
428
|
-
} catch (e) {
|
|
429
|
-
return skippedResult(String(e));
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
scheduleReload(reason) {
|
|
433
|
-
try {
|
|
434
|
-
scheduleReloadApp({
|
|
435
|
-
reason
|
|
436
|
-
});
|
|
437
|
-
return true;
|
|
438
|
-
} catch {
|
|
439
|
-
// No reload mechanism (bare production build?) — restore still stands.
|
|
440
|
-
return false;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Delete, holding the whole record in memory so `undoDelete` can put it
|
|
446
|
-
* back. Swipe-and-tap is deliberate but not recoverable, and those are
|
|
447
|
-
* different properties — a restore point can represent state that takes
|
|
448
|
-
* ten minutes and a cooperative backend to rebuild.
|
|
449
|
-
*/
|
|
450
|
-
async delete(id) {
|
|
451
|
-
if (remoteHandler) {
|
|
452
|
-
await remoteHandler("delete", {
|
|
453
|
-
id
|
|
454
|
-
});
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
await this.ensureLoaded();
|
|
458
|
-
await ensureSourceSettingsLoaded();
|
|
459
|
-
const stored = await readSnapshot(id);
|
|
460
|
-
const snapshots = await deleteSnapshot(id, this.state.snapshots);
|
|
461
|
-
this.setState({
|
|
462
|
-
snapshots,
|
|
463
|
-
recentlyDeleted: stored ? {
|
|
464
|
-
stored,
|
|
465
|
-
at: Date.now()
|
|
466
|
-
} : null
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/** Put the last deleted point back. No-op once the window has passed. */
|
|
471
|
-
async undoDelete() {
|
|
472
|
-
const held = this.state.recentlyDeleted;
|
|
473
|
-
if (!held) return;
|
|
474
|
-
const snapshots = await restoreDeletedSnapshot(held.stored, this.state.snapshots);
|
|
475
|
-
this.setState({
|
|
476
|
-
snapshots,
|
|
477
|
-
recentlyDeleted: null
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/** Drop the undo offer — on timeout, or when the user moves on. */
|
|
482
|
-
clearRecentlyDeleted() {
|
|
483
|
-
if (this.state.recentlyDeleted) this.setState({
|
|
484
|
-
recentlyDeleted: null
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/** Turn this point's route restore on or off. */
|
|
489
|
-
async setRestoreRoute(id, restoreRoute) {
|
|
490
|
-
if (remoteHandler) {
|
|
491
|
-
await remoteHandler("setRestoreRoute", {
|
|
492
|
-
id,
|
|
493
|
-
restoreRoute
|
|
494
|
-
});
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
await this.ensureLoaded();
|
|
498
|
-
await ensureSourceSettingsLoaded();
|
|
499
|
-
const snapshots = await setSnapshotRestoreRoute(id, restoreRoute, this.state.snapshots);
|
|
500
|
-
this.setState({
|
|
501
|
-
snapshots
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
/** Copy a point so a variation can start from a known-good base. */
|
|
506
|
-
async duplicate(id) {
|
|
507
|
-
if (remoteHandler) {
|
|
508
|
-
const result = await remoteHandler("duplicate", {
|
|
509
|
-
id
|
|
510
|
-
});
|
|
511
|
-
return result?.snapshot ?? null;
|
|
512
|
-
}
|
|
513
|
-
await this.ensureLoaded();
|
|
514
|
-
await ensureSourceSettingsLoaded();
|
|
515
|
-
const source = this.state.snapshots.find(s => s.id === id);
|
|
516
|
-
if (!source) return null;
|
|
517
|
-
const result = await duplicateSnapshot(id, `${source.name} copy`, this.state.snapshots);
|
|
518
|
-
if (!result) return null;
|
|
519
|
-
this.setState({
|
|
520
|
-
snapshots: result.metas
|
|
521
|
-
});
|
|
522
|
-
return result.meta;
|
|
523
|
-
}
|
|
524
|
-
async rename(id, name) {
|
|
525
|
-
if (remoteHandler) {
|
|
526
|
-
await remoteHandler("rename", {
|
|
527
|
-
id,
|
|
528
|
-
name
|
|
529
|
-
});
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
await this.ensureLoaded();
|
|
533
|
-
await ensureSourceSettingsLoaded();
|
|
534
|
-
const snapshots = await renameSnapshot(id, name, this.state.snapshots);
|
|
535
|
-
this.setState({
|
|
536
|
-
snapshots
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* Save (or clear with null/empty) a snapshot's restore scope — compound
|
|
542
|
-
* "sourceId::itemKey" strings. A scoped restore point only ever touches
|
|
543
|
-
* its scoped items.
|
|
544
|
-
*/
|
|
545
|
-
async setScope(id, scope) {
|
|
546
|
-
if (remoteHandler) {
|
|
547
|
-
await remoteHandler("setScope", {
|
|
548
|
-
id,
|
|
549
|
-
scope
|
|
550
|
-
});
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
await this.ensureLoaded();
|
|
554
|
-
await ensureSourceSettingsLoaded();
|
|
555
|
-
const snapshots = await setSnapshotScope(id, scope, this.state.snapshots);
|
|
556
|
-
this.setState({
|
|
557
|
-
snapshots
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* Save (or clear with null/empty) the items every later restore of this
|
|
563
|
-
* snapshot must leave untouched — what the restore preview's checkboxes
|
|
564
|
-
* write when you save a selection.
|
|
565
|
-
*/
|
|
566
|
-
async setExclusions(id, excludedKeys) {
|
|
567
|
-
if (remoteHandler) {
|
|
568
|
-
await remoteHandler("setExclusions", {
|
|
569
|
-
id,
|
|
570
|
-
excludedKeys
|
|
571
|
-
});
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
await this.ensureLoaded();
|
|
575
|
-
await ensureSourceSettingsLoaded();
|
|
576
|
-
const snapshots = await setSnapshotExclusions(id, excludedKeys, this.state.snapshots);
|
|
577
|
-
this.setState({
|
|
578
|
-
snapshots
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
clearLastOutcome() {
|
|
582
|
-
this.setState({
|
|
583
|
-
lastOutcome: null
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* Wipe every clearable source to fresh-install state and reload — the
|
|
589
|
-
* standalone utility behind the home screen's "Wipe all app state" button
|
|
590
|
-
* (no baseline snapshot involved).
|
|
591
|
-
*/
|
|
592
|
-
async wipeAll() {
|
|
593
|
-
if (remoteHandler) {
|
|
594
|
-
const outcome = await remoteHandler("wipeAll", {});
|
|
595
|
-
this.setState({
|
|
596
|
-
lastOutcome: outcome
|
|
597
|
-
});
|
|
598
|
-
return outcome;
|
|
599
|
-
}
|
|
600
|
-
this.setState({
|
|
601
|
-
busy: "restore"
|
|
602
|
-
});
|
|
603
|
-
try {
|
|
604
|
-
const outcome = {
|
|
605
|
-
snapshotId: "wipe-all",
|
|
606
|
-
snapshotName: "Wipe all",
|
|
607
|
-
mode: "reload",
|
|
608
|
-
at: Date.now(),
|
|
609
|
-
willReload: false,
|
|
610
|
-
results: {}
|
|
611
|
-
};
|
|
612
|
-
for (const provider of getStateSnapshotProviders()) {
|
|
613
|
-
if (provider.clear) {
|
|
614
|
-
try {
|
|
615
|
-
outcome.results[provider.id] = await provider.clear();
|
|
616
|
-
} catch (e) {
|
|
617
|
-
outcome.results[provider.id] = skippedResult(String(e));
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
outcome.willReload = this.scheduleReload("buoy time-machine wipe all");
|
|
622
|
-
this.setState({
|
|
623
|
-
lastOutcome: outcome
|
|
624
|
-
});
|
|
625
|
-
return outcome;
|
|
626
|
-
} finally {
|
|
627
|
-
this.setState({
|
|
628
|
-
busy: null
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
clearPreview() {
|
|
633
|
-
this.previewRunId += 1;
|
|
634
|
-
this.setState({
|
|
635
|
-
preview: null
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
/**
|
|
640
|
-
* Build (or rebuild) the restore preview: item-level verdicts per source,
|
|
641
|
-
* filled in incrementally with a frame yield between sources so a big app
|
|
642
|
-
* never freezes the JS thread in one go.
|
|
643
|
-
*/
|
|
644
|
-
async buildPreview(right, left = "current") {
|
|
645
|
-
if (remoteHandler) {
|
|
646
|
-
// Buoy Desktop: the device builds it; result mirrors into state.
|
|
647
|
-
const runId = ++this.previewRunId;
|
|
648
|
-
this.setState({
|
|
649
|
-
preview: {
|
|
650
|
-
left,
|
|
651
|
-
right,
|
|
652
|
-
building: true,
|
|
653
|
-
builtAt: Date.now(),
|
|
654
|
-
sources: []
|
|
655
|
-
}
|
|
656
|
-
});
|
|
657
|
-
try {
|
|
658
|
-
const result = await remoteHandler("preview", {
|
|
659
|
-
id: right === "current" ? undefined : right.snapshotId,
|
|
660
|
-
compareTo: left === "current" ? undefined : left.snapshotId
|
|
661
|
-
});
|
|
662
|
-
if (runId !== this.previewRunId) return;
|
|
663
|
-
this.setState({
|
|
664
|
-
preview: {
|
|
665
|
-
left,
|
|
666
|
-
right,
|
|
667
|
-
building: false,
|
|
668
|
-
builtAt: Date.now(),
|
|
669
|
-
sources: result.sources ?? []
|
|
670
|
-
}
|
|
671
|
-
});
|
|
672
|
-
} catch (e) {
|
|
673
|
-
if (runId !== this.previewRunId) return;
|
|
674
|
-
this.setState({
|
|
675
|
-
preview: {
|
|
676
|
-
left,
|
|
677
|
-
right,
|
|
678
|
-
building: false,
|
|
679
|
-
builtAt: Date.now(),
|
|
680
|
-
sources: [{
|
|
681
|
-
id: "error",
|
|
682
|
-
label: "Preview",
|
|
683
|
-
items: [],
|
|
684
|
-
notes: [],
|
|
685
|
-
error: String(e)
|
|
686
|
-
}]
|
|
687
|
-
}
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
await this.ensureLoaded();
|
|
693
|
-
await ensureSourceSettingsLoaded();
|
|
694
|
-
const runId = ++this.previewRunId;
|
|
695
|
-
this.setState({
|
|
696
|
-
preview: {
|
|
697
|
-
left,
|
|
698
|
-
right,
|
|
699
|
-
building: true,
|
|
700
|
-
builtAt: Date.now(),
|
|
701
|
-
sources: []
|
|
702
|
-
}
|
|
703
|
-
});
|
|
704
|
-
const loadSide = async ref => {
|
|
705
|
-
if (ref === "current") return null; // providers capture live themselves
|
|
706
|
-
const stored = await loadSnapshot(ref.snapshotId);
|
|
707
|
-
if (!stored) throw new Error(`No snapshot with id "${ref.snapshotId}"`);
|
|
708
|
-
return stored.data;
|
|
709
|
-
};
|
|
710
|
-
try {
|
|
711
|
-
if (right === "current") throw new Error("preview target must be a snapshot");
|
|
712
|
-
const [leftData, rightStored] = await Promise.all([loadSide(left), loadSnapshot(right.snapshotId)]);
|
|
713
|
-
if (runId !== this.previewRunId) return;
|
|
714
|
-
if (!rightStored) {
|
|
715
|
-
throw new Error(`No snapshot with id "${right.snapshotId}"`);
|
|
716
|
-
}
|
|
717
|
-
const rightData = rightStored.data;
|
|
718
|
-
|
|
719
|
-
// Baseline = empty target: preview every clearable source against live,
|
|
720
|
-
// so each live key naturally reads as "removed" — the informed wipe.
|
|
721
|
-
const isBaseline = rightStored.meta.baseline === true;
|
|
722
|
-
const sourceIds = isBaseline ? getStateSnapshotProviders().filter(p => p.clear).map(p => p.id) : [...RESTORE_ORDER.filter(sid => sid in rightData), ...Object.keys(rightData).filter(sid => !RESTORE_ORDER.includes(sid))];
|
|
723
|
-
const sources = [];
|
|
724
|
-
for (const sid of sourceIds) {
|
|
725
|
-
// Off in Sources: a restore will not touch it, so there is nothing
|
|
726
|
-
// to preview.
|
|
727
|
-
if (!isSourceEnabled(sid)) continue;
|
|
728
|
-
// Yield a frame between sources — verdict passes are bounded but a
|
|
729
|
-
// five-source build shouldn't run as one synchronous block.
|
|
730
|
-
await new Promise(r => setTimeout(r, 0));
|
|
731
|
-
if (runId !== this.previewRunId) return;
|
|
732
|
-
const provider = getStateSnapshotProvider(sid);
|
|
733
|
-
const label = provider?.label ?? sid;
|
|
734
|
-
try {
|
|
735
|
-
const targetData = isBaseline ? undefined // provider treats an absent target as "empty"
|
|
736
|
-
: superJsonDeserialize(rightData[sid]);
|
|
737
|
-
const baseData = leftData === null ? undefined : sid in leftData ? superJsonDeserialize(leftData[sid]) : undefined;
|
|
738
|
-
let result;
|
|
739
|
-
if (!provider) {
|
|
740
|
-
result = {
|
|
741
|
-
items: [{
|
|
742
|
-
key: sid,
|
|
743
|
-
label,
|
|
744
|
-
verdict: "wont-apply",
|
|
745
|
-
reason: "tool not installed — restore skips this source"
|
|
746
|
-
}],
|
|
747
|
-
notes: []
|
|
748
|
-
};
|
|
749
|
-
} else if (provider.previewItems) {
|
|
750
|
-
result = await provider.previewItems(targetData, baseData);
|
|
751
|
-
} else {
|
|
752
|
-
// Whole-source fallback for custom providers without previewItems.
|
|
753
|
-
const diff = objectDiff(baseData, targetData);
|
|
754
|
-
result = {
|
|
755
|
-
items: [{
|
|
756
|
-
key: sid,
|
|
757
|
-
label,
|
|
758
|
-
verdict: diff.equal ? "unchanged" : "changed",
|
|
759
|
-
oldValue: baseData,
|
|
760
|
-
newValue: targetData
|
|
761
|
-
}],
|
|
762
|
-
notes: diff.truncated ? ["diff truncated (large value)"] : []
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
if (runId !== this.previewRunId) return;
|
|
766
|
-
sources.push({
|
|
767
|
-
id: sid,
|
|
768
|
-
label,
|
|
769
|
-
...result
|
|
770
|
-
});
|
|
771
|
-
} catch (e) {
|
|
772
|
-
sources.push({
|
|
773
|
-
id: sid,
|
|
774
|
-
label,
|
|
775
|
-
items: [],
|
|
776
|
-
notes: [],
|
|
777
|
-
error: String(e)
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
this.setState({
|
|
781
|
-
preview: {
|
|
782
|
-
left,
|
|
783
|
-
right,
|
|
784
|
-
building: true,
|
|
785
|
-
builtAt: Date.now(),
|
|
786
|
-
sources: [...sources]
|
|
787
|
-
}
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
if (runId !== this.previewRunId) return;
|
|
791
|
-
this.setState({
|
|
792
|
-
preview: {
|
|
793
|
-
left,
|
|
794
|
-
right,
|
|
795
|
-
building: false,
|
|
796
|
-
builtAt: Date.now(),
|
|
797
|
-
sources
|
|
798
|
-
}
|
|
799
|
-
});
|
|
800
|
-
} catch (e) {
|
|
801
|
-
if (runId !== this.previewRunId) return;
|
|
802
|
-
this.setState({
|
|
803
|
-
preview: {
|
|
804
|
-
left,
|
|
805
|
-
right,
|
|
806
|
-
building: false,
|
|
807
|
-
builtAt: Date.now(),
|
|
808
|
-
sources: [{
|
|
809
|
-
id: "error",
|
|
810
|
-
label: "Preview",
|
|
811
|
-
items: [],
|
|
812
|
-
notes: [],
|
|
813
|
-
error: String(e)
|
|
814
|
-
}]
|
|
815
|
-
}
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
export const timeMachineStore = new TimeMachineStore();
|
|
821
|
-
|
|
822
|
-
/**
|
|
823
|
-
* Buoy Desktop: route every Time Machine operation through a remote handler
|
|
824
|
-
* (the device's sync adapter actions) instead of running locally. Pass null
|
|
825
|
-
* to detach. While attached, feed device payloads to
|
|
826
|
-
* {@link replaceTimeMachineFromSnapshot}.
|
|
827
|
-
*/
|
|
828
|
-
export function setRemoteTimeMachineHandler(handler) {
|
|
829
|
-
remoteHandler = handler;
|
|
830
|
-
if (!handler) {
|
|
831
|
-
timeMachineStore.replaceFromSnapshot({});
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
/** Buoy Desktop: mirror a device's synced Time Machine metadata payload. */
|
|
836
|
-
export function replaceTimeMachineFromSnapshot(payload) {
|
|
837
|
-
timeMachineStore.replaceFromSnapshot(payload);
|
|
838
|
-
}
|
|
839
|
-
//# sourceMappingURL=timeMachineStore.js.map
|
|
1
|
+
"use strict";import{getStateSnapshotProvider as L,getStateSnapshotProviders as b,scheduleReloadApp as B,superJsonDeserialize as D,superJsonSerialize as M}from"@buoy-gg/shared-ui";import{objectDiff as _}from"@buoy-gg/shared-ui/dataViewer";import{deleteSnapshot as P,duplicateSnapshot as z,loadIndex as C,loadSnapshot as I,markSnapshotRestored as A,readSnapshot as F,renameSnapshot as H,restoreDeletedSnapshot as j,saveSnapshot as k,setSnapshotRestoreRoute as q,setSnapshotScope as J,setSnapshotExclusions as U}from"./snapshotVault";import{captureCurrentRoute as V,navigateToRoute as W,savePendingRoute as Y}from"./routeRestore";import{ensureSourceSettingsLoaded as w,isSourceEnabled as g}from"./sourceSettings";import{ROUTE_SOURCE_ID as $}from"./sourceIds";export const RESTORE_ORDER=["storage","redux","zustand","jotai","query"];function G(c){const t=new Date;const e=`${String(t.getHours()).padStart(2,"0")}:${String(t.getMinutes()).padStart(2,"0")}`;const s=c?c.pathname==="/"?"Home":c.pathname:null;return s?`${s} \xB7 ${e}`:`Snapshot \xB7 ${e}`}function Q(c,t){const e=new Set(t.map(s=>s.name));if(!e.has(c))return c;for(let s=2;s<1e3;s++){const l=`${c} (${s})`;if(!e.has(l))return l}return c}export function willRestoreRoute(c){return!!c.route&&(c.restoreRoute??true)}export const SAFETY_SNAPSHOT_NAME="Before last restore";let o=null;function R(c){return{ok:false,applied:0,skipped:[{id:"*",reason:c}],warnings:[]}}let x=0;function T(){x+=1;return`snap_${Date.now().toString(36)}_${x.toString(36)}`}class X{state={snapshots:[],hasLoaded:false,busy:null,lastOutcome:null,preview:null,remoteProviders:null,remoteRoute:null,recentlyDeleted:null};previewRunId=0;listeners=new Set;loadPromise=null;getState(){return this.state}subscribe=t=>{this.listeners.add(t);void this.ensureLoaded();return()=>{this.listeners.delete(t)}};setState(t){this.state={...this.state,...t};this.listeners.forEach(e=>{try{e()}catch{}})}ensureLoaded(){if(o)return Promise.resolve();if(!this.loadPromise){this.loadPromise=C().then(t=>{if(o)return;this.setState({snapshots:t,hasLoaded:true})})}return this.loadPromise}replaceFromSnapshot(t){this.setState({snapshots:t.snapshots??[],busy:t.busy??null,lastOutcome:t.lastOutcome??null,remoteProviders:t.providers??[],remoteRoute:t.route??{available:false},hasLoaded:true})}async capture(t,e,s){if(o){const l=await o("capture",{name:t,sourceIds:e});return l.snapshot}await this.ensureLoaded();await w();this.setState({busy:"capture"});try{const l=b().filter(r=>r.status().canCapture&&g(r.id)&&(!e||e.includes(r.id)));const u={};const a={};for(const r of l){try{const d=await r.capture();u[r.id]=M(d.data);a[r.id]={warnings:d.warnings}}catch(d){a[r.id]={warnings:[`capture failed: ${String(d)}`]}}}const i=g($)?V():null;let p=this.state.snapshots;if(s?.safety){for(const r of p.filter(d=>d.safety)){p=await P(r.id,p)}}const S={meta:{id:T(),name:s?.safety?SAFETY_SNAPSHOT_NAME:Q(t.trim()||G(i),p),createdAt:Date.now(),sizeBytes:0,...i?{route:i}:{},...s?.safety?{safety:true}:{},sources:a},data:u};const f=await k(S,p);this.setState({snapshots:f});return S.meta}finally{this.setState({busy:null})}}async captureBaseline(){if(o){const s=await o("captureBaseline",{});return s.snapshot}await this.ensureLoaded();await w();const t={meta:{id:T(),name:"Fresh install",createdAt:Date.now(),sizeBytes:0,baseline:true,sources:{}},data:{}};const e=await k(t,this.state.snapshots);this.setState({snapshots:e});return t.meta}async restore(t,e,s,l){if(o){const a=await o("restore",{id:t,mode:e,excludeKeys:s,restoreRoute:l?.restoreRoute});this.setState({lastOutcome:a});return a}await this.ensureLoaded();await w();let u;if(l?.safetyCapture&&!this.state.snapshots.find(a=>a.id===t)?.safety){u=(await this.capture(SAFETY_SNAPSHOT_NAME,void 0,{safety:true})).id}this.setState({busy:"restore"});try{const a=await I(t);if(!a)throw new Error(`No snapshot with id "${t}"`);const i={snapshotId:t,snapshotName:a.meta.name,mode:e,at:Date.now(),willReload:false,results:{},...u?{safetySnapshotId:u}:{}};if(a.meta.baseline){for(const n of b()){if(n.clear){try{i.results[n.id]=await n.clear()}catch(h){i.results[n.id]=R(String(h))}}}i.willReload=this.scheduleReload("buoy time-machine baseline");this.setState({lastOutcome:i});return i}const p=[...RESTORE_ORDER.filter(n=>n in a.data),...Object.keys(a.data).filter(n=>!RESTORE_ORDER.includes(n))];const S=n=>{const h=new Map;for(const m of n){const v=m.indexOf("::");if(v<=0)continue;const O=m.slice(0,v);const N=m.slice(v+2);const E=h.get(O)??[];E.push(N);h.set(O,E)}return h};const f=s??a.meta.excludedKeys??[];const r=S(f);const d=!s&&a.meta.scope?.length?S(a.meta.scope):null;const y=g($)&&(l?.restoreRoute??willRestoreRoute(a.meta));if(e==="reload"){if(a.data.storage&&g("storage")&&(!d||d.has("storage"))){i.results.storage=await this.restoreSource("storage",a.data.storage,r.get("storage"),d?.get("storage"))}if(y){const n=a.meta.route;try{await Y(n,a.meta.name);i.route={...n,status:"deferred"}}catch(h){i.route={...n,status:"failed",error:String(h)}}}i.willReload=this.scheduleReload("buoy time-machine restore");this.setState({lastOutcome:i,snapshots:await A(t,i.at,this.state.snapshots)});return i}for(const n of p){if(d&&!d.has(n))continue;if(!g(n)){i.results[n]={ok:true,applied:0,skipped:[],warnings:["Switched off in Sources; left as it is"]};continue}i.results[n]=await this.restoreSource(n,a.data[n],r.get(n),d?.get(n))}if(y){i.route=W(a.meta.route)}this.setState({lastOutcome:i,snapshots:await A(t,i.at,this.state.snapshots)});return i}finally{this.setState({busy:null})}}async restoreSource(t,e,s,l){const u=L(t);if(!u){return R("provider not available (package missing or not initialized)")}const a=u.status();if(!a.canRestore){return R(a.detail??"restore unavailable")}try{const i=s?.length||l?.length?{...s?.length?{excludeKeys:s}:{},...l?.length?{includeKeys:l}:{}}:void 0;return await u.restore(D(e),i)}catch(i){return R(String(i))}}scheduleReload(t){try{B({reason:t});return true}catch{return false}}async delete(t){if(o){await o("delete",{id:t});return}await this.ensureLoaded();await w();const e=await F(t);const s=await P(t,this.state.snapshots);this.setState({snapshots:s,recentlyDeleted:e?{stored:e,at:Date.now()}:null})}async undoDelete(){const t=this.state.recentlyDeleted;if(!t)return;const e=await j(t.stored,this.state.snapshots);this.setState({snapshots:e,recentlyDeleted:null})}clearRecentlyDeleted(){if(this.state.recentlyDeleted)this.setState({recentlyDeleted:null})}async setRestoreRoute(t,e){if(o){await o("setRestoreRoute",{id:t,restoreRoute:e});return}await this.ensureLoaded();await w();const s=await q(t,e,this.state.snapshots);this.setState({snapshots:s})}async duplicate(t){if(o){const l=await o("duplicate",{id:t});return l?.snapshot??null}await this.ensureLoaded();await w();const e=this.state.snapshots.find(l=>l.id===t);if(!e)return null;const s=await z(t,`${e.name} copy`,this.state.snapshots);if(!s)return null;this.setState({snapshots:s.metas});return s.meta}async rename(t,e){if(o){await o("rename",{id:t,name:e});return}await this.ensureLoaded();await w();const s=await H(t,e,this.state.snapshots);this.setState({snapshots:s})}async setScope(t,e){if(o){await o("setScope",{id:t,scope:e});return}await this.ensureLoaded();await w();const s=await J(t,e,this.state.snapshots);this.setState({snapshots:s})}async setExclusions(t,e){if(o){await o("setExclusions",{id:t,excludedKeys:e});return}await this.ensureLoaded();await w();const s=await U(t,e,this.state.snapshots);this.setState({snapshots:s})}clearLastOutcome(){this.setState({lastOutcome:null})}async wipeAll(){if(o){const t=await o("wipeAll",{});this.setState({lastOutcome:t});return t}this.setState({busy:"restore"});try{const t={snapshotId:"wipe-all",snapshotName:"Wipe all",mode:"reload",at:Date.now(),willReload:false,results:{}};for(const e of b()){if(e.clear){try{t.results[e.id]=await e.clear()}catch(s){t.results[e.id]=R(String(s))}}}t.willReload=this.scheduleReload("buoy time-machine wipe all");this.setState({lastOutcome:t});return t}finally{this.setState({busy:null})}}clearPreview(){this.previewRunId+=1;this.setState({preview:null})}async buildPreview(t,e="current"){if(o){const u=++this.previewRunId;this.setState({preview:{left:e,right:t,building:true,builtAt:Date.now(),sources:[]}});try{const a=await o("preview",{id:t==="current"?void 0:t.snapshotId,compareTo:e==="current"?void 0:e.snapshotId});if(u!==this.previewRunId)return;this.setState({preview:{left:e,right:t,building:false,builtAt:Date.now(),sources:a.sources??[]}})}catch(a){if(u!==this.previewRunId)return;this.setState({preview:{left:e,right:t,building:false,builtAt:Date.now(),sources:[{id:"error",label:"Preview",items:[],notes:[],error:String(a)}]}})}return}await this.ensureLoaded();await w();const s=++this.previewRunId;this.setState({preview:{left:e,right:t,building:true,builtAt:Date.now(),sources:[]}});const l=async u=>{if(u==="current")return null;const a=await I(u.snapshotId);if(!a)throw new Error(`No snapshot with id "${u.snapshotId}"`);return a.data};try{if(t==="current")throw new Error("preview target must be a snapshot");const[u,a]=await Promise.all([l(e),I(t.snapshotId)]);if(s!==this.previewRunId)return;if(!a){throw new Error(`No snapshot with id "${t.snapshotId}"`)}const i=a.data;const p=a.meta.baseline===true;const S=p?b().filter(r=>r.clear).map(r=>r.id):[...RESTORE_ORDER.filter(r=>r in i),...Object.keys(i).filter(r=>!RESTORE_ORDER.includes(r))];const f=[];for(const r of S){if(!g(r))continue;await new Promise(n=>setTimeout(n,0));if(s!==this.previewRunId)return;const d=L(r);const y=d?.label??r;try{const n=p?void 0:D(i[r]);const h=u===null?void 0:r in u?D(u[r]):void 0;let m;if(!d){m={items:[{key:r,label:y,verdict:"wont-apply",reason:"tool not installed \u2014 restore skips this source"}],notes:[]}}else if(d.previewItems){m=await d.previewItems(n,h)}else{const v=_(h,n);m={items:[{key:r,label:y,verdict:v.equal?"unchanged":"changed",oldValue:h,newValue:n}],notes:v.truncated?["diff truncated (large value)"]:[]}}if(s!==this.previewRunId)return;f.push({id:r,label:y,...m})}catch(n){f.push({id:r,label:y,items:[],notes:[],error:String(n)})}this.setState({preview:{left:e,right:t,building:true,builtAt:Date.now(),sources:[...f]}})}if(s!==this.previewRunId)return;this.setState({preview:{left:e,right:t,building:false,builtAt:Date.now(),sources:f}})}catch(u){if(s!==this.previewRunId)return;this.setState({preview:{left:e,right:t,building:false,builtAt:Date.now(),sources:[{id:"error",label:"Preview",items:[],notes:[],error:String(u)}]}})}}}export const timeMachineStore=new X;export function setRemoteTimeMachineHandler(c){o=c;if(!c){timeMachineStore.replaceFromSnapshot({})}}export function replaceTimeMachineFromSnapshot(c){timeMachineStore.replaceFromSnapshot(c)}
|