@buoy-gg/time-machine 7.0.20 → 7.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/components/TimeMachineBody.js +103 -8
- package/lib/commonjs/components/TimeMachineBody.js.map +1 -1
- package/lib/commonjs/index.js +55 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/store/routeRestore.js +188 -0
- package/lib/commonjs/store/routeRestore.js.map +1 -0
- package/lib/commonjs/store/snapshotVault.js.map +1 -1
- package/lib/commonjs/store/timeMachineStore.js +41 -3
- package/lib/commonjs/store/timeMachineStore.js.map +1 -1
- package/lib/commonjs/sync/timeMachineSyncAdapter.js +18 -4
- package/lib/commonjs/sync/timeMachineSyncAdapter.js.map +1 -1
- package/lib/module/components/TimeMachineBody.js +104 -9
- package/lib/module/components/TimeMachineBody.js.map +1 -1
- package/lib/module/index.js +11 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/store/routeRestore.js +176 -0
- package/lib/module/store/routeRestore.js.map +1 -0
- package/lib/module/store/snapshotVault.js.map +1 -1
- package/lib/module/store/timeMachineStore.js +41 -3
- package/lib/module/store/timeMachineStore.js.map +1 -1
- package/lib/module/sync/timeMachineSyncAdapter.js +18 -4
- package/lib/module/sync/timeMachineSyncAdapter.js.map +1 -1
- package/lib/typescript/components/TimeMachineBody.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/store/routeRestore.d.ts +51 -0
- package/lib/typescript/store/routeRestore.d.ts.map +1 -0
- package/lib/typescript/store/snapshotVault.d.ts +7 -0
- package/lib/typescript/store/snapshotVault.d.ts.map +1 -1
- package/lib/typescript/store/timeMachineStore.d.ts +29 -1
- package/lib/typescript/store/timeMachineStore.d.ts.map +1 -1
- package/lib/typescript/sync/timeMachineSyncAdapter.d.ts +8 -0
- package/lib/typescript/sync/timeMachineSyncAdapter.d.ts.map +1 -1
- package/package.json +12 -8
- package/src/components/TimeMachineBody.tsx +146 -10
- package/src/index.tsx +22 -0
- package/src/store/routeRestore.ts +194 -0
- package/src/store/snapshotVault.ts +7 -0
- package/src/store/timeMachineStore.ts +60 -1
- package/src/sync/timeMachineSyncAdapter.ts +27 -4
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route capture & restore for Time Machine.
|
|
3
|
+
*
|
|
4
|
+
* A restore point is only half the story: state without the screen you were
|
|
5
|
+
* on drops you into the right data on the wrong page. Every snapshot records
|
|
6
|
+
* the route it was captured on, and a restore can navigate back to it.
|
|
7
|
+
*
|
|
8
|
+
* Two navigation paths, because a "reload" restore kills the JS realm:
|
|
9
|
+
* - LIVE restore: navigate immediately once the sources are applied.
|
|
10
|
+
* - RELOAD restore: persist the route BEFORE the reload is scheduled, then
|
|
11
|
+
* {@link armPendingRouteRestore} picks it up on the next boot, waits for the
|
|
12
|
+
* router to come online, and navigates.
|
|
13
|
+
*
|
|
14
|
+
* Everything here is best-effort: apps without expo-router capture no route,
|
|
15
|
+
* so the UI simply never offers the option.
|
|
16
|
+
*/
|
|
17
|
+
import {
|
|
18
|
+
getSafeCurrentRoute,
|
|
19
|
+
getSafeRouter,
|
|
20
|
+
persistentStorage,
|
|
21
|
+
type SafeRouteLocation,
|
|
22
|
+
} from "@buoy-gg/shared-ui";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The route a snapshot was captured on. Only the URL is stored — imperative
|
|
26
|
+
* params passed as objects (`navigate(path, { user })`) are not part of a
|
|
27
|
+
* route and are not restored.
|
|
28
|
+
*/
|
|
29
|
+
export interface SnapshotRoute {
|
|
30
|
+
/** Pathname with dynamic segments resolved, e.g. `/pokemon/25`. */
|
|
31
|
+
pathname: string;
|
|
32
|
+
/** What we navigate to — pathname plus search params. */
|
|
33
|
+
href: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** How a restore's route leg ended, reported back in the RestoreOutcome. */
|
|
37
|
+
export type RouteRestoreStatus =
|
|
38
|
+
| "navigated"
|
|
39
|
+
| "deferred"
|
|
40
|
+
| "failed"
|
|
41
|
+
| "unavailable";
|
|
42
|
+
|
|
43
|
+
export interface RouteRestoreResult {
|
|
44
|
+
pathname: string;
|
|
45
|
+
href: string;
|
|
46
|
+
status: RouteRestoreStatus;
|
|
47
|
+
/** Present when status is "failed" / "unavailable". */
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Same `@react_buoy` family as the snapshot vault, so every tool's
|
|
53
|
+
* isDevToolsStorageKey filter (and therefore every wipe/restore) leaves it
|
|
54
|
+
* alone — a pending route must survive the storage restore that precedes the
|
|
55
|
+
* reload it's waiting on.
|
|
56
|
+
*/
|
|
57
|
+
const PENDING_KEY = "@react_buoy_time_machine:pending-route";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A pending route older than this belongs to a reload that never happened
|
|
61
|
+
* (the app was killed, the user moved on). Boot-time navigation into a screen
|
|
62
|
+
* someone left minutes ago is worse than doing nothing.
|
|
63
|
+
*/
|
|
64
|
+
export const PENDING_ROUTE_TTL_MS = 60_000;
|
|
65
|
+
|
|
66
|
+
/** How long to wait at boot for the router to come online. */
|
|
67
|
+
const ROUTER_READY_TIMEOUT_MS = 8_000;
|
|
68
|
+
|
|
69
|
+
/** Settle window after the router appears, so the initial route mounts first. */
|
|
70
|
+
const ROUTER_SETTLE_MS = 250;
|
|
71
|
+
|
|
72
|
+
interface PendingRoute extends SnapshotRoute {
|
|
73
|
+
savedAt: number;
|
|
74
|
+
snapshotName?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toSnapshotRoute(location: SafeRouteLocation): SnapshotRoute {
|
|
78
|
+
return { pathname: location.pathname, href: location.href };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The route to stamp on a snapshot being captured, or null when unavailable. */
|
|
82
|
+
export function captureCurrentRoute(): SnapshotRoute | null {
|
|
83
|
+
const current = getSafeCurrentRoute();
|
|
84
|
+
return current ? toSnapshotRoute(current) : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The live route, for "you're already here" checks in the UI. */
|
|
88
|
+
export function currentRoute(): SnapshotRoute | null {
|
|
89
|
+
return captureCurrentRoute();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** True when imperative navigation is possible on this device. */
|
|
93
|
+
export function canNavigate(): boolean {
|
|
94
|
+
return getSafeRouter() != null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Navigate to a captured route now. Uses `navigate` (not `push`) so restoring
|
|
99
|
+
* the same route twice doesn't stack duplicate screens.
|
|
100
|
+
*/
|
|
101
|
+
export function navigateToRoute(route: SnapshotRoute): RouteRestoreResult {
|
|
102
|
+
const base = { pathname: route.pathname, href: route.href };
|
|
103
|
+
const router = getSafeRouter();
|
|
104
|
+
if (!router?.navigate) {
|
|
105
|
+
return {
|
|
106
|
+
...base,
|
|
107
|
+
status: "unavailable",
|
|
108
|
+
error: "expo-router is not available on this device",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
router.navigate(route.href);
|
|
113
|
+
return { ...base, status: "navigated" };
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return { ...base, status: "failed", error: String(e) };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Pending route (survives the reload) ──────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
export async function savePendingRoute(
|
|
122
|
+
route: SnapshotRoute,
|
|
123
|
+
snapshotName?: string
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
const pending: PendingRoute = {
|
|
126
|
+
...route,
|
|
127
|
+
savedAt: Date.now(),
|
|
128
|
+
...(snapshotName ? { snapshotName } : {}),
|
|
129
|
+
};
|
|
130
|
+
await persistentStorage.setItem(PENDING_KEY, JSON.stringify(pending));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function clearPendingRoute(): Promise<void> {
|
|
134
|
+
try {
|
|
135
|
+
await persistentStorage.removeItem(PENDING_KEY);
|
|
136
|
+
} catch {
|
|
137
|
+
// best-effort — a stale key is dropped by the TTL check anyway
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function loadPendingRoute(): Promise<PendingRoute | null> {
|
|
142
|
+
try {
|
|
143
|
+
const raw = await persistentStorage.getItem(PENDING_KEY);
|
|
144
|
+
if (!raw) return null;
|
|
145
|
+
const parsed = JSON.parse(raw) as Partial<PendingRoute>;
|
|
146
|
+
if (typeof parsed?.href !== "string" || !parsed.href) return null;
|
|
147
|
+
if (typeof parsed.savedAt !== "number") return null;
|
|
148
|
+
return {
|
|
149
|
+
pathname:
|
|
150
|
+
typeof parsed.pathname === "string" ? parsed.pathname : parsed.href,
|
|
151
|
+
href: parsed.href,
|
|
152
|
+
savedAt: parsed.savedAt,
|
|
153
|
+
snapshotName:
|
|
154
|
+
typeof parsed.snapshotName === "string" ? parsed.snapshotName : undefined,
|
|
155
|
+
};
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let armed = false;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Boot-time consumer of a pending route: called once when the package loads
|
|
165
|
+
* (core requires it while mounting the floating menu), it navigates to the
|
|
166
|
+
* route a reload-mode restore asked for and clears the slot.
|
|
167
|
+
*
|
|
168
|
+
* Idempotent and self-limiting — the slot is cleared before navigating, so a
|
|
169
|
+
* route that crashes the app on arrival can't put it in a boot loop.
|
|
170
|
+
*/
|
|
171
|
+
export function armPendingRouteRestore(): void {
|
|
172
|
+
if (armed) return;
|
|
173
|
+
armed = true;
|
|
174
|
+
void (async () => {
|
|
175
|
+
const pending = await loadPendingRoute();
|
|
176
|
+
if (!pending) return;
|
|
177
|
+
// Clear FIRST: whatever happens next, this route is spent.
|
|
178
|
+
await clearPendingRoute();
|
|
179
|
+
if (Date.now() - pending.savedAt > PENDING_ROUTE_TTL_MS) return;
|
|
180
|
+
|
|
181
|
+
const deadline = Date.now() + ROUTER_READY_TIMEOUT_MS;
|
|
182
|
+
while (!canNavigate() && Date.now() < deadline) {
|
|
183
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
184
|
+
}
|
|
185
|
+
if (!canNavigate()) return;
|
|
186
|
+
await new Promise((r) => setTimeout(r, ROUTER_SETTLE_MS));
|
|
187
|
+
navigateToRoute({ pathname: pending.pathname, href: pending.href });
|
|
188
|
+
})();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** @internal test seam — re-arms the boot-time consumer. */
|
|
192
|
+
export function resetPendingRouteArm(): void {
|
|
193
|
+
armed = false;
|
|
194
|
+
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
persistentStorage,
|
|
19
19
|
type SuperJSONResult,
|
|
20
20
|
} from "@buoy-gg/shared-ui";
|
|
21
|
+
import type { SnapshotRoute } from "./routeRestore";
|
|
21
22
|
|
|
22
23
|
const INDEX_KEY = "@react_buoy_time_machine:index";
|
|
23
24
|
const SNAP_PREFIX = "@react_buoy_time_machine:snap:";
|
|
@@ -43,6 +44,12 @@ export interface SnapshotMeta {
|
|
|
43
44
|
* means just my cart". Absent = full restore.
|
|
44
45
|
*/
|
|
45
46
|
scope?: string[];
|
|
47
|
+
/**
|
|
48
|
+
* The route the app was on when this snapshot was captured. Absent when the
|
|
49
|
+
* app has no expo-router (nothing to return to). A restore can navigate back
|
|
50
|
+
* to it — see routeRestore.ts.
|
|
51
|
+
*/
|
|
52
|
+
route?: SnapshotRoute;
|
|
46
53
|
/** Source ids captured, with their capture warnings. */
|
|
47
54
|
sources: Record<string, SnapshotSourceMeta>;
|
|
48
55
|
}
|
|
@@ -36,6 +36,12 @@ import {
|
|
|
36
36
|
type SnapshotMeta,
|
|
37
37
|
type StoredSnapshot,
|
|
38
38
|
} from "./snapshotVault";
|
|
39
|
+
import {
|
|
40
|
+
captureCurrentRoute,
|
|
41
|
+
navigateToRoute,
|
|
42
|
+
savePendingRoute,
|
|
43
|
+
type RouteRestoreResult,
|
|
44
|
+
} from "./routeRestore";
|
|
39
45
|
|
|
40
46
|
export const RESTORE_ORDER = ["storage", "redux", "zustand", "jotai", "query"];
|
|
41
47
|
|
|
@@ -49,6 +55,21 @@ export interface RestoreOutcome {
|
|
|
49
55
|
/** True when a JS reload was scheduled as part of this restore. */
|
|
50
56
|
willReload: boolean;
|
|
51
57
|
results: Record<string, StateSnapshotRestoreResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Present when the caller asked to also return to the snapshot's route.
|
|
60
|
+
* "deferred" means the navigation is queued for the post-reload boot.
|
|
61
|
+
*/
|
|
62
|
+
route?: RouteRestoreResult;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Extra knobs a restore accepts beyond mode + exclusions. */
|
|
66
|
+
export interface RestoreOptions {
|
|
67
|
+
/**
|
|
68
|
+
* Also navigate back to the route the snapshot was captured on. Ignored for
|
|
69
|
+
* snapshots with no captured route (and for baselines, which wipe to the
|
|
70
|
+
* app's own start route).
|
|
71
|
+
*/
|
|
72
|
+
restoreRoute?: boolean;
|
|
52
73
|
}
|
|
53
74
|
|
|
54
75
|
/** Provider capability row as mirrored to a remote dashboard. */
|
|
@@ -92,6 +113,18 @@ export interface TimeMachineState {
|
|
|
92
113
|
* registry, and every operation forwards over the socket.
|
|
93
114
|
*/
|
|
94
115
|
remoteProviders: RemoteProviderStatus[] | null;
|
|
116
|
+
/**
|
|
117
|
+
* Route capture/restore availability, mirrored from the device's payload
|
|
118
|
+
* (Buoy Desktop). Null on-device, where the live router is probed directly.
|
|
119
|
+
*/
|
|
120
|
+
remoteRoute: RouteToolStatus | null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Whether this device can capture and return to routes, and where it is now. */
|
|
124
|
+
export interface RouteToolStatus {
|
|
125
|
+
available: boolean;
|
|
126
|
+
/** The route the app is on right now, when one can be read. */
|
|
127
|
+
current?: string;
|
|
95
128
|
}
|
|
96
129
|
|
|
97
130
|
type RemoteHandler = (
|
|
@@ -119,6 +152,7 @@ class TimeMachineStore {
|
|
|
119
152
|
lastOutcome: null,
|
|
120
153
|
preview: null,
|
|
121
154
|
remoteProviders: null,
|
|
155
|
+
remoteRoute: null,
|
|
122
156
|
};
|
|
123
157
|
/** Monotonic token — a preview build checks it after every await and
|
|
124
158
|
* abandons itself when a newer build (or clearPreview) supersedes it. */
|
|
@@ -166,12 +200,14 @@ class TimeMachineStore {
|
|
|
166
200
|
busy?: TimeMachineState["busy"];
|
|
167
201
|
lastOutcome?: RestoreOutcome | null;
|
|
168
202
|
providers?: RemoteProviderStatus[];
|
|
203
|
+
route?: RouteToolStatus;
|
|
169
204
|
}): void {
|
|
170
205
|
this.setState({
|
|
171
206
|
snapshots: payload.snapshots ?? [],
|
|
172
207
|
busy: payload.busy ?? null,
|
|
173
208
|
lastOutcome: payload.lastOutcome ?? null,
|
|
174
209
|
remoteProviders: payload.providers ?? [],
|
|
210
|
+
remoteRoute: payload.route ?? { available: false },
|
|
175
211
|
hasLoaded: true,
|
|
176
212
|
});
|
|
177
213
|
}
|
|
@@ -204,12 +240,15 @@ class TimeMachineStore {
|
|
|
204
240
|
};
|
|
205
241
|
}
|
|
206
242
|
}
|
|
243
|
+
// Where the app was when this state was true — restoring can return here.
|
|
244
|
+
const route = captureCurrentRoute();
|
|
207
245
|
const snapshot: StoredSnapshot = {
|
|
208
246
|
meta: {
|
|
209
247
|
id: nextId(),
|
|
210
248
|
name: name.trim() || "Untitled snapshot",
|
|
211
249
|
createdAt: Date.now(),
|
|
212
250
|
sizeBytes: 0,
|
|
251
|
+
...(route ? { route } : {}),
|
|
213
252
|
sources,
|
|
214
253
|
},
|
|
215
254
|
data,
|
|
@@ -259,13 +298,15 @@ class TimeMachineStore {
|
|
|
259
298
|
async restore(
|
|
260
299
|
id: string,
|
|
261
300
|
mode: RestoreMode,
|
|
262
|
-
excludeKeys?: readonly string[]
|
|
301
|
+
excludeKeys?: readonly string[],
|
|
302
|
+
options?: RestoreOptions
|
|
263
303
|
): Promise<RestoreOutcome> {
|
|
264
304
|
if (remoteHandler) {
|
|
265
305
|
const outcome = (await remoteHandler("restore", {
|
|
266
306
|
id,
|
|
267
307
|
mode,
|
|
268
308
|
excludeKeys,
|
|
309
|
+
restoreRoute: options?.restoreRoute,
|
|
269
310
|
})) as RestoreOutcome;
|
|
270
311
|
this.setState({ lastOutcome: outcome });
|
|
271
312
|
return outcome;
|
|
@@ -330,6 +371,8 @@ class TimeMachineStore {
|
|
|
330
371
|
? splitCompound(stored.meta.scope)
|
|
331
372
|
: null;
|
|
332
373
|
|
|
374
|
+
const wantsRoute = options?.restoreRoute === true && !!stored.meta.route;
|
|
375
|
+
|
|
333
376
|
if (mode === "reload") {
|
|
334
377
|
// Storage only; everything in-memory rebuilds after the reload.
|
|
335
378
|
if (
|
|
@@ -343,6 +386,17 @@ class TimeMachineStore {
|
|
|
343
386
|
includeBySource?.get("storage")
|
|
344
387
|
);
|
|
345
388
|
}
|
|
389
|
+
// Hand the route to the NEXT boot — this realm is about to die, so the
|
|
390
|
+
// slot must be written before the reload is armed.
|
|
391
|
+
if (wantsRoute) {
|
|
392
|
+
const route = stored.meta.route!;
|
|
393
|
+
try {
|
|
394
|
+
await savePendingRoute(route, stored.meta.name);
|
|
395
|
+
outcome.route = { ...route, status: "deferred" };
|
|
396
|
+
} catch (e) {
|
|
397
|
+
outcome.route = { ...route, status: "failed", error: String(e) };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
346
400
|
outcome.willReload = this.scheduleReload("buoy time-machine restore");
|
|
347
401
|
this.setState({ lastOutcome: outcome });
|
|
348
402
|
return outcome;
|
|
@@ -358,6 +412,10 @@ class TimeMachineStore {
|
|
|
358
412
|
includeBySource?.get(sid)
|
|
359
413
|
);
|
|
360
414
|
}
|
|
415
|
+
// Navigate LAST — the screen we land on should read the restored state.
|
|
416
|
+
if (wantsRoute) {
|
|
417
|
+
outcome.route = navigateToRoute(stored.meta.route!);
|
|
418
|
+
}
|
|
361
419
|
this.setState({ lastOutcome: outcome });
|
|
362
420
|
return outcome;
|
|
363
421
|
} finally {
|
|
@@ -686,6 +744,7 @@ export function replaceTimeMachineFromSnapshot(payload: {
|
|
|
686
744
|
busy?: TimeMachineState["busy"];
|
|
687
745
|
lastOutcome?: RestoreOutcome | null;
|
|
688
746
|
providers?: RemoteProviderStatus[];
|
|
747
|
+
route?: RouteToolStatus;
|
|
689
748
|
}): void {
|
|
690
749
|
timeMachineStore.replaceFromSnapshot(payload);
|
|
691
750
|
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type RestoreMode,
|
|
24
24
|
} from "../store/timeMachineStore";
|
|
25
25
|
import { loadSnapshot } from "../store/snapshotVault";
|
|
26
|
+
import { canNavigate, currentRoute } from "../store/routeRestore";
|
|
26
27
|
|
|
27
28
|
interface CaptureParams {
|
|
28
29
|
name?: string;
|
|
@@ -33,6 +34,8 @@ interface RestoreParams {
|
|
|
33
34
|
mode?: RestoreMode;
|
|
34
35
|
/** Compound "sourceId::itemKey" strings — items the caller opted out of. */
|
|
35
36
|
excludeKeys?: string[];
|
|
37
|
+
/** Also navigate back to the route the snapshot was captured on. */
|
|
38
|
+
restoreRoute?: boolean;
|
|
36
39
|
}
|
|
37
40
|
interface IdParams {
|
|
38
41
|
id: string;
|
|
@@ -61,11 +64,24 @@ function providerStatuses() {
|
|
|
61
64
|
|
|
62
65
|
function metadata() {
|
|
63
66
|
const { snapshots, busy, lastOutcome } = timeMachineStore.getState();
|
|
64
|
-
|
|
67
|
+
const current = currentRoute();
|
|
68
|
+
return {
|
|
69
|
+
snapshots,
|
|
70
|
+
busy,
|
|
71
|
+
lastOutcome,
|
|
72
|
+
providers: providerStatuses(),
|
|
73
|
+
// Route capture/restore is a device capability, not a snapshot provider —
|
|
74
|
+
// a dashboard has no router of its own to probe, so it's mirrored here.
|
|
75
|
+
route: {
|
|
76
|
+
available: canNavigate(),
|
|
77
|
+
...(current ? { current: current.href } : {}),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
export const timeMachineSyncAdapter = {
|
|
68
|
-
|
|
83
|
+
// v2 adds `route` (device route capability) and `snapshots[].route`.
|
|
84
|
+
version: 2,
|
|
69
85
|
getSnapshot: () => metadata(),
|
|
70
86
|
subscribe: (onChange: () => void) => {
|
|
71
87
|
const unsubStore = timeMachineStore.subscribe(onChange);
|
|
@@ -96,9 +112,16 @@ export const timeMachineSyncAdapter = {
|
|
|
96
112
|
* persisted storage then reloads the JS bundle (always the path for
|
|
97
113
|
* baseline snapshots). */
|
|
98
114
|
restore: async (params: unknown) => {
|
|
99
|
-
const {
|
|
115
|
+
const {
|
|
116
|
+
id,
|
|
117
|
+
mode = "live",
|
|
118
|
+
excludeKeys,
|
|
119
|
+
restoreRoute,
|
|
120
|
+
} = (params as RestoreParams) ?? {};
|
|
100
121
|
if (!id) throw new Error("restore requires a snapshot `id`");
|
|
101
|
-
return await timeMachineStore.restore(id, mode, excludeKeys
|
|
122
|
+
return await timeMachineStore.restore(id, mode, excludeKeys, {
|
|
123
|
+
restoreRoute,
|
|
124
|
+
});
|
|
102
125
|
},
|
|
103
126
|
|
|
104
127
|
delete: async (params: unknown) => {
|