Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,11 @@
"echarts": "^6.0.0",
"element-plus": "2.13.2",
"punycode": "^2.3.1",
"typescript-guard": "^0.2.1",
"vue": "^3.5.28",
"vue-router": "^5.0.2"
},
"engines": {
"node": ">=22"
}
}
}
1 change: 0 additions & 1 deletion rspack/rspack.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const generateJsonPlugins: RspackPluginInstance[] = []

const localeJsonFiles = Object.entries(i18nChrome)
.map(([locale, message]) => new GenerateJsonPlugin(`_locales/${locale}/messages.json`, message))
.map(plugin => plugin as unknown as RspackPluginInstance)
generateJsonPlugins.push(...localeJsonFiles)

type EntryConfig = {
Expand Down
2 changes: 1 addition & 1 deletion src/background/message-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class MessageDispatcher {
const result = await handler(message.data, sender)
return { code: 'success', data: result }
} catch (error) {
const msg = (error as Error)?.message ?? error?.toString?.()
const msg = error instanceof Error ? error.message : error?.toString?.()
return { code: 'fail', msg }
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/background/track-server/group.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import itemService from '@service/item-service'

function handleTabGroupRemove(group: chrome.tabGroups.TabGroup) {
itemService.batchDeleteGroupById(group.id)
itemService.deleteByGroup(group.id)
}

export function handleTabGroupEnabled() {
Expand Down
4 changes: 0 additions & 4 deletions src/database/backup-database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ class BackupDatabase extends BaseDatabase {
async updateCache(type: timer.backup.Type, newVal: unknown): Promise<void> {
return this.storage.put(cacheKeyOf(type), newVal as Object)
}

async importData(_data: any): Promise<void> {
// Do nothing
}
}

const backupDatabase = new BackupDatabase()
Expand Down
8 changes: 0 additions & 8 deletions src/database/common/base-database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,4 @@ export default abstract class BaseDatabase {
protected setByKey(key: string, val: any): Promise<void> {
return this.storage.put(key, val)
}

/**
* Import data
*
* @since 0.2.5
* @param data backup data
*/
abstract importData(data: any): Promise<void>
}
185 changes: 185 additions & 0 deletions src/database/common/indexed-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
const ALL_TABLES = ['stat'] as const

export type Table = typeof ALL_TABLES[number]

export type Key<T = Record<string, number>> = keyof T & string

type IndexConfig<T = Record<string, unknown>> = {
key: Key<T> | Key<T>[]
unique?: boolean
}

export type Index<T = Record<string, unknown>> = Key<T> | Key<T>[] | IndexConfig<T>

const DB_NAME = `tt4b_${chrome.runtime.id}`

function normalizeIndex<T = Record<string, number>>(index: Index<T>): IndexConfig<T> {
return typeof index === 'string' || Array.isArray(index) ? { key: index } : index
}

function formatIdxName<T = Record<string, number>>(key: IndexConfig<T>['key']): string {
const keyStr = Array.isArray(key) ? key.sort().join('_') : key
return `idx_${keyStr}`
}

export function req2Promise<T = unknown>(req: IDBRequest<T>): Promise<T | undefined> {
return new Promise((resolve, reject) => {
req.onsuccess = () => resolve(req.result)
req.onerror = (ev) => {
console.error("Failed to request indexed-db", ev, req.error)
reject(req.error)
}
})
}

export async function iterateCursor<T = unknown>(
req: IDBRequest<IDBCursorWithValue | null>
): Promise<readonly T[]>
export async function iterateCursor<T = unknown>(
req: IDBRequest<IDBCursorWithValue | null>,
processor: (cursor: IDBCursorWithValue) => void | Promise<void>
): Promise<void>

export async function iterateCursor<T = unknown>(
req: IDBRequest<IDBCursorWithValue | null>,
processor?: (cursor: IDBCursorWithValue) => void | Promise<void>
): Promise<readonly T[] | void> {
const collectResults = !processor
const results: T[] = []

return new Promise((resolve, reject) => {
req.onsuccess = async () => {
const cursor = req.result
if (!cursor) return resolve(collectResults ? results : undefined)

try {
collectResults && results.push(cursor.value as T)
await processor?.(cursor)
cursor.continue()
} catch (error) {
reject(error)
}
}

req.onerror = () => reject(req.error)
})
}

export abstract class BaseIDBStorage<T = Record<string, unknown>> {
private db: IDBDatabase | undefined
abstract indexes: Index<T>[]
abstract key: Key<T> | Key<T>[]
abstract table: Table

protected async initDb(): Promise<IDBDatabase> {
if (this.db) return this.db

const factory = typeof window === 'undefined' ? self.indexedDB : window.indexedDB
const checkRequest = factory.open(DB_NAME)

return new Promise((resolve, reject) => {
checkRequest.onsuccess = () => {
const db = checkRequest.result
const storeExisted = db.objectStoreNames.contains(this.table)
const needUpgrade = !storeExisted || this.needUpgradeIndexes(db)

if (!needUpgrade) {
this.db = db
return resolve(db)
}

const currentVersion = db.version
db.close()

const upgradeRequest = factory.open(DB_NAME, currentVersion + 1)

upgradeRequest.onupgradeneeded = () => {
const upgradeDb = upgradeRequest.result
const transaction = upgradeRequest.transaction
if (!transaction) return reject("Failed to get transaction of upgrading request")

let store = upgradeDb.objectStoreNames.contains(this.table)
? transaction.objectStore(this.table)
: upgradeDb.createObjectStore(this.table, { keyPath: this.key })
this.createIndexes(store)
}

upgradeRequest.onsuccess = () => {
console.log("IndexedDB upgraded")
this.db = upgradeRequest.result
resolve(upgradeRequest.result)
}

upgradeRequest.onerror = () => reject(upgradeRequest.error)
}

checkRequest.onerror = () => reject(checkRequest.error)
})
}

private needUpgradeIndexes(db: IDBDatabase): boolean {
try {
const transaction = db.transaction(this.table, 'readonly')
const store = transaction.objectStore(this.table)
const indexNames = store.indexNames

for (const index of this.indexes) {
const { key } = normalizeIndex(index)
const idxName = formatIdxName(key)
if (!indexNames.contains(idxName)) {
return true
}
}
return false
} catch (e) {
console.error("Failed to check indexes", e)
return true
}
}

private createIndexes(store: IDBObjectStore) {
const existingIndexes = store.indexNames

for (const index of this.indexes) {
const { key, unique } = normalizeIndex(index)
const idxName = formatIdxName(key)
if (!existingIndexes.contains(idxName)) {
store.createIndex(idxName, key, { unique })
}
}
}

protected async withStore<T = unknown>(operation: (store: IDBObjectStore) => T | Promise<T>, mode?: IDBTransactionMode): Promise<T> {
const db = await this.initDb()
const trans = db.transaction(this.table, mode ?? 'readwrite')
try {
const store = trans.objectStore(this.table)
const result = await operation(store)
// Waiting for transaction completed
await new Promise<void>((resolve, reject) => {
trans.oncomplete = () => resolve()
trans.onerror = () => reject(trans.error)
trans.onabort = () => reject(new Error('Transaction aborted'))
})
return result
} catch (e) {
console.error("Failed to process with transaction", e)
if (!trans.error && trans.mode !== 'readonly') {
try {
trans.abort()
} catch (ignored) { }
}
throw e
}
}

protected assertIndex(store: IDBObjectStore, key: Key<T> | Key<T>[]): IDBIndex {
const idxName = formatIdxName(key)
try {
return store.index(idxName)
} catch (err) {
console.error(`Failed to query index: table=${this.table}`, err)
throw err
}
}
}
29 changes: 29 additions & 0 deletions src/database/common/migratable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { BrowserMigratableNamespace } from '@db/types'
import { isRecord } from '@util/guard'
import { createObjectGuard, isInt, isString, TypeGuard } from 'typescript-guard'

export const isExportData = createObjectGuard<Pick<timer.backup.ExportData, '__meta__'>>({
__meta__: createObjectGuard({
version: isString,
ts: isInt,
}),
})

export const isLegacyVersion = (data: unknown): data is timer.backup.ExportData => {
if (!isExportData(data)) return false

const version = data.__meta__.version
const match = version.match(/^(\d+)\.(\d+)\.(\d+)/)
if (!match) return true

const major = parseInt(match[1])

return major < 4
}

export const extractNamespace = <T>(data: unknown, namespace: BrowserMigratableNamespace, guard: TypeGuard<T>): T | undefined => {
if (!isRecord(data)) return undefined
if (!(namespace in data)) return undefined
const nsData = data[namespace]
return guard(nsData) ? nsData : undefined
}
23 changes: 23 additions & 0 deletions src/database/common/storage-holder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import optionHolder from '@service/components/option-holder'

export class StorageHolder<Database> {
current: Database
delegates: Record<timer.option.StorageType, Database>

constructor(delegates: Record<timer.option.StorageType, Database>) {
this.delegates = delegates
this.current = delegates['classic']

optionHolder.get().then(val => this.handleOption(val))
optionHolder.addChangeListener(val => this.handleOption(val))
}

private handleOption(option: timer.option.TrackingOption) {
const delegate = this.delegates[option.storage]
delegate && (this.current = delegate)
}

get(type: timer.option.StorageType): Database | null {
return this.delegates[type] ?? null
}
}
2 changes: 1 addition & 1 deletion src/database/common/storage-promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default class StoragePromise {
/**
* @since 0.5.0
*/
put(key: string, val: Object): Promise<void> {
put(key: string, val: unknown): Promise<void> {
return this.set({ [key]: val })
}

Expand Down
Loading
Loading