1- // Copy from chrome.storage, but not exported
2- type NoInferX < T > = T [ ] [ T extends any ? 0 : never ]
3-
41const ALL_TABLES = [ 'stat' ] as const
52
6- export type IndexedTable = typeof ALL_TABLES [ number ]
7-
8- const DB_NAME = `tt4b_${ chrome . runtime . id } `
3+ export type Table = typeof ALL_TABLES [ number ]
94
10- const initDb = ( ) : Promise < IDBDatabase > => {
11- const factory = typeof window === 'undefined' ? self . indexedDB : window . indexedDB
12- const request = factory . open ( DB_NAME )
13- return new Promise ( ( resolve , reject ) => {
14- request . onsuccess = ( ) => {
15- const db = request . result
16- const existingStores = Array . from ( db . objectStoreNames )
17- const missingStores = ALL_TABLES . filter ( table => ! existingStores . includes ( table ) )
18- if ( ! missingStores . length ) {
19- return resolve ( db )
20- }
5+ export type Key < T = Record < string , number > > = keyof T & string
216
22- const currentVersion = db . version
23- db . close ( )
7+ type IndexConfig < T = Record < string , unknown > > = {
8+ key : Key < T > | Key < T > [ ]
9+ unique ?: boolean
10+ }
2411
25- const upgradeRequest = factory . open ( DB_NAME , currentVersion + 1 )
12+ export type Index < T = Record < string , unknown > > = Key < T > | Key < T > [ ] | IndexConfig < T >
2613
27- upgradeRequest . onupgradeneeded = ( ) => {
28- const upgradeDb = upgradeRequest . result
29- for ( const table of missingStores ) {
30- if ( ! upgradeDb . objectStoreNames . contains ( table ) ) {
31- upgradeDb . createObjectStore ( table )
32- }
33- }
34- }
35-
36- upgradeRequest . onsuccess = ( ) => {
37- console . log ( "IndexedDB upgraded" )
38- resolve ( upgradeRequest . result )
39- }
14+ const DB_NAME = `tt4b_${ chrome . runtime . id } `
4015
41- upgradeRequest . onerror = ( ) => reject ( upgradeRequest . error )
42- }
16+ function normalizeIndex < T = Record < string , number > > ( index : Index < T > ) : IndexConfig < T > {
17+ return typeof index === 'string' || Array . isArray ( index ) ? { key : index } : index
18+ }
4319
44- request . onerror = ( ev ) => {
45- console . error ( "IndexedDB initialized error: " , ev )
46- reject ( ev )
47- }
48- } )
20+ function formatIdxName < T = Record < string , number > > ( key : IndexConfig < T > [ 'key' ] ) : string {
21+ const keyStr = Array . isArray ( key ) ? key . sort ( ) . join ( '_' ) : key
22+ return `idx_${ keyStr } `
4923}
5024
51- function promiseRequest < T > ( req : IDBRequest < T > ) : Promise < T > {
25+ export function req2Promise < T = unknown > ( req : IDBRequest < T > ) : Promise < T | undefined > {
5226 return new Promise ( ( resolve , reject ) => {
5327 req . onsuccess = ( ) => resolve ( req . result )
5428 req . onerror = ( ev ) => {
@@ -58,153 +32,145 @@ function promiseRequest<T>(req: IDBRequest<T>): Promise<T> {
5832 } )
5933}
6034
61- function cvtDbKey2Str ( dbKey : IDBValidKey ) : string {
62- if ( typeof dbKey === 'string' ) {
63- return dbKey
64- } else if ( typeof dbKey === 'number' ) {
65- return `${ dbKey } `
66- } else if ( dbKey instanceof Date ) {
67- return `${ dbKey . getTime ( ) } `
68- } else {
69- return dbKey . toString ( )
70- }
35+ export async function iterateCursor < T = unknown > (
36+ req : IDBRequest < IDBCursorWithValue | null > ,
37+ processor ?: ( cursor : IDBCursorWithValue ) => void | Promise < void >
38+ ) : Promise < T [ ] > {
39+ const results : T [ ] = [ ]
40+
41+ return new Promise ( ( resolve , reject ) => {
42+ req . onsuccess = async ( ) => {
43+ const cursor = req . result
44+ if ( ! cursor ) return resolve ( results )
45+
46+ try {
47+ results . push ( cursor . value as T )
48+ await processor ?.( cursor )
49+ cursor . continue ( )
50+ } catch ( error ) {
51+ reject ( error )
52+ }
53+ }
54+
55+ req . onerror = ( ) => reject ( req . error )
56+ } )
7157}
7258
73- export class IndexedDbStorage implements chrome . storage . StorageArea {
59+ export abstract class BaseIDBStorage < T = Record < string , unknown > > {
7460 private db : IDBDatabase | undefined
61+ abstract indexes : Index < T > [ ]
62+ abstract key : Key < T > | Key < T > [ ]
63+ abstract table : Table
7564
76- onChanged : chrome . events . Event < ( changes : { [ key : string ] : chrome . storage . StorageChange } ) => void > = {
77- addListener : function ( callback : ( changes : { [ key : string ] : chrome . storage . StorageChange } ) => void ) : void {
78- throw new Error ( 'Function not implemented.' )
79- } ,
80- getRules : function ( ruleIdentifiers : string [ ] | ArgCallback < chrome . events . Rule [ ] > , callback ?: ArgCallback < chrome . events . Rule [ ] > ) : void {
81- // not unsupported
82- } ,
83- hasListener : function ( callback : ( changes : { [ key : string ] : chrome . storage . StorageChange } ) => void ) : boolean {
84- return false
85- } ,
86- removeRules : function ( ruleIdentifiers : string [ ] | undefined | ArgCallback < chrome . events . Rule [ ] > , callback ?: ArgCallback < chrome . events . Rule [ ] > ) : void {
87- // not unsupported
88- } ,
89- addRules : function ( rules : chrome . events . Rule [ ] , callback ?: ( rules : chrome . events . Rule [ ] ) => void ) : void {
90- // not unsupported
91- } ,
92- removeListener : function ( callback : ( changes : { [ key : string ] : chrome . storage . StorageChange } ) => void ) : void {
93- // not unsupported
94- } ,
95- hasListeners : function ( ) : boolean {
96- return false
97- }
98- }
65+ protected async initDb ( ) : Promise < IDBDatabase > {
66+ if ( this . db ) return this . db
9967
100- constructor ( private table : IndexedTable ) { }
68+ const factory = typeof window === 'undefined' ? self . indexedDB : window . indexedDB
69+ const checkRequest = factory . open ( DB_NAME )
10170
102- private async withTransaction < T > ( operation : ( store : IDBObjectStore ) => Promise < T > , mode ?: IDBTransactionMode ) : Promise < T > {
103- if ( ! this . db ) {
104- ( self as any ) . _db = this . db = await initDb ( )
105- }
106- const transaction = this . db . transaction ( [ this . table ] , mode ?? 'readwrite' )
107- const store = transaction . objectStore ( this . table )
108- try {
109- return await operation ( store )
110- } finally {
111- transaction . commit ( )
112- }
113- }
71+ return new Promise ( ( resolve , reject ) => {
72+ checkRequest . onsuccess = ( ) => {
73+ const db = checkRequest . result
74+ const storeExisted = db . objectStoreNames . contains ( this . table )
75+ const needUpgrade = ! storeExisted || this . needUpgradeIndexes ( db )
11476
115- async clear ( callback ?: NoArgCallback ) : Promise < void > {
116- await this . withTransaction ( async store => {
117- const req = store . clear ( )
118- await promiseRequest ( req )
119- } )
120- callback ?.( )
121- }
77+ if ( ! needUpgrade ) {
78+ this . db = db
79+ return resolve ( db )
80+ }
12281
123- async set < T = { [ key : string ] : any } > ( items : Partial < T > , callback ?: NoArgCallback ) : Promise < void > {
124- await this . withTransaction ( async store => {
125- const promises : Promise < unknown > [ ] = [ ]
82+ const currentVersion = db . version
83+ db . close ( )
12684
127- for ( const [ key , value ] of Object . entries ( items ) ) {
128- const promise = promiseRequest ( store . put ( value , key ) )
129- promises . push ( promise )
130- }
131- await Promise . all ( promises )
132- } )
85+ const upgradeRequest = factory . open ( DB_NAME , currentVersion + 1 )
13386
134- callback ?.( )
135- }
87+ upgradeRequest . onupgradeneeded = ( ) => {
88+ const upgradeDb = upgradeRequest . result
89+ const transaction = upgradeRequest . transaction
90+ if ( ! transaction ) return reject ( "Failed to get transaction of upgrading request" )
13691
137- async remove < T = { [ key : string ] : any } > ( keys : keyof T | Array < keyof T > , callback ?: ( ) => void ) : Promise < void > {
138- await this . withTransaction ( async store => {
139- keys = Array . isArray ( keys ) ? keys : [ keys ]
140- await Promise . all ( keys . map ( key => typeof key !== 'symbol' && promiseRequest ( store . delete ( key ) ) ) )
141- } )
142- callback ?.( )
143- }
144-
145- async get < T = { [ key : string ] : unknown } > (
146- keys : NoInferX < keyof T > | Array < NoInferX < keyof T > > | Partial < NoInferX < T > > | null | undefined | ArgCallback < T > ,
147- callback ?: ArgCallback < T > ,
148- ) : Promise < T > {
149- if ( typeof keys === 'function' ) {
150- callback = keys
151- keys = null
152- }
153- const data = await this . getData ( keys )
154- callback ?.( data )
155- return data
156- }
92+ let store = upgradeDb . objectStoreNames . contains ( this . table )
93+ ? transaction . objectStore ( this . table )
94+ : upgradeDb . createObjectStore ( this . table , { keyPath : this . key } )
95+ this . createIndexes ( store )
96+ }
15797
158- private async getData < T = { [ key : string ] : unknown } > ( keys ?: NoInferX < keyof T > | Array < NoInferX < keyof T > > | Partial < NoInferX < T > > | null ) : Promise < T > {
159- const result = { } as { [ key : string ] : unknown }
98+ upgradeRequest . onsuccess = ( ) => {
99+ console . log ( "IndexedDB upgraded" )
100+ this . db = upgradeRequest . result
101+ resolve ( upgradeRequest . result )
102+ }
160103
161- await this . withTransaction ( async store => {
162- const fill = async ( key : keyof NoInferX < T > ) => {
163- if ( typeof key === 'symbol' ) return
164- const value = await promiseRequest < unknown > ( store . get ( key ) )
165- result [ cvtDbKey2Str ( key ) ] = value
104+ upgradeRequest . onerror = ( ) => reject ( upgradeRequest . error )
166105 }
167106
168- if ( keys == null ) {
169- const allKeys = await promiseRequest ( store . getAllKeys ( ) )
170- const allValues = await promiseRequest ( store . getAll ( ) )
171- allKeys . forEach ( ( key , index ) => result [ cvtDbKey2Str ( key ) ] = allValues [ index ] )
172- } else if ( Array . isArray ( keys ) ) {
173- await Promise . all ( keys . map ( fill ) )
174- } else if ( typeof keys === 'object' ) {
175- for ( const key in keys ) {
176- await fill ( key )
107+ checkRequest . onerror = ( ) => reject ( checkRequest . error )
108+ } )
109+ }
110+
111+ private needUpgradeIndexes ( db : IDBDatabase ) : boolean {
112+ try {
113+ const transaction = db . transaction ( this . table , 'readonly' )
114+ const store = transaction . objectStore ( this . table )
115+ const indexNames = store . indexNames
116+
117+ for ( const index of this . indexes ) {
118+ const { key } = normalizeIndex ( index )
119+ const idxName = formatIdxName ( key )
120+ if ( ! indexNames . contains ( idxName ) ) {
121+ return true
177122 }
178- } else {
179- await fill ( keys )
180123 }
181- } , 'readonly' )
182-
183- return result as T
124+ return false
125+ } catch ( e ) {
126+ console . error ( "Failed to check indexes" , e )
127+ return true
128+ }
184129 }
185130
186- async setAccessLevel ( accessOptions : { accessLevel : `${chrome . storage . AccessLevel } ` } , callback ?: NoArgCallback ) : Promise < void > {
187- // no nothing
188- console . info ( "Invoked setAccessLevel()" , accessOptions )
189- callback ?.( )
131+ private createIndexes ( store : IDBObjectStore ) {
132+ const existingIndexes = store . indexNames
133+
134+ for ( const index of this . indexes ) {
135+ const { key, unique } = normalizeIndex ( index )
136+ const idxName = formatIdxName ( key )
137+ if ( ! existingIndexes . contains ( idxName ) ) {
138+ store . createIndex ( idxName , key , { unique } )
139+ }
140+ }
190141 }
191142
192- async getKeys ( callback ?: ArgCallback < string [ ] > ) : Promise < string [ ] > {
193- const keys = await this . withTransaction ( async store => {
194- const dbKeys = await promiseRequest ( store . getAllKeys ( ) )
195- return dbKeys . map ( cvtDbKey2Str )
196- } , 'readonly' )
197- callback ?.( keys )
198- return keys
143+ protected async withStore < T = unknown > ( operation : ( store : IDBObjectStore ) => T | Promise < T > , mode ?: IDBTransactionMode ) : Promise < T > {
144+ const db = await this . initDb ( )
145+ const trans = db . transaction ( this . table , mode ?? 'readwrite' )
146+ try {
147+ const store = trans . objectStore ( this . table )
148+ const result = await operation ( store )
149+ // Waiting for transaction completed
150+ await new Promise < void > ( ( resolve , reject ) => {
151+ trans . oncomplete = ( ) => resolve ( )
152+ trans . onerror = ( ) => reject ( trans . error )
153+ trans . onabort = ( ) => reject ( new Error ( 'Transaction aborted' ) )
154+ } )
155+ return result
156+ } catch ( e ) {
157+ console . error ( "Failed to process with transaction" , e )
158+ if ( ! trans . error && trans . mode !== 'readonly' ) {
159+ try {
160+ trans . abort ( )
161+ } catch ( ignored ) { }
162+ }
163+ throw e
164+ }
199165 }
200166
201- async getBytesInUse < T = { [ key : string ] : any } > (
202- keys ?: keyof T | Array < keyof T > | null | ArgCallback < number > ,
203- callback ?: ArgCallback < number >
204- ) : Promise < number > {
205- [ keys , callback ] = typeof keys === 'function' ? [ null , keys ] : [ keys , callback ]
206- const value = 0
207- callback ?. ( value )
208- return value
167+ protected assertIndex ( store : IDBObjectStore , key : Key < T > | Key < T > [ ] ) : IDBIndex {
168+ const idxName = formatIdxName ( key )
169+ try {
170+ return store . index ( idxName )
171+ } catch ( err ) {
172+ console . error ( `Failed to query index: table= ${ this . table } ` , err )
173+ throw err
174+ }
209175 }
210- }
176+ }
0 commit comments