refactor: moved most of sqlite implementation to core
This commit is contained in:
parent
9eb3ed0088
commit
142dddd253
13 changed files with 275 additions and 245 deletions
|
@ -2,4 +2,5 @@ export * from './driver.js'
|
||||||
export * from './memory/index.js'
|
export * from './memory/index.js'
|
||||||
export * from './provider.js'
|
export * from './provider.js'
|
||||||
export * from './repository/index.js'
|
export * from './repository/index.js'
|
||||||
|
export * from './sqlite/index.js'
|
||||||
export * from './storage.js'
|
export * from './storage.js'
|
||||||
|
|
168
packages/core/src/storage/sqlite/driver.ts
Normal file
168
packages/core/src/storage/sqlite/driver.ts
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
import { getPlatform } from '../../platform.js'
|
||||||
|
import { BaseStorageDriver } from '../driver.js'
|
||||||
|
import { ISqliteDatabase, ISqliteStatement } from './types.js'
|
||||||
|
|
||||||
|
const MIGRATIONS_TABLE_NAME = 'mtcute_migrations'
|
||||||
|
const MIGRATIONS_TABLE_SQL = `
|
||||||
|
create table if not exists ${MIGRATIONS_TABLE_NAME} (
|
||||||
|
repo text not null primary key,
|
||||||
|
version integer not null
|
||||||
|
);
|
||||||
|
`.trim()
|
||||||
|
|
||||||
|
type MigrationFunction = (db: ISqliteDatabase) => void
|
||||||
|
|
||||||
|
export abstract class BaseSqliteStorageDriver extends BaseStorageDriver {
|
||||||
|
db!: ISqliteDatabase
|
||||||
|
|
||||||
|
private _pending: [ISqliteStatement, unknown[]][] = []
|
||||||
|
private _runMany!: (stmts: [ISqliteStatement, unknown[]][]) => void
|
||||||
|
private _cleanup?: () => void
|
||||||
|
|
||||||
|
private _migrations: Map<string, Map<number, MigrationFunction>> = new Map()
|
||||||
|
private _maxVersion: Map<string, number> = new Map()
|
||||||
|
|
||||||
|
// todo: remove in 1.0.0
|
||||||
|
private _legacyMigrations: Map<string, MigrationFunction> = new Map()
|
||||||
|
|
||||||
|
registerLegacyMigration(repo: string, migration: MigrationFunction): void {
|
||||||
|
if (this.loaded) {
|
||||||
|
throw new Error('Cannot register migrations after loading')
|
||||||
|
}
|
||||||
|
|
||||||
|
this._legacyMigrations.set(repo, migration)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerMigration(repo: string, version: number, migration: MigrationFunction): void {
|
||||||
|
if (this.loaded) {
|
||||||
|
throw new Error('Cannot register migrations after loading')
|
||||||
|
}
|
||||||
|
|
||||||
|
let map = this._migrations.get(repo)
|
||||||
|
|
||||||
|
if (!map) {
|
||||||
|
map = new Map()
|
||||||
|
this._migrations.set(repo, map)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map.has(version)) {
|
||||||
|
throw new Error(`Migration for ${repo} version ${version} is already registered`)
|
||||||
|
}
|
||||||
|
|
||||||
|
map.set(version, migration)
|
||||||
|
|
||||||
|
const prevMax = this._maxVersion.get(repo) ?? 0
|
||||||
|
|
||||||
|
if (version > prevMax) {
|
||||||
|
this._maxVersion.set(repo, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onLoad = new Set<(db: ISqliteDatabase) => void>()
|
||||||
|
|
||||||
|
onLoad(cb: (db: ISqliteDatabase) => void): void {
|
||||||
|
if (this.loaded) {
|
||||||
|
cb(this.db)
|
||||||
|
} else {
|
||||||
|
this._onLoad.add(cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeLater(stmt: ISqliteStatement, params: unknown[]): void {
|
||||||
|
this._pending.push([stmt, params])
|
||||||
|
}
|
||||||
|
|
||||||
|
private _runLegacyMigrations = false
|
||||||
|
|
||||||
|
_initialize(): void {
|
||||||
|
const hasLegacyTables = this.db
|
||||||
|
.prepare("select name from sqlite_master where type = 'table' and name = 'kv'")
|
||||||
|
.get()
|
||||||
|
|
||||||
|
if (hasLegacyTables) {
|
||||||
|
this._log.info('legacy tables detected, will run migrations')
|
||||||
|
this._runLegacyMigrations = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.db.exec(MIGRATIONS_TABLE_SQL)
|
||||||
|
|
||||||
|
const writeVersion = this.db.prepare(
|
||||||
|
`insert or replace into ${MIGRATIONS_TABLE_NAME} (repo, version) values (?, ?)`,
|
||||||
|
)
|
||||||
|
const getVersion = this.db.prepare(`select version from ${MIGRATIONS_TABLE_NAME} where repo = ?`)
|
||||||
|
|
||||||
|
const didUpgrade = new Set<string>()
|
||||||
|
|
||||||
|
for (const repo of this._migrations.keys()) {
|
||||||
|
const res = getVersion.get(repo) as { version: number } | undefined
|
||||||
|
|
||||||
|
const startVersion = res?.version ?? 0
|
||||||
|
let fromVersion = startVersion
|
||||||
|
|
||||||
|
const migrations = this._migrations.get(repo)!
|
||||||
|
const targetVer = this._maxVersion.get(repo)!
|
||||||
|
|
||||||
|
while (fromVersion < targetVer) {
|
||||||
|
const nextVersion = fromVersion + 1
|
||||||
|
const migration = migrations.get(nextVersion)
|
||||||
|
|
||||||
|
if (!migration) {
|
||||||
|
throw new Error(`No migration for ${repo} to version ${nextVersion}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
migration(this.db)
|
||||||
|
|
||||||
|
fromVersion = nextVersion
|
||||||
|
didUpgrade.add(repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromVersion !== startVersion) {
|
||||||
|
writeVersion.run(repo, targetVer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract _createDatabase(): ISqliteDatabase
|
||||||
|
|
||||||
|
_load(): void {
|
||||||
|
this.db = this._createDatabase()
|
||||||
|
|
||||||
|
this._runMany = this.db.transaction((stmts: [ISqliteStatement, unknown[]][]) => {
|
||||||
|
stmts.forEach((stmt) => {
|
||||||
|
stmt[0].run(stmt[1])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.db.transaction(() => this._initialize())()
|
||||||
|
|
||||||
|
this._cleanup = getPlatform().beforeExit(() => {
|
||||||
|
this._save()
|
||||||
|
this._destroy()
|
||||||
|
})
|
||||||
|
for (const cb of this._onLoad) cb(this.db)
|
||||||
|
|
||||||
|
if (this._runLegacyMigrations) {
|
||||||
|
this.db.transaction(() => {
|
||||||
|
for (const migration of this._legacyMigrations.values()) {
|
||||||
|
migration(this.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// in case _writeLater was used
|
||||||
|
this._runMany(this._pending)
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_save(): void {
|
||||||
|
if (!this._pending.length) return
|
||||||
|
|
||||||
|
this._runMany(this._pending)
|
||||||
|
this._pending = []
|
||||||
|
}
|
||||||
|
|
||||||
|
_destroy(): void {
|
||||||
|
this.db.close()
|
||||||
|
this._cleanup?.()
|
||||||
|
this._cleanup = undefined
|
||||||
|
}
|
||||||
|
}
|
19
packages/core/src/storage/sqlite/index.ts
Normal file
19
packages/core/src/storage/sqlite/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { ITelegramStorageProvider } from '../../highlevel/storage/provider.js'
|
||||||
|
import { IMtStorageProvider } from '../provider.js'
|
||||||
|
import { BaseSqliteStorageDriver } from './driver.js'
|
||||||
|
import { SqliteAuthKeysRepository } from './repository/auth-keys.js'
|
||||||
|
import { SqliteKeyValueRepository } from './repository/kv.js'
|
||||||
|
import { SqlitePeersRepository } from './repository/peers.js'
|
||||||
|
import { SqliteRefMessagesRepository } from './repository/ref-messages.js'
|
||||||
|
|
||||||
|
export { BaseSqliteStorageDriver }
|
||||||
|
export * from './types.js'
|
||||||
|
|
||||||
|
export class BaseSqliteStorage implements IMtStorageProvider, ITelegramStorageProvider {
|
||||||
|
constructor(readonly driver: BaseSqliteStorageDriver) {}
|
||||||
|
|
||||||
|
readonly authKeys = new SqliteAuthKeysRepository(this.driver)
|
||||||
|
readonly kv = new SqliteKeyValueRepository(this.driver)
|
||||||
|
readonly refMessages = new SqliteRefMessagesRepository(this.driver)
|
||||||
|
readonly peers = new SqlitePeersRepository(this.driver)
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
import { Statement } from 'better-sqlite3'
|
import { IAuthKeysRepository } from '../../repository/auth-keys.js'
|
||||||
|
import { BaseSqliteStorageDriver } from '../driver.js'
|
||||||
import { IAuthKeysRepository } from '@mtcute/core'
|
import { ISqliteStatement } from '../types.js'
|
||||||
|
|
||||||
import { SqliteStorageDriver } from '../driver.js'
|
|
||||||
|
|
||||||
interface AuthKeyDto {
|
interface AuthKeyDto {
|
||||||
dc: number
|
dc: number
|
||||||
|
@ -15,7 +13,7 @@ interface TempAuthKeyDto extends AuthKeyDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SqliteAuthKeysRepository implements IAuthKeysRepository {
|
export class SqliteAuthKeysRepository implements IAuthKeysRepository {
|
||||||
constructor(readonly _driver: SqliteStorageDriver) {
|
constructor(readonly _driver: BaseSqliteStorageDriver) {
|
||||||
_driver.registerMigration('auth_keys', 1, (db) => {
|
_driver.registerMigration('auth_keys', 1, (db) => {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
create table if not exists auth_keys (
|
create table if not exists auth_keys (
|
||||||
|
@ -47,8 +45,8 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private _set!: Statement
|
private _set!: ISqliteStatement
|
||||||
private _del!: Statement
|
private _del!: ISqliteStatement
|
||||||
set(dc: number, key: Uint8Array | null): void {
|
set(dc: number, key: Uint8Array | null): void {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
this._del.run(dc)
|
this._del.run(dc)
|
||||||
|
@ -59,7 +57,7 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository {
|
||||||
this._set.run(dc, key)
|
this._set.run(dc, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _get!: Statement
|
private _get!: ISqliteStatement
|
||||||
get(dc: number): Uint8Array | null {
|
get(dc: number): Uint8Array | null {
|
||||||
const row = this._get.get(dc)
|
const row = this._get.get(dc)
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
|
@ -67,8 +65,8 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository {
|
||||||
return (row as AuthKeyDto).key
|
return (row as AuthKeyDto).key
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setTemp!: Statement
|
private _setTemp!: ISqliteStatement
|
||||||
private _delTemp!: Statement
|
private _delTemp!: ISqliteStatement
|
||||||
setTemp(dc: number, idx: number, key: Uint8Array | null, expires: number): void {
|
setTemp(dc: number, idx: number, key: Uint8Array | null, expires: number): void {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
this._delTemp.run(dc, idx)
|
this._delTemp.run(dc, idx)
|
||||||
|
@ -79,7 +77,7 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository {
|
||||||
this._setTemp.run(dc, idx, key, expires)
|
this._setTemp.run(dc, idx, key, expires)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getTemp!: Statement
|
private _getTemp!: ISqliteStatement
|
||||||
getTemp(dc: number, idx: number, now: number): Uint8Array | null {
|
getTemp(dc: number, idx: number, now: number): Uint8Array | null {
|
||||||
const row = this._getTemp.get(dc, idx, now)
|
const row = this._getTemp.get(dc, idx, now)
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
|
@ -87,13 +85,13 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository {
|
||||||
return (row as TempAuthKeyDto).key
|
return (row as TempAuthKeyDto).key
|
||||||
}
|
}
|
||||||
|
|
||||||
private _delTempAll!: Statement
|
private _delTempAll!: ISqliteStatement
|
||||||
deleteByDc(dc: number): void {
|
deleteByDc(dc: number): void {
|
||||||
this._del.run(dc)
|
this._del.run(dc)
|
||||||
this._delTempAll.run(dc)
|
this._delTempAll.run(dc)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _delAll!: Statement
|
private _delAll!: ISqliteStatement
|
||||||
deleteAll(): void {
|
deleteAll(): void {
|
||||||
this._delAll.run()
|
this._delAll.run()
|
||||||
}
|
}
|
|
@ -1,11 +1,13 @@
|
||||||
import { Statement } from 'better-sqlite3'
|
|
||||||
|
|
||||||
import { IKeyValueRepository } from '@mtcute/core'
|
|
||||||
import { CurrentUserService, DefaultDcsService, ServiceOptions, UpdatesStateService } from '@mtcute/core/utils.js'
|
|
||||||
import { __tlReaderMap } from '@mtcute/tl/binary/reader.js'
|
import { __tlReaderMap } from '@mtcute/tl/binary/reader.js'
|
||||||
import { __tlWriterMap } from '@mtcute/tl/binary/writer.js'
|
import { __tlWriterMap } from '@mtcute/tl/binary/writer.js'
|
||||||
|
|
||||||
import { SqliteStorageDriver } from '../driver.js'
|
import { CurrentUserService } from '../../../highlevel/storage/service/current-user.js'
|
||||||
|
import { UpdatesStateService } from '../../../highlevel/storage/service/updates.js'
|
||||||
|
import { IKeyValueRepository } from '../../repository/key-value.js'
|
||||||
|
import { ServiceOptions } from '../../service/base.js'
|
||||||
|
import { DefaultDcsService } from '../../service/default-dcs.js'
|
||||||
|
import { BaseSqliteStorageDriver } from '../driver.js'
|
||||||
|
import { ISqliteStatement } from '../types.js'
|
||||||
|
|
||||||
interface KeyValueDto {
|
interface KeyValueDto {
|
||||||
key: string
|
key: string
|
||||||
|
@ -13,7 +15,7 @@ interface KeyValueDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SqliteKeyValueRepository implements IKeyValueRepository {
|
export class SqliteKeyValueRepository implements IKeyValueRepository {
|
||||||
constructor(readonly _driver: SqliteStorageDriver) {
|
constructor(readonly _driver: BaseSqliteStorageDriver) {
|
||||||
_driver.registerMigration('kv', 1, (db) => {
|
_driver.registerMigration('kv', 1, (db) => {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
create table key_value (
|
create table key_value (
|
||||||
|
@ -88,12 +90,12 @@ export class SqliteKeyValueRepository implements IKeyValueRepository {
|
||||||
/* eslint-enable @typescript-eslint/no-unsafe-argument */
|
/* eslint-enable @typescript-eslint/no-unsafe-argument */
|
||||||
}
|
}
|
||||||
|
|
||||||
private _set!: Statement
|
private _set!: ISqliteStatement
|
||||||
set(key: string, value: Uint8Array): void {
|
set(key: string, value: Uint8Array): void {
|
||||||
this._driver._writeLater(this._set, [key, value])
|
this._driver._writeLater(this._set, [key, value])
|
||||||
}
|
}
|
||||||
|
|
||||||
private _get!: Statement
|
private _get!: ISqliteStatement
|
||||||
get(key: string): Uint8Array | null {
|
get(key: string): Uint8Array | null {
|
||||||
const res = this._get.get(key)
|
const res = this._get.get(key)
|
||||||
if (!res) return null
|
if (!res) return null
|
||||||
|
@ -101,12 +103,12 @@ export class SqliteKeyValueRepository implements IKeyValueRepository {
|
||||||
return (res as KeyValueDto).value
|
return (res as KeyValueDto).value
|
||||||
}
|
}
|
||||||
|
|
||||||
private _del!: Statement
|
private _del!: ISqliteStatement
|
||||||
delete(key: string): void {
|
delete(key: string): void {
|
||||||
this._del.run(key)
|
this._del.run(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _delAll!: Statement
|
private _delAll!: ISqliteStatement
|
||||||
deleteAll(): void {
|
deleteAll(): void {
|
||||||
this._delAll.run()
|
this._delAll.run()
|
||||||
}
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
import { Statement } from 'better-sqlite3'
|
import { IPeersRepository } from '../../../highlevel/storage/repository/peers.js'
|
||||||
|
import { BaseSqliteStorageDriver } from '../driver.js'
|
||||||
import { IPeersRepository } from '@mtcute/core'
|
import { ISqliteStatement } from '../types.js'
|
||||||
|
|
||||||
import { SqliteStorageDriver } from '../driver.js'
|
|
||||||
|
|
||||||
interface PeerDto {
|
interface PeerDto {
|
||||||
id: number
|
id: number
|
||||||
|
@ -26,7 +24,7 @@ function mapPeerDto(dto: PeerDto): IPeersRepository.PeerInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SqlitePeersRepository implements IPeersRepository {
|
export class SqlitePeersRepository implements IPeersRepository {
|
||||||
constructor(readonly _driver: SqliteStorageDriver) {
|
constructor(readonly _driver: BaseSqliteStorageDriver) {
|
||||||
_driver.registerMigration('peers', 1, (db) => {
|
_driver.registerMigration('peers', 1, (db) => {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
create table peers (
|
create table peers (
|
||||||
|
@ -60,7 +58,7 @@ export class SqlitePeersRepository implements IPeersRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private _store!: Statement
|
private _store!: ISqliteStatement
|
||||||
store(peer: IPeersRepository.PeerInfo): void {
|
store(peer: IPeersRepository.PeerInfo): void {
|
||||||
this._driver._writeLater(this._store, [
|
this._driver._writeLater(this._store, [
|
||||||
peer.id,
|
peer.id,
|
||||||
|
@ -73,7 +71,7 @@ export class SqlitePeersRepository implements IPeersRepository {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getById!: Statement
|
private _getById!: ISqliteStatement
|
||||||
getById(id: number): IPeersRepository.PeerInfo | null {
|
getById(id: number): IPeersRepository.PeerInfo | null {
|
||||||
const row = this._getById.get(id)
|
const row = this._getById.get(id)
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
|
@ -81,7 +79,7 @@ export class SqlitePeersRepository implements IPeersRepository {
|
||||||
return mapPeerDto(row as PeerDto)
|
return mapPeerDto(row as PeerDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getByUsername!: Statement
|
private _getByUsername!: ISqliteStatement
|
||||||
getByUsername(username: string): IPeersRepository.PeerInfo | null {
|
getByUsername(username: string): IPeersRepository.PeerInfo | null {
|
||||||
const row = this._getByUsername.get(username)
|
const row = this._getByUsername.get(username)
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
|
@ -89,7 +87,7 @@ export class SqlitePeersRepository implements IPeersRepository {
|
||||||
return mapPeerDto(row as PeerDto)
|
return mapPeerDto(row as PeerDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getByPhone!: Statement
|
private _getByPhone!: ISqliteStatement
|
||||||
getByPhone(phone: string): IPeersRepository.PeerInfo | null {
|
getByPhone(phone: string): IPeersRepository.PeerInfo | null {
|
||||||
const row = this._getByPhone.get(phone)
|
const row = this._getByPhone.get(phone)
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
|
@ -97,7 +95,7 @@ export class SqlitePeersRepository implements IPeersRepository {
|
||||||
return mapPeerDto(row as PeerDto)
|
return mapPeerDto(row as PeerDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _delAll!: Statement
|
private _delAll!: ISqliteStatement
|
||||||
deleteAll(): void {
|
deleteAll(): void {
|
||||||
this._delAll.run()
|
this._delAll.run()
|
||||||
}
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
import { Statement } from 'better-sqlite3'
|
|
||||||
|
|
||||||
import { IReferenceMessagesRepository } from '@mtcute/core'
|
import { IReferenceMessagesRepository } from '@mtcute/core'
|
||||||
|
|
||||||
import { SqliteStorageDriver } from '../driver.js'
|
import { BaseSqliteStorageDriver } from '../driver.js'
|
||||||
|
import { ISqliteStatement } from '../types.js'
|
||||||
|
|
||||||
interface ReferenceMessageDto {
|
interface ReferenceMessageDto {
|
||||||
peer_id: number
|
peer_id: number
|
||||||
|
@ -11,7 +10,7 @@ interface ReferenceMessageDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SqliteRefMessagesRepository implements IReferenceMessagesRepository {
|
export class SqliteRefMessagesRepository implements IReferenceMessagesRepository {
|
||||||
constructor(readonly _driver: SqliteStorageDriver) {
|
constructor(readonly _driver: BaseSqliteStorageDriver) {
|
||||||
_driver.registerMigration('ref_messages', 1, (db) => {
|
_driver.registerMigration('ref_messages', 1, (db) => {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
create table if not exists message_refs (
|
create table if not exists message_refs (
|
||||||
|
@ -36,12 +35,12 @@ export class SqliteRefMessagesRepository implements IReferenceMessagesRepository
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private _store!: Statement
|
private _store!: ISqliteStatement
|
||||||
store(peerId: number, chatId: number, msgId: number): void {
|
store(peerId: number, chatId: number, msgId: number): void {
|
||||||
this._store.run(peerId, chatId, msgId)
|
this._store.run(peerId, chatId, msgId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getByPeer!: Statement
|
private _getByPeer!: ISqliteStatement
|
||||||
getByPeer(peerId: number): [number, number] | null {
|
getByPeer(peerId: number): [number, number] | null {
|
||||||
const res = this._getByPeer.get(peerId)
|
const res = this._getByPeer.get(peerId)
|
||||||
if (!res) return null
|
if (!res) return null
|
||||||
|
@ -51,19 +50,19 @@ export class SqliteRefMessagesRepository implements IReferenceMessagesRepository
|
||||||
return [res_.chat_id, res_.msg_id]
|
return [res_.chat_id, res_.msg_id]
|
||||||
}
|
}
|
||||||
|
|
||||||
private _del!: Statement
|
private _del!: ISqliteStatement
|
||||||
delete(chatId: number, msgIds: number[]): void {
|
delete(chatId: number, msgIds: number[]): void {
|
||||||
for (const msgId of msgIds) {
|
for (const msgId of msgIds) {
|
||||||
this._driver._writeLater(this._del, [chatId, msgId])
|
this._driver._writeLater(this._del, [chatId, msgId])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _delByPeer!: Statement
|
private _delByPeer!: ISqliteStatement
|
||||||
deleteByPeer(peerId: number): void {
|
deleteByPeer(peerId: number): void {
|
||||||
this._delByPeer.run(peerId)
|
this._delByPeer.run(peerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _delAll!: Statement
|
private _delAll!: ISqliteStatement
|
||||||
deleteAll(): void {
|
deleteAll(): void {
|
||||||
this._delAll.run()
|
this._delAll.run()
|
||||||
}
|
}
|
22
packages/core/src/storage/sqlite/types.ts
Normal file
22
packages/core/src/storage/sqlite/types.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* An abstract interface for a SQLite database.
|
||||||
|
*
|
||||||
|
* Roughly based on `better-sqlite3`'s `Database` class,
|
||||||
|
* (which can be used as-is), but only with the methods
|
||||||
|
* that are used by mtcute.
|
||||||
|
*/
|
||||||
|
export interface ISqliteDatabase {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
transaction<F extends (...args: any[]) => any>(fn: F): F
|
||||||
|
|
||||||
|
prepare<BindParameters extends unknown[]>(sql: string): ISqliteStatement<BindParameters>
|
||||||
|
|
||||||
|
exec(sql: string): void
|
||||||
|
close(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISqliteStatement<BindParameters extends unknown[] = unknown[]> {
|
||||||
|
run(...params: BindParameters): void
|
||||||
|
get(...params: BindParameters): unknown
|
||||||
|
all(...params: BindParameters): unknown[]
|
||||||
|
}
|
|
@ -26,13 +26,5 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mtcute/test": "workspace:^"
|
"@mtcute/test": "workspace:^"
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@mtcute/sqlite": "workspace:^"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@mtcute/sqlite": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { MaybePromise } from '@mtcute/core'
|
import { BaseSqliteStorage, BaseSqliteStorageDriver, ISqliteStatement, MaybePromise } from '@mtcute/core'
|
||||||
import type { SqliteStorage, SqliteStorageDriver, Statement } from '@mtcute/sqlite'
|
|
||||||
|
|
||||||
import { IStateStorageProvider } from '../provider.js'
|
import { IStateStorageProvider } from '../provider.js'
|
||||||
import { IStateRepository } from '../repository.js'
|
import { IStateRepository } from '../repository.js'
|
||||||
|
@ -15,7 +14,7 @@ interface RateLimitDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SqliteStateRepository implements IStateRepository {
|
class SqliteStateRepository implements IStateRepository {
|
||||||
constructor(readonly _driver: SqliteStorageDriver) {
|
constructor(readonly _driver: BaseSqliteStorageDriver) {
|
||||||
_driver.registerMigration('state', 1, (db) => {
|
_driver.registerMigration('state', 1, (db) => {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
create table fsm_state (
|
create table fsm_state (
|
||||||
|
@ -49,12 +48,12 @@ class SqliteStateRepository implements IStateRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setState!: Statement
|
private _setState!: ISqliteStatement
|
||||||
setState(key: string, state: string, ttl?: number | undefined): MaybePromise<void> {
|
setState(key: string, state: string, ttl?: number | undefined): MaybePromise<void> {
|
||||||
this._setState.run(key, state, ttl ? Date.now() + ttl * 1000 : undefined)
|
this._setState.run(key, state, ttl ? Date.now() + ttl * 1000 : undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getState!: Statement
|
private _getState!: ISqliteStatement
|
||||||
getState(key: string, now: number): MaybePromise<string | null> {
|
getState(key: string, now: number): MaybePromise<string | null> {
|
||||||
const res_ = this._getState.get(key)
|
const res_ = this._getState.get(key)
|
||||||
if (!res_) return null
|
if (!res_) return null
|
||||||
|
@ -69,21 +68,21 @@ class SqliteStateRepository implements IStateRepository {
|
||||||
return res.value
|
return res.value
|
||||||
}
|
}
|
||||||
|
|
||||||
private _deleteState!: Statement
|
private _deleteState!: ISqliteStatement
|
||||||
deleteState(key: string): MaybePromise<void> {
|
deleteState(key: string): MaybePromise<void> {
|
||||||
this._deleteState.run(key)
|
this._deleteState.run(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _deleteOldState!: Statement
|
private _deleteOldState!: ISqliteStatement
|
||||||
private _deleteOldRl!: Statement
|
private _deleteOldRl!: ISqliteStatement
|
||||||
vacuum(now: number): MaybePromise<void> {
|
vacuum(now: number): MaybePromise<void> {
|
||||||
this._deleteOldState.run(now)
|
this._deleteOldState.run(now)
|
||||||
this._deleteOldRl.run(now)
|
this._deleteOldRl.run(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setRl!: Statement
|
private _setRl!: ISqliteStatement
|
||||||
private _getRl!: Statement
|
private _getRl!: ISqliteStatement
|
||||||
private _deleteRl!: Statement
|
private _deleteRl!: ISqliteStatement
|
||||||
|
|
||||||
getRateLimit(key: string, now: number, limit: number, window: number): [number, number] {
|
getRateLimit(key: string, now: number, limit: number, window: number): [number, number] {
|
||||||
const val = this._getRl.get(key) as RateLimitDto | undefined
|
const val = this._getRl.get(key) as RateLimitDto | undefined
|
||||||
|
@ -117,9 +116,9 @@ class SqliteStateRepository implements IStateRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SqliteStateStorage implements IStateStorageProvider {
|
export class SqliteStateStorage implements IStateStorageProvider {
|
||||||
constructor(readonly driver: SqliteStorageDriver) {}
|
constructor(readonly driver: BaseSqliteStorageDriver) {}
|
||||||
|
|
||||||
static from(provider: SqliteStorage) {
|
static from(provider: BaseSqliteStorage) {
|
||||||
return new SqliteStateStorage(provider.driver)
|
return new SqliteStateStorage(provider.driver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import sqlite3, { Database, Options, Statement } from 'better-sqlite3'
|
import sqlite3, { Options } from 'better-sqlite3'
|
||||||
|
|
||||||
import { BaseStorageDriver } from '@mtcute/core'
|
import { BaseSqliteStorageDriver, ISqliteDatabase } from '@mtcute/core'
|
||||||
import { getPlatform } from '@mtcute/core/platform.js'
|
|
||||||
|
|
||||||
export interface SqliteStorageDriverOptions {
|
export interface SqliteStorageDriverOptions {
|
||||||
/**
|
/**
|
||||||
|
@ -22,19 +21,7 @@ export interface SqliteStorageDriverOptions {
|
||||||
options?: Options
|
options?: Options
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIGRATIONS_TABLE_NAME = 'mtcute_migrations'
|
export class SqliteStorageDriver extends BaseSqliteStorageDriver {
|
||||||
const MIGRATIONS_TABLE_SQL = `
|
|
||||||
create table if not exists ${MIGRATIONS_TABLE_NAME} (
|
|
||||||
repo text not null primary key,
|
|
||||||
version integer not null
|
|
||||||
);
|
|
||||||
`.trim()
|
|
||||||
|
|
||||||
type MigrationFunction = (db: Database) => void
|
|
||||||
|
|
||||||
export class SqliteStorageDriver extends BaseStorageDriver {
|
|
||||||
db!: Database
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly filename = ':memory:',
|
readonly filename = ':memory:',
|
||||||
readonly params?: SqliteStorageDriverOptions,
|
readonly params?: SqliteStorageDriverOptions,
|
||||||
|
@ -42,159 +29,16 @@ export class SqliteStorageDriver extends BaseStorageDriver {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
private _pending: [Statement, unknown[]][] = []
|
_createDatabase(): ISqliteDatabase {
|
||||||
private _runMany!: (stmts: [Statement, unknown[]][]) => void
|
const db = sqlite3(this.filename, {
|
||||||
private _cleanup?: () => void
|
|
||||||
|
|
||||||
private _migrations: Map<string, Map<number, MigrationFunction>> = new Map()
|
|
||||||
private _maxVersion: Map<string, number> = new Map()
|
|
||||||
|
|
||||||
// todo: remove in 1.0.0 + remove direct dep on @mtcute/tl
|
|
||||||
private _legacyMigrations: Map<string, MigrationFunction> = new Map()
|
|
||||||
|
|
||||||
registerLegacyMigration(repo: string, migration: MigrationFunction): void {
|
|
||||||
if (this.loaded) {
|
|
||||||
throw new Error('Cannot register migrations after loading')
|
|
||||||
}
|
|
||||||
|
|
||||||
this._legacyMigrations.set(repo, migration)
|
|
||||||
}
|
|
||||||
|
|
||||||
registerMigration(repo: string, version: number, migration: MigrationFunction): void {
|
|
||||||
if (this.loaded) {
|
|
||||||
throw new Error('Cannot register migrations after loading')
|
|
||||||
}
|
|
||||||
|
|
||||||
let map = this._migrations.get(repo)
|
|
||||||
|
|
||||||
if (!map) {
|
|
||||||
map = new Map()
|
|
||||||
this._migrations.set(repo, map)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (map.has(version)) {
|
|
||||||
throw new Error(`Migration for ${repo} version ${version} is already registered`)
|
|
||||||
}
|
|
||||||
|
|
||||||
map.set(version, migration)
|
|
||||||
|
|
||||||
const prevMax = this._maxVersion.get(repo) ?? 0
|
|
||||||
|
|
||||||
if (version > prevMax) {
|
|
||||||
this._maxVersion.set(repo, version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onLoad = new Set<(db: Database) => void>()
|
|
||||||
|
|
||||||
onLoad(cb: (db: Database) => void): void {
|
|
||||||
if (this.loaded) {
|
|
||||||
cb(this.db)
|
|
||||||
} else {
|
|
||||||
this._onLoad.add(cb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_writeLater(stmt: Statement, params: unknown[]): void {
|
|
||||||
this._pending.push([stmt, params])
|
|
||||||
}
|
|
||||||
|
|
||||||
private _runLegacyMigrations = false
|
|
||||||
|
|
||||||
_initialize(): void {
|
|
||||||
const hasLegacyTables = this.db
|
|
||||||
.prepare("select name from sqlite_master where type = 'table' and name = 'kv'")
|
|
||||||
.get()
|
|
||||||
|
|
||||||
if (hasLegacyTables) {
|
|
||||||
this._log.info('legacy tables detected, will run migrations')
|
|
||||||
this._runLegacyMigrations = true
|
|
||||||
}
|
|
||||||
|
|
||||||
this.db.exec(MIGRATIONS_TABLE_SQL)
|
|
||||||
|
|
||||||
const writeVersion = this.db.prepare(
|
|
||||||
`insert or replace into ${MIGRATIONS_TABLE_NAME} (repo, version) values (?, ?)`,
|
|
||||||
)
|
|
||||||
const getVersion = this.db.prepare(`select version from ${MIGRATIONS_TABLE_NAME} where repo = ?`)
|
|
||||||
|
|
||||||
const didUpgrade = new Set<string>()
|
|
||||||
|
|
||||||
for (const repo of this._migrations.keys()) {
|
|
||||||
const res = getVersion.get(repo) as { version: number } | undefined
|
|
||||||
|
|
||||||
const startVersion = res?.version ?? 0
|
|
||||||
let fromVersion = startVersion
|
|
||||||
|
|
||||||
const migrations = this._migrations.get(repo)!
|
|
||||||
const targetVer = this._maxVersion.get(repo)!
|
|
||||||
|
|
||||||
while (fromVersion < targetVer) {
|
|
||||||
const nextVersion = fromVersion + 1
|
|
||||||
const migration = migrations.get(nextVersion)
|
|
||||||
|
|
||||||
if (!migration) {
|
|
||||||
throw new Error(`No migration for ${repo} to version ${nextVersion}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
migration(this.db)
|
|
||||||
|
|
||||||
fromVersion = nextVersion
|
|
||||||
didUpgrade.add(repo)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fromVersion !== startVersion) {
|
|
||||||
writeVersion.run(repo, targetVer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_load(): void {
|
|
||||||
this.db = sqlite3(this.filename, {
|
|
||||||
...this.params?.options,
|
...this.params?.options,
|
||||||
verbose: this._log.mgr.level >= 5 ? (this._log.verbose as Options['verbose']) : undefined,
|
verbose: this._log.mgr.level >= 5 ? (this._log.verbose as Options['verbose']) : undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!this.params?.disableWal) {
|
if (!this.params?.disableWal) {
|
||||||
this.db.pragma('journal_mode = WAL')
|
db.pragma('journal_mode = WAL')
|
||||||
}
|
}
|
||||||
|
|
||||||
this._runMany = this.db.transaction((stmts: [Statement, unknown[]][]) => {
|
return db as ISqliteDatabase
|
||||||
stmts.forEach((stmt) => {
|
|
||||||
stmt[0].run(stmt[1])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.db.transaction(() => this._initialize())()
|
|
||||||
|
|
||||||
this._cleanup = getPlatform().beforeExit(() => {
|
|
||||||
this._save()
|
|
||||||
this._destroy()
|
|
||||||
})
|
|
||||||
for (const cb of this._onLoad) cb(this.db)
|
|
||||||
|
|
||||||
if (this._runLegacyMigrations) {
|
|
||||||
this.db.transaction(() => {
|
|
||||||
for (const migration of this._legacyMigrations.values()) {
|
|
||||||
migration(this.db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// in case _writeLater was used
|
|
||||||
this._runMany(this._pending)
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_save(): void {
|
|
||||||
if (!this._pending.length) return
|
|
||||||
|
|
||||||
this._runMany(this._pending)
|
|
||||||
this._pending = []
|
|
||||||
}
|
|
||||||
|
|
||||||
_destroy(): void {
|
|
||||||
this.db.close()
|
|
||||||
this._cleanup?.()
|
|
||||||
this._cleanup = undefined
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,15 @@
|
||||||
import { IMtStorageProvider, ITelegramStorageProvider } from '@mtcute/core'
|
import { BaseSqliteStorage } from '@mtcute/core'
|
||||||
|
|
||||||
import { SqliteStorageDriver, SqliteStorageDriverOptions } from './driver.js'
|
import { SqliteStorageDriver, SqliteStorageDriverOptions } from './driver.js'
|
||||||
import { SqliteAuthKeysRepository } from './repository/auth-keys.js'
|
|
||||||
import { SqliteKeyValueRepository } from './repository/kv.js'
|
|
||||||
import { SqlitePeersRepository } from './repository/peers.js'
|
|
||||||
import { SqliteRefMessagesRepository } from './repository/ref-messages.js'
|
|
||||||
|
|
||||||
export { SqliteStorageDriver } from './driver.js'
|
export { SqliteStorageDriver } from './driver.js'
|
||||||
export type { Statement } from 'better-sqlite3'
|
export type { Statement } from 'better-sqlite3'
|
||||||
|
|
||||||
export class SqliteStorage implements IMtStorageProvider, ITelegramStorageProvider {
|
export class SqliteStorage extends BaseSqliteStorage {
|
||||||
constructor(
|
constructor(
|
||||||
readonly filename = ':memory:',
|
readonly filename = ':memory:',
|
||||||
readonly params?: SqliteStorageDriverOptions,
|
readonly params?: SqliteStorageDriverOptions,
|
||||||
) {}
|
) {
|
||||||
|
super(new SqliteStorageDriver(filename, params))
|
||||||
readonly driver = new SqliteStorageDriver(this.filename, this.params)
|
}
|
||||||
|
|
||||||
readonly authKeys = new SqliteAuthKeysRepository(this.driver)
|
|
||||||
readonly kv = new SqliteKeyValueRepository(this.driver)
|
|
||||||
readonly refMessages = new SqliteRefMessagesRepository(this.driver)
|
|
||||||
readonly peers = new SqlitePeersRepository(this.driver)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,9 +205,6 @@ importers:
|
||||||
'@mtcute/core':
|
'@mtcute/core':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../core
|
version: link:../core
|
||||||
'@mtcute/sqlite':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../sqlite
|
|
||||||
events:
|
events:
|
||||||
specifier: 3.2.0
|
specifier: 3.2.0
|
||||||
version: 3.2.0
|
version: 3.2.0
|
||||||
|
|
Loading…
Reference in a new issue