Skip to content

Commit 9b1c45f

Browse files
authored
🔀 Merge pull request #753 from FrostCo/feat/data-migration-improvements
Feat/data migration improvements
2 parents fd7b345 + 3342194 commit 9b1c45f

1 file changed

Lines changed: 144 additions & 12 deletions

File tree

src/script/DataMigration.ts

Lines changed: 144 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Constants from '@APF/lib/Constants';
2-
import { booleanToNumber, getVersion, isVersionOlder } from '@APF/lib/helper';
2+
import { booleanToNumber, deepCloneJson, getVersion, isVersionOlder } from '@APF/lib/helper';
33
import WebConfig from '@APF/WebConfig';
44
import type { WordOptions } from '@APF/lib/Word';
55

@@ -13,6 +13,17 @@ export interface Migration {
1313
export default class DataMigration {
1414
cfg: WebConfig;
1515

16+
/**
17+
* Which high-level runner is executing migration methods: `'import'` for {@link DataMigration.runImportMigrations}
18+
* or `'versionUpgrade'` for {@link DataMigration.byVersion}. Otherwise `null`.
19+
*/
20+
private _migrationRunner: 'import' | 'versionUpgrade' | null = null;
21+
22+
/** `true` only while `runImportMigrations` is running (parsed JSON / in-memory config), not during extension upgrade. */
23+
get isConfigImportMigration(): boolean {
24+
return this._migrationRunner === 'import';
25+
}
26+
1627
//#region Class reference helpers
1728
// Can be overridden in children classes
1829
static get Config() {
@@ -73,6 +84,116 @@ export default class DataMigration {
7384
this.cfg = config;
7485
}
7586

87+
/**
88+
* Merges host maps for {@link DataMigration.loadFormerLargeKeyStorage}: later `part` wins per host and per
89+
* field within a host.
90+
*/
91+
static mergeFormerLargeKeyParts(
92+
acc: Record<string, unknown> | null,
93+
part: Record<string, unknown> | null,
94+
): Record<string, unknown> | null {
95+
if (part == null || !Object.keys(part).length) return acc;
96+
if (acc == null || !Object.keys(acc).length) return deepCloneJson(part) as Record<string, unknown>;
97+
const out = deepCloneJson(acc) as Record<string, unknown>;
98+
for (const h of Object.keys(part)) {
99+
const O = part[h];
100+
const A = out[h];
101+
if (A && typeof A === 'object' && !Array.isArray(A) && O && typeof O === 'object' && !Array.isArray(O)) {
102+
out[h] = { ...(A as object), ...(O as object) };
103+
} else {
104+
out[h] = deepCloneJson(O);
105+
}
106+
}
107+
return out;
108+
}
109+
110+
/**
111+
* Loads a retired large-key blob when it is no longer in {@link WebConfig._largeKeys}. Merges **non-split**
112+
* (`formerLogicalKey` as a single object in sync or local) and **split** (`_${key}N` containers) reads;
113+
* later sources overlay earlier (bare local → bare sync → local splits → sync splits). Returns `null` if
114+
* nothing was stored. Pair with {@link DataMigration.removeFormerLargeKeyStorage}.
115+
*/
116+
static async loadFormerLargeKeyStorage(
117+
cfg: WebConfig,
118+
formerLogicalKey: string,
119+
): Promise<Record<string, unknown> | null> {
120+
const C = this.Config;
121+
if (!C.chromeStorageAvailable() || !formerLogicalKey) return null;
122+
123+
const parseSplitContainers = (raw: Record<string, unknown>): Record<string, unknown> | null => {
124+
const temp = { ...raw };
125+
const combinedKeys = C.combineData(temp, formerLogicalKey);
126+
if (!combinedKeys?.length) return null;
127+
const obj = temp[formerLogicalKey];
128+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return null;
129+
if (!Object.keys(obj).length) return null;
130+
return deepCloneJson(obj) as Record<string, unknown>;
131+
};
132+
133+
const readBareLocal = async (): Promise<Record<string, unknown> | null> => {
134+
const localData = (await C.getLocalStorage([formerLogicalKey])) as Record<string, unknown>;
135+
const obj = localData[formerLogicalKey];
136+
if (obj == null || typeof obj !== 'object' || Array.isArray(obj)) return null;
137+
if (!Object.keys(obj).length) return null;
138+
return deepCloneJson(obj) as Record<string, unknown>;
139+
};
140+
141+
const readBareSync = async (): Promise<Record<string, unknown> | null> => {
142+
const syncData = (await C.getSyncStorage([formerLogicalKey])) as Record<string, unknown>;
143+
const obj = syncData[formerLogicalKey];
144+
if (obj == null || typeof obj !== 'object' || Array.isArray(obj)) return null;
145+
if (!Object.keys(obj).length) return null;
146+
return deepCloneJson(obj) as Record<string, unknown>;
147+
};
148+
149+
const readSyncSplits = async (): Promise<Record<string, unknown> | null> => {
150+
const raw = (await C.getSyncStorage(C.splitKeyNames(formerLogicalKey))) as Record<string, unknown>;
151+
return parseSplitContainers(raw);
152+
};
153+
154+
const readLocalSplits = async (): Promise<Record<string, unknown> | null> => {
155+
const raw = (await C.getLocalStorage(C.splitKeyNames(formerLogicalKey))) as Record<string, unknown>;
156+
return parseSplitContainers(raw);
157+
};
158+
159+
const [bareLocal, bareSync, localSplits, syncSplits] = await Promise.all([
160+
readBareLocal(),
161+
readBareSync(),
162+
readLocalSplits(),
163+
readSyncSplits(),
164+
]);
165+
166+
let merged: Record<string, unknown> | null = null;
167+
merged = this.mergeFormerLargeKeyParts(merged, bareLocal);
168+
merged = this.mergeFormerLargeKeyParts(merged, bareSync);
169+
merged = this.mergeFormerLargeKeyParts(merged, localSplits);
170+
merged = this.mergeFormerLargeKeyParts(merged, syncSplits);
171+
172+
return merged;
173+
}
174+
175+
/** @see DataMigration.loadFormerLargeKeyStorage */
176+
loadFormerLargeKeyStorage(formerLogicalKey: string): Promise<Record<string, unknown> | null> {
177+
return (this.constructor as typeof DataMigration).loadFormerLargeKeyStorage(this.cfg, formerLogicalKey);
178+
}
179+
180+
/**
181+
* Removes the bare logical key (non-split) and all split containers (`_${key}N`) from sync **and** local
182+
* storage. Use when the key was removed from {@link WebConfig._largeKeys} so {@link WebConfig.remove} would
183+
* not expand split names.
184+
*/
185+
static async removeFormerLargeKeyStorage(formerLogicalKey: string): Promise<void> {
186+
const C = this.Config;
187+
if (!C.chromeStorageAvailable() || !formerLogicalKey) return;
188+
const keysToRemove = [formerLogicalKey, ...C.splitKeyNames(formerLogicalKey)];
189+
await Promise.all([C.removeSyncStorage(keysToRemove), C.removeLocalStorage(keysToRemove)]);
190+
}
191+
192+
/** @see DataMigration.removeFormerLargeKeyStorage */
193+
removeFormerLargeKeyStorage(formerLogicalKey: string): Promise<void> {
194+
return (this.constructor as typeof DataMigration).removeFormerLargeKeyStorage(formerLogicalKey);
195+
}
196+
76197
// TODO: Only tested with arrays
77198
_renameConfigKeys(oldCfg: WebConfig, oldKeys: string[], mapping: { [key: string]: string }) {
78199
for (const oldKey of oldKeys) {
@@ -101,12 +222,18 @@ export default class DataMigration {
101222
async byVersion(oldVersion: string) {
102223
const version = getVersion(oldVersion);
103224
let migrated = false;
104-
for (const migration of (this.constructor as typeof DataMigration).migrations) {
105-
if (isVersionOlder(version, getVersion(migration.version))) {
106-
migrated = true;
107-
if (migration.async) await this[migration.name]();
108-
else this[migration.name]();
225+
const prevRunner = this._migrationRunner;
226+
this._migrationRunner = 'versionUpgrade';
227+
try {
228+
for (const migration of (this.constructor as typeof DataMigration).migrations) {
229+
if (isVersionOlder(version, getVersion(migration.version))) {
230+
migrated = true;
231+
if (migration.async) await this[migration.name]();
232+
else this[migration.name]();
233+
}
109234
}
235+
} finally {
236+
this._migrationRunner = prevRunner;
110237
}
111238

112239
return migrated;
@@ -200,13 +327,18 @@ export default class DataMigration {
200327

201328
async runImportMigrations() {
202329
let migrated = false;
203-
204-
for (const migration of (this.constructor as typeof DataMigration).migrations) {
205-
if (migration.runOnImport) {
206-
migrated = true;
207-
if (migration.async) await this[migration.name]();
208-
else this[migration.name]();
330+
const prevRunner = this._migrationRunner;
331+
this._migrationRunner = 'import';
332+
try {
333+
for (const migration of (this.constructor as typeof DataMigration).migrations) {
334+
if (migration.runOnImport) {
335+
migrated = true;
336+
if (migration.async) await this[migration.name]();
337+
else this[migration.name]();
338+
}
209339
}
340+
} finally {
341+
this._migrationRunner = prevRunner;
210342
}
211343

212344
return migrated;

0 commit comments

Comments
 (0)