11import Constants from '@APF/lib/Constants' ;
2- import { booleanToNumber , getVersion , isVersionOlder } from '@APF/lib/helper' ;
2+ import { booleanToNumber , deepCloneJson , getVersion , isVersionOlder } from '@APF/lib/helper' ;
33import WebConfig from '@APF/WebConfig' ;
44import type { WordOptions } from '@APF/lib/Word' ;
55
@@ -13,6 +13,17 @@ export interface Migration {
1313export 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