forked from
tranquil.farm/tranquil-pds
Our Personal Data Server from scratch!
1import type {
2 MigrationDirection,
3 MigrationState,
4 StoredMigrationState,
5} from "./types.ts";
6import { clearDPoPKey } from "./atproto-client.ts";
7
8const STORAGE_KEY = "tranquil_migration_state";
9const MAX_AGE_MS = 24 * 60 * 60 * 1000;
10
11export function saveMigrationState(state: MigrationState): void {
12 const storedState: StoredMigrationState = {
13 version: 1,
14 direction: state.direction,
15 step: state.step,
16 startedAt: new Date().toISOString(),
17 sourcePdsUrl: state.sourcePdsUrl,
18 targetPdsUrl: globalThis.location.origin,
19 sourceDid: state.sourceDid,
20 sourceHandle: state.sourceHandle,
21 targetHandle: state.targetHandle,
22 targetEmail: state.targetEmail,
23 authMethod: state.authMethod,
24 passkeySetupToken: state.passkeySetupToken ?? undefined,
25 localAccessToken: state.localAccessToken ?? undefined,
26 progress: {
27 repoExported: state.progress.repoExported,
28 repoImported: state.progress.repoImported,
29 blobsTotal: state.progress.blobsTotal,
30 blobsMigrated: state.progress.blobsMigrated,
31 prefsMigrated: state.progress.prefsMigrated,
32 plcSigned: state.progress.plcSigned,
33 },
34 lastError: state.error ?? undefined,
35 lastErrorStep: state.error ? state.step : undefined,
36 };
37
38 try {
39 localStorage.setItem(STORAGE_KEY, JSON.stringify(storedState));
40 } catch { /* localStorage unavailable */ }
41}
42
43export function loadMigrationState(): StoredMigrationState | null {
44 try {
45 const stored = localStorage.getItem(STORAGE_KEY);
46 if (!stored) return null;
47
48 const state = JSON.parse(stored) as StoredMigrationState;
49
50 if (state.version !== 1) {
51 clearMigrationState();
52 return null;
53 }
54
55 const startedAt = new Date(state.startedAt).getTime();
56 if (Date.now() - startedAt > MAX_AGE_MS) {
57 clearMigrationState();
58 return null;
59 }
60
61 return state;
62 } catch {
63 clearMigrationState();
64 return null;
65 }
66}
67
68export function clearMigrationState(): void {
69 try {
70 localStorage.removeItem(STORAGE_KEY);
71 clearDPoPKey();
72 } catch { /* localStorage unavailable */ }
73}
74
75export function hasPendingMigration(): boolean {
76 return loadMigrationState() !== null;
77}
78
79export function getResumeInfo(): {
80 direction: MigrationDirection;
81 sourceHandle: string;
82 targetHandle: string;
83 sourcePdsUrl: string;
84 targetPdsUrl: string;
85 targetEmail: string;
86 authMethod?: "password" | "passkey";
87 progressSummary: string;
88 step: string;
89} | null {
90 const state = loadMigrationState();
91 if (!state) return null;
92
93 const progressParts: string[] = [];
94 if (state.progress.repoExported) progressParts.push("repo exported");
95 if (state.progress.repoImported) progressParts.push("repo imported");
96 if (state.progress.blobsMigrated > 0) {
97 progressParts.push(
98 `${state.progress.blobsMigrated}/${state.progress.blobsTotal} blobs`,
99 );
100 }
101 if (state.progress.prefsMigrated) progressParts.push("preferences migrated");
102 if (state.progress.plcSigned) progressParts.push("PLC signed");
103
104 return {
105 direction: state.direction,
106 sourceHandle: state.sourceHandle,
107 targetHandle: state.targetHandle,
108 sourcePdsUrl: state.sourcePdsUrl,
109 targetPdsUrl: state.targetPdsUrl,
110 targetEmail: state.targetEmail,
111 authMethod: state.authMethod,
112 progressSummary: progressParts.length > 0
113 ? progressParts.join(", ")
114 : "just started",
115 step: state.step,
116 };
117}
118
119export function updateProgress(
120 updates: Partial<StoredMigrationState["progress"]>,
121): void {
122 const state = loadMigrationState();
123 if (!state) return;
124
125 state.progress = { ...state.progress, ...updates };
126 try {
127 localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
128 } catch { /* localStorage unavailable */ }
129}
130
131export function updateStep(step: string): void {
132 const state = loadMigrationState();
133 if (!state) return;
134
135 state.step = step;
136 try {
137 localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
138 } catch { /* localStorage unavailable */ }
139}
140
141export function setError(error: string, step: string): void {
142 const state = loadMigrationState();
143 if (!state) return;
144
145 state.lastError = error;
146 state.lastErrorStep = step;
147 try {
148 localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
149 } catch { /* localStorage unavailable */ }
150}