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