Repository navigation
Expand file tree
/
Copy pathmigration.js
More file actions
96 lines (81 loc) · 3.61 KB
/
Copy pathmigration.js
File metadata and controls
96 lines (81 loc) · 3.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
(function () {
const helpers = window.dbWalletHelpers || null;
if (!helpers) return;
const {
randomWalletId,
parseCompactEventId,
hash53,
extractLegacyDeviceKey,
} = helpers;
function walletNeedsMigration(wallet) {
if (!wallet || typeof wallet !== "object") return false;
const v = typeof wallet.v === "number" ? wallet.v : 1;
if (v < 2) return true;
const events = Array.isArray(wallet.events) ? wallet.events : [];
return events.some((e) => {
const id = e && typeof e.id === "string" ? e.id : "";
return !!id && !parseCompactEventId(id);
});
}
function migrateWalletV1toV2(wallet) {
if (!wallet || typeof wallet !== "object") return wallet;
if (!Array.isArray(wallet.events)) wallet.events = [];
if (!wallet.walletId) wallet.walletId = randomWalletId();
if (!wallet.seq || typeof wallet.seq !== "object") wallet.seq = {};
const usedIds = new Set();
for (const e of wallet.events) {
if (e && typeof e.id === "string" && e.id) usedIds.add(e.id);
}
// Map every pre-migration id (and its legacy alias) to its new compact id so
// tombstone refs can be re-pointed in a second pass below.
const idMap = new Map();
for (const e of wallet.events) {
if (!e || typeof e !== "object") continue;
const id = typeof e.id === "string" ? e.id : "";
const oid = typeof e.oid === "string" ? e.oid : "";
// An already-compact id is in v2 form regardless of oid — never re-migrate.
// Re-migrating would re-roll the id through the collision loop (the false
// self-collision against usedIds), breaking idempotency and cross-device sync.
if (id && parseCompactEventId(id)) continue;
// Derive a stable fallback from the event's content (not Date.now()) so two
// devices migrating the same id-less event converge on the same id instead
// of minting divergent, non-deduplicable ids.
const contentKey = `legacy-${hash53(
`${e.t || ""}:${e.n != null ? e.n : ""}:${e.ts != null ? e.ts : ""}`,
).toString(36)}`;
const legacyId = oid || id || contentKey;
const legacyDevice = extractLegacyDeviceKey(legacyId);
let seq = hash53(legacyId);
let newId = `${legacyDevice}.${seq.toString(36)}`;
while (usedIds.has(newId)) {
seq = (seq + 1) % Number.MAX_SAFE_INTEGER;
if (seq <= 0) seq = 1;
newId = `${legacyDevice}.${seq.toString(36)}`;
}
usedIds.add(newId);
// The event's current id is authoritative; map it unconditionally. The
// legacy alias (oid) is secondary — only add it if that key is still free,
// so one event's oid can't clobber another event's id->newId mapping and
// mis-resolve a tombstone ref in the second pass below.
if (id) idMap.set(id, newId);
if (legacyId && !idMap.has(legacyId)) idMap.set(legacyId, newId);
if (!e.oid) e.oid = legacyId;
e.id = newId;
}
// Second pass: re-point tombstone refs from the deleted event's old id to its
// new compact id. Without this, applyTombstones can no longer match the
// tombstone to its target and previously-deleted events resurface.
for (const e of wallet.events) {
if (!e || typeof e !== "object" || e.t !== "x") continue;
const ref = typeof e.ref === "string" ? e.ref : "";
if (ref && idMap.has(ref)) {
e.ref = idMap.get(ref);
}
}
const currentV = typeof wallet.v === "number" ? wallet.v : 1;
wallet.v = Math.max(currentV, 2);
return wallet;
}
window.dbWalletMigrateV1toV2 = migrateWalletV1toV2;
window.dbWalletNeedsMigration = walletNeedsMigration;
})();