@buoy-gg/time-machine 7.0.40 → 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.
Files changed (173) hide show
  1. package/README.md +4 -0
  2. package/lib/commonjs/components/HeaderActions.js +1 -91
  3. package/lib/commonjs/components/HeaderReloadButton.js +1 -58
  4. package/lib/commonjs/components/TimeMachineBody.js +1 -282
  5. package/lib/commonjs/components/TimeMachineModal.js +1 -74
  6. package/lib/commonjs/components/confirm.js +2 -38
  7. package/lib/commonjs/components/floating/NamePromptModal.js +1 -193
  8. package/lib/commonjs/components/floating/TimeMachineFloatingTool.js +1 -715
  9. package/lib/commonjs/components/floating/armedPoint.js +1 -92
  10. package/lib/commonjs/components/floating/barSettings.js +1 -66
  11. package/lib/commonjs/components/format.js +1 -24
  12. package/lib/commonjs/components/list/CaptureBar.js +1 -97
  13. package/lib/commonjs/components/list/RestoreOutcomeCard.js +1 -175
  14. package/lib/commonjs/components/list/SnapshotRow.js +1 -170
  15. package/lib/commonjs/components/list/SourcesStrip.js +1 -172
  16. package/lib/commonjs/components/list/UndoDeleteBar.js +1 -85
  17. package/lib/commonjs/components/list/WipeAllRow.js +1 -46
  18. package/lib/commonjs/components/preview/ItemDiffPanel.js +1 -79
  19. package/lib/commonjs/components/preview/PreviewFlow.js +1 -51
  20. package/lib/commonjs/components/preview/RestorePointNameRow.js +1 -83
  21. package/lib/commonjs/components/preview/RestorePreviewScreen.js +1 -1010
  22. package/lib/commonjs/components/preview/verdictMeta.js +1 -58
  23. package/lib/commonjs/components/sources.js +1 -83
  24. package/lib/commonjs/hooks/useTimeMachine.js +1 -24
  25. package/lib/commonjs/index.js +1 -161
  26. package/lib/commonjs/preset.js +1 -56
  27. package/lib/commonjs/store/routeRestore.js +1 -188
  28. package/lib/commonjs/store/snapshotVault.js +1 -186
  29. package/lib/commonjs/store/sourceIds.js +1 -9
  30. package/lib/commonjs/store/sourceSettings.js +1 -79
  31. package/lib/commonjs/store/timeMachineStore.js +1 -848
  32. package/lib/commonjs/sync/timeMachineSyncAdapter.js +1 -258
  33. package/lib/commonjs/ui/SwipeActionsRow.js +1 -178
  34. package/lib/commonjs/ui/primitives.js +1 -172
  35. package/lib/commonjs/ui/tmTheme.js +1 -118
  36. package/lib/module/components/HeaderActions.js +1 -85
  37. package/lib/module/components/HeaderReloadButton.js +1 -53
  38. package/lib/module/components/TimeMachineBody.js +1 -277
  39. package/lib/module/components/TimeMachineModal.js +1 -68
  40. package/lib/module/components/confirm.js +2 -33
  41. package/lib/module/components/floating/NamePromptModal.js +1 -188
  42. package/lib/module/components/floating/TimeMachineFloatingTool.js +1 -708
  43. package/lib/module/components/floating/armedPoint.js +1 -82
  44. package/lib/module/components/floating/barSettings.js +1 -57
  45. package/lib/module/components/format.js +1 -19
  46. package/lib/module/components/list/CaptureBar.js +1 -92
  47. package/lib/module/components/list/RestoreOutcomeCard.js +1 -169
  48. package/lib/module/components/list/SnapshotRow.js +1 -167
  49. package/lib/module/components/list/SourcesStrip.js +1 -169
  50. package/lib/module/components/list/UndoDeleteBar.js +1 -82
  51. package/lib/module/components/list/WipeAllRow.js +1 -41
  52. package/lib/module/components/preview/ItemDiffPanel.js +1 -74
  53. package/lib/module/components/preview/PreviewFlow.js +1 -46
  54. package/lib/module/components/preview/RestorePointNameRow.js +1 -78
  55. package/lib/module/components/preview/RestorePreviewScreen.js +1 -1005
  56. package/lib/module/components/preview/verdictMeta.js +1 -54
  57. package/lib/module/components/sources.js +1 -73
  58. package/lib/module/hooks/useTimeMachine.js +1 -19
  59. package/lib/module/index.js +1 -46
  60. package/lib/module/preset.js +1 -51
  61. package/lib/module/store/routeRestore.js +1 -176
  62. package/lib/module/store/snapshotVault.js +1 -170
  63. package/lib/module/store/sourceIds.js +1 -5
  64. package/lib/module/store/sourceSettings.js +1 -69
  65. package/lib/module/store/timeMachineStore.js +1 -839
  66. package/lib/module/sync/timeMachineSyncAdapter.js +1 -253
  67. package/lib/module/ui/SwipeActionsRow.js +1 -173
  68. package/lib/module/ui/primitives.js +1 -164
  69. package/lib/module/ui/tmTheme.js +1 -114
  70. package/lib/web/index.mjs +3 -0
  71. package/package.json +43 -10
  72. package/lib/commonjs/components/HeaderActions.js.map +0 -1
  73. package/lib/commonjs/components/HeaderReloadButton.js.map +0 -1
  74. package/lib/commonjs/components/TimeMachineBody.js.map +0 -1
  75. package/lib/commonjs/components/TimeMachineModal.js.map +0 -1
  76. package/lib/commonjs/components/confirm.js.map +0 -1
  77. package/lib/commonjs/components/floating/NamePromptModal.js.map +0 -1
  78. package/lib/commonjs/components/floating/TimeMachineFloatingTool.js.map +0 -1
  79. package/lib/commonjs/components/floating/armedPoint.js.map +0 -1
  80. package/lib/commonjs/components/floating/barSettings.js.map +0 -1
  81. package/lib/commonjs/components/format.js.map +0 -1
  82. package/lib/commonjs/components/list/CaptureBar.js.map +0 -1
  83. package/lib/commonjs/components/list/RestoreOutcomeCard.js.map +0 -1
  84. package/lib/commonjs/components/list/SnapshotRow.js.map +0 -1
  85. package/lib/commonjs/components/list/SourcesStrip.js.map +0 -1
  86. package/lib/commonjs/components/list/UndoDeleteBar.js.map +0 -1
  87. package/lib/commonjs/components/list/WipeAllRow.js.map +0 -1
  88. package/lib/commonjs/components/preview/ItemDiffPanel.js.map +0 -1
  89. package/lib/commonjs/components/preview/PreviewFlow.js.map +0 -1
  90. package/lib/commonjs/components/preview/RestorePointNameRow.js.map +0 -1
  91. package/lib/commonjs/components/preview/RestorePreviewScreen.js.map +0 -1
  92. package/lib/commonjs/components/preview/verdictMeta.js.map +0 -1
  93. package/lib/commonjs/components/sources.js.map +0 -1
  94. package/lib/commonjs/hooks/useTimeMachine.js.map +0 -1
  95. package/lib/commonjs/index.js.map +0 -1
  96. package/lib/commonjs/preset.js.map +0 -1
  97. package/lib/commonjs/store/routeRestore.js.map +0 -1
  98. package/lib/commonjs/store/snapshotVault.js.map +0 -1
  99. package/lib/commonjs/store/sourceIds.js.map +0 -1
  100. package/lib/commonjs/store/sourceSettings.js.map +0 -1
  101. package/lib/commonjs/store/timeMachineStore.js.map +0 -1
  102. package/lib/commonjs/sync/timeMachineSyncAdapter.js.map +0 -1
  103. package/lib/commonjs/ui/SwipeActionsRow.js.map +0 -1
  104. package/lib/commonjs/ui/primitives.js.map +0 -1
  105. package/lib/commonjs/ui/tmTheme.js.map +0 -1
  106. package/lib/module/components/HeaderActions.js.map +0 -1
  107. package/lib/module/components/HeaderReloadButton.js.map +0 -1
  108. package/lib/module/components/TimeMachineBody.js.map +0 -1
  109. package/lib/module/components/TimeMachineModal.js.map +0 -1
  110. package/lib/module/components/confirm.js.map +0 -1
  111. package/lib/module/components/floating/NamePromptModal.js.map +0 -1
  112. package/lib/module/components/floating/TimeMachineFloatingTool.js.map +0 -1
  113. package/lib/module/components/floating/armedPoint.js.map +0 -1
  114. package/lib/module/components/floating/barSettings.js.map +0 -1
  115. package/lib/module/components/format.js.map +0 -1
  116. package/lib/module/components/list/CaptureBar.js.map +0 -1
  117. package/lib/module/components/list/RestoreOutcomeCard.js.map +0 -1
  118. package/lib/module/components/list/SnapshotRow.js.map +0 -1
  119. package/lib/module/components/list/SourcesStrip.js.map +0 -1
  120. package/lib/module/components/list/UndoDeleteBar.js.map +0 -1
  121. package/lib/module/components/list/WipeAllRow.js.map +0 -1
  122. package/lib/module/components/preview/ItemDiffPanel.js.map +0 -1
  123. package/lib/module/components/preview/PreviewFlow.js.map +0 -1
  124. package/lib/module/components/preview/RestorePointNameRow.js.map +0 -1
  125. package/lib/module/components/preview/RestorePreviewScreen.js.map +0 -1
  126. package/lib/module/components/preview/verdictMeta.js.map +0 -1
  127. package/lib/module/components/sources.js.map +0 -1
  128. package/lib/module/hooks/useTimeMachine.js.map +0 -1
  129. package/lib/module/index.js.map +0 -1
  130. package/lib/module/preset.js.map +0 -1
  131. package/lib/module/store/routeRestore.js.map +0 -1
  132. package/lib/module/store/snapshotVault.js.map +0 -1
  133. package/lib/module/store/sourceIds.js.map +0 -1
  134. package/lib/module/store/sourceSettings.js.map +0 -1
  135. package/lib/module/store/timeMachineStore.js.map +0 -1
  136. package/lib/module/sync/timeMachineSyncAdapter.js.map +0 -1
  137. package/lib/module/ui/SwipeActionsRow.js.map +0 -1
  138. package/lib/module/ui/primitives.js.map +0 -1
  139. package/lib/module/ui/tmTheme.js.map +0 -1
  140. package/lib/typescript/components/HeaderActions.d.ts.map +0 -1
  141. package/lib/typescript/components/HeaderReloadButton.d.ts.map +0 -1
  142. package/lib/typescript/components/TimeMachineBody.d.ts.map +0 -1
  143. package/lib/typescript/components/TimeMachineModal.d.ts.map +0 -1
  144. package/lib/typescript/components/confirm.d.ts.map +0 -1
  145. package/lib/typescript/components/floating/NamePromptModal.d.ts.map +0 -1
  146. package/lib/typescript/components/floating/TimeMachineFloatingTool.d.ts.map +0 -1
  147. package/lib/typescript/components/floating/armedPoint.d.ts.map +0 -1
  148. package/lib/typescript/components/floating/barSettings.d.ts.map +0 -1
  149. package/lib/typescript/components/format.d.ts.map +0 -1
  150. package/lib/typescript/components/list/CaptureBar.d.ts.map +0 -1
  151. package/lib/typescript/components/list/RestoreOutcomeCard.d.ts.map +0 -1
  152. package/lib/typescript/components/list/SnapshotRow.d.ts.map +0 -1
  153. package/lib/typescript/components/list/SourcesStrip.d.ts.map +0 -1
  154. package/lib/typescript/components/list/UndoDeleteBar.d.ts.map +0 -1
  155. package/lib/typescript/components/list/WipeAllRow.d.ts.map +0 -1
  156. package/lib/typescript/components/preview/ItemDiffPanel.d.ts.map +0 -1
  157. package/lib/typescript/components/preview/PreviewFlow.d.ts.map +0 -1
  158. package/lib/typescript/components/preview/RestorePointNameRow.d.ts.map +0 -1
  159. package/lib/typescript/components/preview/RestorePreviewScreen.d.ts.map +0 -1
  160. package/lib/typescript/components/preview/verdictMeta.d.ts.map +0 -1
  161. package/lib/typescript/components/sources.d.ts.map +0 -1
  162. package/lib/typescript/hooks/useTimeMachine.d.ts.map +0 -1
  163. package/lib/typescript/index.d.ts.map +0 -1
  164. package/lib/typescript/preset.d.ts.map +0 -1
  165. package/lib/typescript/store/routeRestore.d.ts.map +0 -1
  166. package/lib/typescript/store/snapshotVault.d.ts.map +0 -1
  167. package/lib/typescript/store/sourceIds.d.ts.map +0 -1
  168. package/lib/typescript/store/sourceSettings.d.ts.map +0 -1
  169. package/lib/typescript/store/timeMachineStore.d.ts.map +0 -1
  170. package/lib/typescript/sync/timeMachineSyncAdapter.d.ts.map +0 -1
  171. package/lib/typescript/ui/SwipeActionsRow.d.ts.map +0 -1
  172. package/lib/typescript/ui/primitives.d.ts.map +0 -1
  173. 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)}