diff --git a/.config/eslint.cjs b/.config/eslint.cjs index 48b52cb1..7320893e 100644 --- a/.config/eslint.cjs +++ b/.config/eslint.cjs @@ -273,11 +273,19 @@ module.exports = { }, }, { - files: ['e2e/**', 'packages/node/**'], + files: ['e2e/**', 'packages/node/**', 'packages/bun/**'], rules: { 'no-restricted-globals': 'off', }, }, + { + files: ['packages/bun/**'], + rules: { + 'import/no-unresolved': 'off', + 'no-restricted-imports': 'off', + 'import/no-relative-packages': 'off', // common-internals is symlinked from node + } + } ], settings: { 'import/resolver': { diff --git a/.config/vite-utils/test-setup.mts b/.config/vite-utils/test-setup.mts index 7e112de8..5b9a57c9 100644 --- a/.config/vite-utils/test-setup.mts +++ b/.config/vite-utils/test-setup.mts @@ -5,5 +5,5 @@ const TEST_ENV = import.meta.env.TEST_ENV if (TEST_ENV === 'browser') { setPlatform(new (await import('../../packages/web/src/platform.js')).WebPlatform()) } else { - setPlatform(new (await import('../../packages/node/src/platform.js')).NodePlatform()) + setPlatform(new (await import('../../packages/node/src/common-internals-node/platform.js')).NodePlatform()) } \ No newline at end of file diff --git a/.config/vite.bun.mts b/.config/vite.bun.mts index 8552be38..10e0d3d8 100644 --- a/.config/vite.bun.mts +++ b/.config/vite.bun.mts @@ -26,7 +26,7 @@ const SKIP_TESTS = [ export default defineConfig({ build: { lib: { - entry: (() => { + entry: process.env.ENTRYPOINT ? [process.env.ENTRYPOINT] : (() => { const files: string[] = [] const packages = resolve(__dirname, '../packages') @@ -57,11 +57,15 @@ export default defineConfig({ 'module', 'fs', 'fs/promises', + 'node:fs', + 'readline', + 'worker_threads', 'events', 'path', 'util', 'os', 'bun:test', + 'bun:sqlite', ], output: { chunkFileNames: 'chunk-[hash].js', @@ -73,7 +77,7 @@ export default defineConfig({ commonjsOptions: { ignoreDynamicRequires: true, }, - outDir: 'dist/tests', + outDir: process.env.OUT_DIR || 'dist/tests', emptyOutDir: true, target: 'esnext', minify: false, diff --git a/.github/actions/init-bun/action.yml b/.github/actions/init-bun/action.yml index b6a45f46..af4e51e4 100644 --- a/.github/actions/init-bun/action.yml +++ b/.github/actions/init-bun/action.yml @@ -1,6 +1,6 @@ inputs: bun-version: - default: '1.0.25' + default: '1.0.32' runs: using: 'composite' diff --git a/e2e/package.json b/e2e/package.json index db322429..a94bfe9f 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -2,6 +2,7 @@ "name": "mtcute-e2e", "private": true, "dependencies": { + "@mtcute/bun": "*", "@mtcute/core": "*", "@mtcute/crypto-node": "*", "@mtcute/dispatcher": "*", @@ -13,7 +14,6 @@ "@mtcute/mtproxy": "*", "@mtcute/node": "*", "@mtcute/socks-proxy": "*", - "@mtcute/sqlite": "*", "@mtcute/tl": "*", "@mtcute/tl-runtime": "*", "@mtcute/tl-utils": "*", diff --git a/e2e/ts/utils.ts b/e2e/ts/utils.ts index 7a392e2c..0e289f24 100644 --- a/e2e/ts/utils.ts +++ b/e2e/ts/utils.ts @@ -4,8 +4,7 @@ import { join } from 'path' import { MaybePromise, MemoryStorage } from '@mtcute/core' import { setPlatform } from '@mtcute/core/platform.js' import { LogManager, sleep } from '@mtcute/core/utils.js' -import { NodeCryptoProvider, NodePlatform, TcpTransport } from '@mtcute/node' -import { SqliteStorage } from '@mtcute/sqlite' +import { NodeCryptoProvider, NodePlatform, SqliteStorage, TcpTransport } from '@mtcute/node' export const getApiParams = (storage?: string) => { if (!process.env.API_ID || !process.env.API_HASH) { diff --git a/packages/bun/README.md b/packages/bun/README.md new file mode 100644 index 00000000..02894f34 --- /dev/null +++ b/packages/bun/README.md @@ -0,0 +1,25 @@ +# @mtcute/bun + +📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_node.html) + +‼️ **Experimental** Bun support package for mtcute. Includes: +- SQLite storage (based on `bun:sqlite`) +- TCP transport (based on Bun-native APIs) +- `TelegramClient` implementation using the above +- HTML and Markdown parsers + +## Usage + +```typescript +import { TelegramClient } from '@mtcute/bun' + +const tg = new TelegramClient({ + apiId: 12345, + apiHash: 'abcdef', + storage: 'my-account' +}) + +tg.run(async (user) => { + console.log(`✨ logged in as ${user.displayName}`) +}) +``` diff --git a/packages/bun/build.config.cjs b/packages/bun/build.config.cjs new file mode 100644 index 00000000..17718026 --- /dev/null +++ b/packages/bun/build.config.cjs @@ -0,0 +1 @@ +module.exports = () => ({ buildCjs: false }) diff --git a/packages/bun/package.json b/packages/bun/package.json new file mode 100644 index 00000000..80e0d713 --- /dev/null +++ b/packages/bun/package.json @@ -0,0 +1,35 @@ +{ + "name": "@mtcute/bun", + "private": true, + "version": "0.8.0", + "description": "Meta-package for Bun", + "author": "alina sireneva ", + "license": "MIT", + "main": "src/index.ts", + "type": "module", + "sideEffects": false, + "scripts": { + "docs": "typedoc", + "build": "pnpm run -w build-package bun" + }, + "exports": { + ".": "./src/index.ts", + "./utils.js": "./src/utils.ts" + }, + "distOnlyFields": { + "exports": { + ".": "./index.js", + "./utils.js": "./utils.js" + } + }, + "dependencies": { + "@mtcute/core": "workspace:^", + "@mtcute/wasm": "workspace:^", + "@mtcute/markdown-parser": "workspace:^", + "@mtcute/html-parser": "workspace:^" + }, + "devDependencies": { + "@mtcute/test": "workspace:^", + "bun-types": "1.0.33" + } +} diff --git a/packages/bun/src/client.ts b/packages/bun/src/client.ts new file mode 100644 index 00000000..fea59d01 --- /dev/null +++ b/packages/bun/src/client.ts @@ -0,0 +1,147 @@ +import { createInterface, Interface as RlInterface } from 'readline' + +import { FileDownloadLocation, FileDownloadParameters, ITelegramStorageProvider, PartialOnly, User } from '@mtcute/core' +import { + BaseTelegramClient as BaseTelegramClientBase, + BaseTelegramClientOptions as BaseTelegramClientOptionsBase, + TelegramClient as TelegramClientBase, + TelegramClientOptions, +} from '@mtcute/core/client.js' +import { setPlatform } from '@mtcute/core/platform.js' + +import { NodePlatform } from './common-internals-node/platform.js' +import { downloadToFile } from './methods/download-file.js' +import { downloadAsNodeStream } from './methods/download-node-stream.js' +import { SqliteStorage } from './sqlite/index.js' +import { BunCryptoProvider } from './utils/crypto.js' +import { TcpTransport } from './utils/tcp.js' + +export type { TelegramClientOptions } + +export interface BaseTelegramClientOptions + extends PartialOnly, 'transport' | 'crypto'> { + /** + * Storage to use for this client. + * + * If a string is passed, it will be used as + * a name for an SQLite database file. + * + * @default `"client.session"` + */ + storage?: string | ITelegramStorageProvider + + /** + * **ADVANCED USE ONLY** + * + * Whether to not set up the platform. + * This is useful if you call `setPlatform` yourself. + */ + platformless?: boolean +} + +export class BaseTelegramClient extends BaseTelegramClientBase { + constructor(opts: BaseTelegramClientOptions) { + if (!opts.platformless) setPlatform(new NodePlatform()) + + super({ + crypto: new BunCryptoProvider(), + transport: () => new TcpTransport(), + ...opts, + storage: + typeof opts.storage === 'string' ? + new SqliteStorage(opts.storage) : + opts.storage ?? new SqliteStorage('client.session'), + }) + } +} + +/** + * Telegram client for use in Node.js + */ +export class TelegramClient extends TelegramClientBase { + constructor(opts: TelegramClientOptions) { + if ('client' in opts) { + super(opts) + + return + } + + super({ + client: new BaseTelegramClient(opts), + }) + } + + private _rl?: RlInterface + + /** + * Tiny wrapper over Node `readline` package + * for simpler user input for `.run()` method. + * + * Associated `readline` interface is closed + * after `run()` returns, or with the client. + * + * @param text Text of the question + */ + input(text: string): Promise { + if (!this._rl) { + this._rl = createInterface({ + input: process.stdin, + output: process.stdout, + }) + } + + return new Promise((res) => this._rl?.question(text, res)) + } + + close(): Promise { + this._rl?.close() + + return super.close() + } + + start(params: Parameters[0] = {}): Promise { + if (!params.botToken) { + if (!params.phone) params.phone = () => this.input('phone > ') + if (!params.code) params.code = () => this.input('code > ') + + if (!params.password) { + params.password = () => this.input('2fa password > ') + } + } + + return super.start(params).then((user) => { + if (this._rl) { + this._rl.close() + delete this._rl + } + + return user + }) + } + + run( + params: Parameters[0] | ((user: User) => void | Promise), + then?: (user: User) => void | Promise, + ): void { + if (typeof params === 'function') { + then = params + params = {} + } + + this.start(params) + .then(then) + .catch((err) => this.emitError(err)) + } + + downloadToFile( + filename: string, + location: FileDownloadLocation, + params?: FileDownloadParameters | undefined, + ): Promise { + return downloadToFile(this, filename, location, params) + } + + downloadAsNodeStream(location: FileDownloadLocation, params?: FileDownloadParameters | undefined) { + return downloadAsNodeStream(this, location, params) + } +} diff --git a/packages/bun/src/common-internals-node b/packages/bun/src/common-internals-node new file mode 120000 index 00000000..d48d33e3 --- /dev/null +++ b/packages/bun/src/common-internals-node @@ -0,0 +1 @@ +../../node/src/common-internals-node \ No newline at end of file diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts new file mode 100644 index 00000000..d354f7eb --- /dev/null +++ b/packages/bun/src/index.ts @@ -0,0 +1,9 @@ +export * from './client.js' +export * from './common-internals-node/platform.js' +export * from './sqlite/index.js' +export * from './utils/crypto.js' +export * from './utils/tcp.js' +export * from './worker.js' +export * from '@mtcute/core' +export * from '@mtcute/html-parser' +export * from '@mtcute/markdown-parser' diff --git a/packages/bun/src/methods.ts b/packages/bun/src/methods.ts new file mode 100644 index 00000000..cef20775 --- /dev/null +++ b/packages/bun/src/methods.ts @@ -0,0 +1,3 @@ +export { downloadToFile } from './methods/download-file.js' +export { downloadAsNodeStream } from './methods/download-node-stream.js' +export * from '@mtcute/core/methods.js' diff --git a/packages/bun/src/methods/download-file.ts b/packages/bun/src/methods/download-file.ts new file mode 100644 index 00000000..f847b361 --- /dev/null +++ b/packages/bun/src/methods/download-file.ts @@ -0,0 +1,39 @@ +import { unlinkSync } from 'node:fs' + +import { FileDownloadLocation, FileDownloadParameters, FileLocation, ITelegramClient } from '@mtcute/core' +import { downloadAsIterable } from '@mtcute/core/methods.js' + +/** + * Download a remote file to a local file (only for NodeJS). + * Promise will resolve once the download is complete. + * + * @param filename Local file name to which the remote file will be downloaded + * @param params File download parameters + */ +export async function downloadToFile( + client: ITelegramClient, + filename: string, + location: FileDownloadLocation, + params?: FileDownloadParameters, +): Promise { + if (location instanceof FileLocation && ArrayBuffer.isView(location.location)) { + // early return for inline files + await Bun.write(filename, location.location) + } + + const output = Bun.file(filename).writer() + + if (params?.abortSignal) { + params.abortSignal.addEventListener('abort', () => { + client.log.debug('aborting file download %s - cleaning up', filename) + Promise.resolve(output.end()).catch(() => {}) + unlinkSync(filename) + }) + } + + for await (const chunk of downloadAsIterable(client, location, params)) { + output.write(chunk) + } + + await output.end() +} diff --git a/packages/bun/src/methods/download-node-stream.ts b/packages/bun/src/methods/download-node-stream.ts new file mode 100644 index 00000000..fa9874cd --- /dev/null +++ b/packages/bun/src/methods/download-node-stream.ts @@ -0,0 +1,19 @@ +import { Readable } from 'stream' + +import { FileDownloadLocation, FileDownloadParameters, ITelegramClient } from '@mtcute/core' +import { downloadAsStream } from '@mtcute/core/methods.js' + +/** + * Download a remote file as a Node.js Readable stream + * (discouraged under Bun, since Web Streams are first-class). + * + * @param params File download parameters + */ +export function downloadAsNodeStream( + client: ITelegramClient, + location: FileDownloadLocation, + params?: FileDownloadParameters, +): Readable { + // @ts-expect-error typings are wrong + return Readable.fromWeb(downloadAsStream(client, location, params)) +} diff --git a/packages/bun/src/sqlite/driver.ts b/packages/bun/src/sqlite/driver.ts new file mode 100644 index 00000000..362e5f97 --- /dev/null +++ b/packages/bun/src/sqlite/driver.ts @@ -0,0 +1,36 @@ +import { Database } from 'bun:sqlite' + +import { BaseSqliteStorageDriver, ISqliteDatabase } from '@mtcute/core' + +export interface SqliteStorageDriverOptions { + /** + * By default, WAL mode is enabled, which + * significantly improves performance. + * [Learn more](https://bun.sh/docs/api/sqlite#wal-mode) + * + * However, you might encounter some issues, + * and if you do, you can disable WAL by passing `true` + * + * @default false + */ + disableWal?: boolean +} + +export class SqliteStorageDriver extends BaseSqliteStorageDriver { + constructor( + readonly filename = ':memory:', + readonly params?: SqliteStorageDriverOptions, + ) { + super() + } + + _createDatabase(): ISqliteDatabase { + const db = new Database(this.filename) + + if (!this.params?.disableWal) { + db.exec('PRAGMA journal_mode = WAL;') + } + + return db as ISqliteDatabase + } +} diff --git a/packages/bun/src/sqlite/index.ts b/packages/bun/src/sqlite/index.ts new file mode 100644 index 00000000..882b228d --- /dev/null +++ b/packages/bun/src/sqlite/index.ts @@ -0,0 +1,14 @@ +import { BaseSqliteStorage } from '@mtcute/core' + +import { SqliteStorageDriver, SqliteStorageDriverOptions } from './driver.js' + +export { SqliteStorageDriver } from './driver.js' + +export class SqliteStorage extends BaseSqliteStorage { + constructor( + readonly filename = ':memory:', + readonly params?: SqliteStorageDriverOptions, + ) { + super(new SqliteStorageDriver(filename, params)) + } +} diff --git a/packages/bun/src/sqlite/sqlite.test.ts b/packages/bun/src/sqlite/sqlite.test.ts new file mode 100644 index 00000000..6810d931 --- /dev/null +++ b/packages/bun/src/sqlite/sqlite.test.ts @@ -0,0 +1,31 @@ +import { afterAll, beforeAll, describe } from 'vitest' + +import { LogManager } from '@mtcute/core/utils.js' +import { + testAuthKeysRepository, + testKeyValueRepository, + testPeersRepository, + testRefMessagesRepository, +} from '@mtcute/test' + +if (import.meta.env.TEST_ENV === 'bun') { + const { SqliteStorage } = await import('./index.js') + + describe('SqliteStorage', () => { + const storage = new SqliteStorage(':memory:') + + beforeAll(async () => { + storage.driver.setup(new LogManager()) + await storage.driver.load() + }) + + testAuthKeysRepository(storage.authKeys) + testKeyValueRepository(storage.kv, storage.driver) + testPeersRepository(storage.peers, storage.driver) + testRefMessagesRepository(storage.refMessages, storage.driver) + + afterAll(() => storage.driver.destroy()) + }) +} else { + describe.skip('SqliteStorage', () => {}) +} diff --git a/packages/bun/src/utils.ts b/packages/bun/src/utils.ts new file mode 100644 index 00000000..3356b98c --- /dev/null +++ b/packages/bun/src/utils.ts @@ -0,0 +1 @@ +export * from '@mtcute/core/utils.js' diff --git a/packages/bun/src/utils/crypto.test.ts b/packages/bun/src/utils/crypto.test.ts new file mode 100644 index 00000000..17292c91 --- /dev/null +++ b/packages/bun/src/utils/crypto.test.ts @@ -0,0 +1,13 @@ +import { describe } from 'vitest' + +import { testCryptoProvider } from '@mtcute/test' + +if (import.meta.env.TEST_ENV === 'bun') { + describe('BunCryptoProvider', async () => { + const { BunCryptoProvider } = await import('./crypto.js') + + testCryptoProvider(new BunCryptoProvider()) + }) +} else { + describe.skip('BunCryptoProvider', () => {}) +} diff --git a/packages/bun/src/utils/crypto.ts b/packages/bun/src/utils/crypto.ts new file mode 100644 index 00000000..02840d3f --- /dev/null +++ b/packages/bun/src/utils/crypto.ts @@ -0,0 +1,119 @@ +// eslint-disable-next-line no-restricted-imports +import { readFile } from 'fs/promises' + +import { BaseCryptoProvider, IAesCtr, ICryptoProvider, IEncryptionScheme } from '@mtcute/core/utils.js' +import { + createCtr256, + ctr256, + deflateMaxSize, + freeCtr256, + gunzip, + ige256Decrypt, + ige256Encrypt, + initSync, +} from '@mtcute/wasm' + +// we currently prefer subtle crypto and wasm for ctr because bun uses browserify polyfills for node:crypto +// which are slow AND semi-broken +// we currently prefer wasm for gzip because bun uses browserify polyfills for node:zlib too +// native node-api addon is broken on macos so we don't support it either +// +// largely just copy-pasting from @mtcute/web, todo: maybe refactor this into common-internals-web? + +const ALGO_TO_SUBTLE: Record = { + sha256: 'SHA-256', + sha1: 'SHA-1', + sha512: 'SHA-512', +} + +export class BunCryptoProvider extends BaseCryptoProvider implements ICryptoProvider { + async initialize(): Promise { + // eslint-disable-next-line no-restricted-globals + const wasmFile = require.resolve('@mtcute/wasm/mtcute.wasm') + const wasm = await readFile(wasmFile) + initSync(wasm) + } + + createAesIge(key: Uint8Array, iv: Uint8Array): IEncryptionScheme { + return { + encrypt(data: Uint8Array): Uint8Array { + return ige256Encrypt(data, key, iv) + }, + decrypt(data: Uint8Array): Uint8Array { + return ige256Decrypt(data, key, iv) + }, + } + } + + createAesCtr(key: Uint8Array, iv: Uint8Array): IAesCtr { + const ctx = createCtr256(key, iv) + + return { + process: (data) => ctr256(ctx, data), + close: () => freeCtr256(ctx), + } + } + + async pbkdf2( + password: Uint8Array, + salt: Uint8Array, + iterations: number, + keylen = 64, + algo = 'sha512', + ): Promise { + const keyMaterial = await crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits']) + + return crypto.subtle + .deriveBits( + { + name: 'PBKDF2', + salt, + iterations, + hash: algo ? ALGO_TO_SUBTLE[algo] : 'SHA-512', + }, + keyMaterial, + (keylen || 64) * 8, + ) + .then((result) => new Uint8Array(result)) + } + + sha1(data: Uint8Array): Uint8Array { + const res = new Uint8Array(Bun.SHA1.byteLength) + Bun.SHA1.hash(data, res) + + return res + } + + sha256(data: Uint8Array): Uint8Array { + const res = new Uint8Array(Bun.SHA256.byteLength) + Bun.SHA256.hash(data, res) + + return res + } + + async hmacSha256(data: Uint8Array, key: Uint8Array): Promise { + const keyMaterial = await crypto.subtle.importKey( + 'raw', + key, + { name: 'HMAC', hash: { name: 'SHA-256' } }, + false, + ['sign'], + ) + + const res = await crypto.subtle.sign({ name: 'HMAC' }, keyMaterial, data) + + return new Uint8Array(res) + } + + gzip(data: Uint8Array, maxSize: number): Uint8Array | null { + return deflateMaxSize(data, maxSize) + } + + gunzip(data: Uint8Array): Uint8Array { + return gunzip(data) + } + + randomFill(buf: Uint8Array) { + crypto.getRandomValues(buf) + } +} diff --git a/packages/bun/src/utils/normalize-file.ts b/packages/bun/src/utils/normalize-file.ts new file mode 100644 index 00000000..236123f2 --- /dev/null +++ b/packages/bun/src/utils/normalize-file.ts @@ -0,0 +1,33 @@ +import { ReadStream } from 'fs' +import { stat } from 'fs/promises' +import { basename } from 'path' +import { Readable as NodeReadable } from 'stream' + +import { UploadFileLike } from '@mtcute/core' + +export async function normalizeFile(file: UploadFileLike) { + if (typeof file === 'string') { + file = Bun.file(file) + } + + // while these are not Bun-specific, they still may happen + if (file instanceof ReadStream) { + const fileName = basename(file.path.toString()) + const fileSize = await stat(file.path.toString()).then((stat) => stat.size) + + return { + file: NodeReadable.toWeb(file) as unknown as ReadableStream, + fileName, + fileSize, + } + } + + if (file instanceof NodeReadable) { + return { + file: NodeReadable.toWeb(file) as unknown as ReadableStream, + } + } + + // string -> ReadStream, thus already handled + return null +} diff --git a/packages/bun/src/utils/tcp.ts b/packages/bun/src/utils/tcp.ts new file mode 100644 index 00000000..dc0bd618 --- /dev/null +++ b/packages/bun/src/utils/tcp.ts @@ -0,0 +1,126 @@ +import { Socket } from 'bun' +import EventEmitter from 'events' + +import { IntermediatePacketCodec, IPacketCodec, ITelegramTransport, MtcuteError, TransportState } from '@mtcute/core' +import { BasicDcOption, ICryptoProvider, Logger } from '@mtcute/core/utils.js' + +/** + * Base for TCP transports. + * Subclasses must provide packet codec in `_packetCodec` property + */ +export abstract class BaseTcpTransport extends EventEmitter implements ITelegramTransport { + protected _currentDc: BasicDcOption | null = null + protected _state: TransportState = TransportState.Idle + protected _socket: Socket | null = null + + abstract _packetCodec: IPacketCodec + protected _crypto!: ICryptoProvider + protected log!: Logger + + packetCodecInitialized = false + + private _updateLogPrefix() { + if (this._currentDc) { + this.log.prefix = `[TCP:${this._currentDc.ipAddress}:${this._currentDc.port}] ` + } else { + this.log.prefix = '[TCP:disconnected] ' + } + } + + setup(crypto: ICryptoProvider, log: Logger): void { + this._crypto = crypto + this.log = log.create('tcp') + this._updateLogPrefix() + } + + state(): TransportState { + return this._state + } + + currentDc(): BasicDcOption | null { + return this._currentDc + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + connect(dc: BasicDcOption, testMode: boolean): void { + if (this._state !== TransportState.Idle) { + throw new MtcuteError('Transport is not IDLE') + } + + if (!this.packetCodecInitialized) { + this._packetCodec.setup?.(this._crypto, this.log) + this._packetCodec.on('error', (err) => this.emit('error', err)) + this._packetCodec.on('packet', (buf) => this.emit('message', buf)) + this.packetCodecInitialized = true + } + + this._state = TransportState.Connecting + this._currentDc = dc + this._updateLogPrefix() + + this.log.debug('connecting to %j', dc) + + Bun.connect({ + hostname: dc.ipAddress, + port: dc.port, + socket: { + open: this.handleConnect.bind(this), + error: this.handleError.bind(this), + data: (socket, data) => this._packetCodec.feed(data), + close: this.close.bind(this), + }, + }).catch((err) => { + this.handleError(null, err as Error) + this.close() + }) + } + + close(): void { + if (this._state === TransportState.Idle) return + this.log.info('connection closed') + + this.emit('close') + this._state = TransportState.Idle + this._socket?.end() + this._socket = null + this._currentDc = null + this._packetCodec.reset() + } + + handleError(socket: unknown, error: Error): void { + this.log.error('error: %s', error.stack) + this.emit('error', error) + } + + handleConnect(socket: Socket): void { + this._socket = socket + this.log.info('connected') + + Promise.resolve(this._packetCodec.tag()) + .then((initialMessage) => { + if (initialMessage.length) { + this._socket!.write(initialMessage) + this._state = TransportState.Ready + this.emit('ready') + } else { + this._state = TransportState.Ready + this.emit('ready') + } + }) + .catch((err) => this.emit('error', err)) + } + + async send(bytes: Uint8Array): Promise { + const framed = await this._packetCodec.encode(bytes) + + if (this._state !== TransportState.Ready) { + throw new MtcuteError('Transport is not READY') + } + + this._socket!.write(framed) + } +} + +export class TcpTransport extends BaseTcpTransport { + _packetCodec = new IntermediatePacketCodec() +} diff --git a/packages/bun/src/worker.ts b/packages/bun/src/worker.ts new file mode 100644 index 00000000..0c953a52 --- /dev/null +++ b/packages/bun/src/worker.ts @@ -0,0 +1,67 @@ +import { parentPort, Worker } from 'worker_threads' + +import { setPlatform } from '@mtcute/core/platform.js' +import { + ClientMessageHandler, + RespondFn, + SendFn, + SomeWorker, + TelegramWorker as TelegramWorkerBase, + TelegramWorkerOptions, + TelegramWorkerPort as TelegramWorkerPortBase, + TelegramWorkerPortOptions, + WorkerCustomMethods, + WorkerMessageHandler, +} from '@mtcute/core/worker.js' + +import { NodePlatform } from './common-internals-node/platform.js' + +export type { TelegramWorkerOptions, TelegramWorkerPortOptions, WorkerCustomMethods } + +let _registered = false + +export class TelegramWorker extends TelegramWorkerBase { + registerWorker(handler: WorkerMessageHandler): RespondFn { + if (!parentPort) { + throw new Error('TelegramWorker must be created from a worker thread') + } + if (_registered) { + throw new Error('TelegramWorker must be created only once') + } + + _registered = true + + const port = parentPort + + const respond: RespondFn = port.postMessage.bind(port) + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + parentPort.on('message', (message) => handler(message, respond)) + + return respond + } +} + +export class TelegramWorkerPort extends TelegramWorkerPortBase { + constructor(readonly options: TelegramWorkerPortOptions) { + setPlatform(new NodePlatform()) + super(options) + } + + connectToWorker(worker: SomeWorker, handler: ClientMessageHandler): [SendFn, () => void] { + if (!(worker instanceof Worker)) { + throw new Error('Only worker_threads are supported') + } + + const send: SendFn = worker.postMessage.bind(worker) + + worker.on('message', handler) + + return [ + send, + () => { + worker.off('message', handler) + }, + ] + } +} diff --git a/packages/bun/tsconfig.json b/packages/bun/tsconfig.json new file mode 100644 index 00000000..9d487df1 --- /dev/null +++ b/packages/bun/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": [ + "bun-types", + "vite/client" + ] + }, + "include": [ + "./src", + ], + "references": [ + { "path": "../core" }, + { "path": "../dispatcher" }, + { "path": "../html-parser" }, + { "path": "../markdown-parser" } + ] +} diff --git a/packages/bun/typedoc.cjs b/packages/bun/typedoc.cjs new file mode 100644 index 00000000..e1f51e65 --- /dev/null +++ b/packages/bun/typedoc.cjs @@ -0,0 +1,10 @@ +module.exports = { + extends: ['../../.config/typedoc/config.base.cjs'], + entryPoints: ['./src/index.ts'], + externalPattern: [ + '../core/**', + '../html-parser/**', + '../markdown-parser/**', + '../sqlite/**', + ], +} diff --git a/packages/core/src/highlevel/methods/files/upload-file.ts b/packages/core/src/highlevel/methods/files/upload-file.ts index 8800f7c7..958076c8 100644 --- a/packages/core/src/highlevel/methods/files/upload-file.ts +++ b/packages/core/src/highlevel/methods/files/upload-file.ts @@ -25,6 +25,8 @@ const MAX_PART_COUNT_PREMIUM = 8000 // 512 kb * 8000 = 4000 MiB // platform-specific const HAS_FILE = typeof File !== 'undefined' const HAS_RESPONSE = typeof Response !== 'undefined' +const HAS_URL = typeof URL !== 'undefined' +const HAS_BLOB = typeof Blob !== 'undefined' // @available=both /** @@ -129,6 +131,15 @@ export async function uploadFile( file = file.stream() } + if (HAS_URL && file instanceof URL) { + file = await fetch(file) + } + + if (HAS_BLOB && file instanceof Blob) { + fileSize = file.size + file = file.stream() + } + if (HAS_RESPONSE && file instanceof Response) { const length = parseInt(file.headers.get('content-length') || '0') if (!isNaN(length) && length) fileSize = length diff --git a/packages/core/src/highlevel/types/files/utils.ts b/packages/core/src/highlevel/types/files/utils.ts index 44042905..0859ffeb 100644 --- a/packages/core/src/highlevel/types/files/utils.ts +++ b/packages/core/src/highlevel/types/files/utils.ts @@ -11,16 +11,20 @@ import { UploadedFile } from './uploaded-file.js' * Describes types that can be used in {@link TelegramClient.uploadFile} * method. Can be one of: * - `Uint8Array`/`Buffer`, which will be interpreted as raw file contents - * - `File` (from the Web API) + * - `File`, `Blob` (from the Web API) * - `string`, which will be interpreted as file path (**non-browser only!**) - * - `ReadStream` (for NodeJS, from the `fs` module) + * - `URL` (from the Web API, will be `fetch()`-ed; `file://` URLs are not available in browsers) + * - `ReadStream` (for Node.js/Bun, from the `fs` module) + * - `BunFile` (from `Bun.file()`) * - `ReadableStream` (Web API readable stream) - * - `Readable` (NodeJS readable stream) + * - `Readable` (Node.js/Bun readable stream) * - `Response` (from `window.fetch`) */ export type UploadFileLike = + | URL | Uint8Array | File + | Blob | string | ReadStream | ReadableStream @@ -34,14 +38,15 @@ export type UploadFileLike = * Can be one of: * - `Buffer`, which will be interpreted as raw file contents * - `File` (from the Web API) - * - `ReadStream` (for NodeJS, from the `fs` module) + * - `ReadStream` (for Node.js/Bun, from the `fs` module) * - `ReadableStream` (from the Web API, base readable stream) - * - `Readable` (for NodeJS, base readable stream) + * - `Readable` (for Node.js/Bun, base readable stream) * - {@link UploadedFile} returned from {@link TelegramClient.uploadFile} * - `tl.TypeInputFile` and `tl.TypeInputMedia` TL objects * - `string` with a path to a local file prepended with `file:` (non-browser only) (e.g. `file:image.jpg`) * - `string` with a URL to remote files (e.g. `https://example.com/image.jpg`) * - `string` with TDLib and Bot API compatible File ID. + * - `URL` (from the Web API, will be `fetch()`-ed if needed; `file://` URLs are not available in browsers) * - `td.RawFullRemoteFileLocation` (parsed File ID) */ export type InputFileLike = diff --git a/packages/core/src/storage/index.ts b/packages/core/src/storage/index.ts index 6f27696c..7b943ffc 100644 --- a/packages/core/src/storage/index.ts +++ b/packages/core/src/storage/index.ts @@ -2,4 +2,5 @@ export * from './driver.js' export * from './memory/index.js' export * from './provider.js' export * from './repository/index.js' +export * from './sqlite/index.js' export * from './storage.js' diff --git a/packages/sqlite/src/driver.ts b/packages/core/src/storage/sqlite/driver.ts similarity index 71% rename from packages/sqlite/src/driver.ts rename to packages/core/src/storage/sqlite/driver.ts index 42ddeb83..b6478fc8 100644 --- a/packages/sqlite/src/driver.ts +++ b/packages/core/src/storage/sqlite/driver.ts @@ -1,26 +1,6 @@ -import sqlite3, { Database, Options, Statement } from 'better-sqlite3' - -import { BaseStorageDriver } from '@mtcute/core' -import { getPlatform } from '@mtcute/core/platform.js' - -export interface SqliteStorageDriverOptions { - /** - * By default, WAL mode is enabled, which - * significantly improves performance. - * [Learn more](https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/performance.md) - * - * However, you might encounter some issues, - * and if you do, you can disable WAL by passing `true` - * - * @default false - */ - disableWal?: boolean - - /** - * Additional options to pass to `better-sqlite3` - */ - options?: Options -} +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 = ` @@ -30,26 +10,19 @@ create table if not exists ${MIGRATIONS_TABLE_NAME} ( ); `.trim() -type MigrationFunction = (db: Database) => void +type MigrationFunction = (db: ISqliteDatabase) => void -export class SqliteStorageDriver extends BaseStorageDriver { - db!: Database +export abstract class BaseSqliteStorageDriver extends BaseStorageDriver { + db!: ISqliteDatabase - constructor( - readonly filename = ':memory:', - readonly params?: SqliteStorageDriverOptions, - ) { - super() - } - - private _pending: [Statement, unknown[]][] = [] - private _runMany!: (stmts: [Statement, unknown[]][]) => void + private _pending: [ISqliteStatement, unknown[]][] = [] + private _runMany!: (stmts: [ISqliteStatement, unknown[]][]) => void private _cleanup?: () => void private _migrations: Map> = new Map() private _maxVersion: Map = new Map() - // todo: remove in 1.0.0 + remove direct dep on @mtcute/tl + // todo: remove in 1.0.0 private _legacyMigrations: Map = new Map() registerLegacyMigration(repo: string, migration: MigrationFunction): void { @@ -85,9 +58,9 @@ export class SqliteStorageDriver extends BaseStorageDriver { } } - private _onLoad = new Set<(db: Database) => void>() + private _onLoad = new Set<(db: ISqliteDatabase) => void>() - onLoad(cb: (db: Database) => void): void { + onLoad(cb: (db: ISqliteDatabase) => void): void { if (this.loaded) { cb(this.db) } else { @@ -95,7 +68,7 @@ export class SqliteStorageDriver extends BaseStorageDriver { } } - _writeLater(stmt: Statement, params: unknown[]): void { + _writeLater(stmt: ISqliteStatement, params: unknown[]): void { this._pending.push([stmt, params]) } @@ -149,17 +122,12 @@ export class SqliteStorageDriver extends BaseStorageDriver { } } + abstract _createDatabase(): ISqliteDatabase + _load(): void { - this.db = sqlite3(this.filename, { - ...this.params?.options, - verbose: this._log.mgr.level >= 5 ? (this._log.verbose as Options['verbose']) : undefined, - }) + this.db = this._createDatabase() - if (!this.params?.disableWal) { - this.db.pragma('journal_mode = WAL') - } - - this._runMany = this.db.transaction((stmts: [Statement, unknown[]][]) => { + this._runMany = this.db.transaction((stmts: [ISqliteStatement, unknown[]][]) => { stmts.forEach((stmt) => { stmt[0].run(stmt[1]) }) diff --git a/packages/core/src/storage/sqlite/index.ts b/packages/core/src/storage/sqlite/index.ts new file mode 100644 index 00000000..37218f7d --- /dev/null +++ b/packages/core/src/storage/sqlite/index.ts @@ -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) +} diff --git a/packages/sqlite/src/repository/auth-keys.ts b/packages/core/src/storage/sqlite/repository/auth-keys.ts similarity index 82% rename from packages/sqlite/src/repository/auth-keys.ts rename to packages/core/src/storage/sqlite/repository/auth-keys.ts index 06b4e2fd..147f8898 100644 --- a/packages/sqlite/src/repository/auth-keys.ts +++ b/packages/core/src/storage/sqlite/repository/auth-keys.ts @@ -1,8 +1,6 @@ -import { Statement } from 'better-sqlite3' - -import { IAuthKeysRepository } from '@mtcute/core' - -import { SqliteStorageDriver } from '../driver.js' +import { IAuthKeysRepository } from '../../repository/auth-keys.js' +import { BaseSqliteStorageDriver } from '../driver.js' +import { ISqliteStatement } from '../types.js' interface AuthKeyDto { dc: number @@ -15,7 +13,7 @@ interface TempAuthKeyDto extends AuthKeyDto { } export class SqliteAuthKeysRepository implements IAuthKeysRepository { - constructor(readonly _driver: SqliteStorageDriver) { + constructor(readonly _driver: BaseSqliteStorageDriver) { _driver.registerMigration('auth_keys', 1, (db) => { db.exec(` create table if not exists auth_keys ( @@ -47,8 +45,8 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository { }) } - private _set!: Statement - private _del!: Statement + private _set!: ISqliteStatement + private _del!: ISqliteStatement set(dc: number, key: Uint8Array | null): void { if (!key) { this._del.run(dc) @@ -59,7 +57,7 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository { this._set.run(dc, key) } - private _get!: Statement + private _get!: ISqliteStatement get(dc: number): Uint8Array | null { const row = this._get.get(dc) if (!row) return null @@ -67,8 +65,8 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository { return (row as AuthKeyDto).key } - private _setTemp!: Statement - private _delTemp!: Statement + private _setTemp!: ISqliteStatement + private _delTemp!: ISqliteStatement setTemp(dc: number, idx: number, key: Uint8Array | null, expires: number): void { if (!key) { this._delTemp.run(dc, idx) @@ -79,7 +77,7 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository { this._setTemp.run(dc, idx, key, expires) } - private _getTemp!: Statement + private _getTemp!: ISqliteStatement getTemp(dc: number, idx: number, now: number): Uint8Array | null { const row = this._getTemp.get(dc, idx, now) if (!row) return null @@ -87,13 +85,13 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository { return (row as TempAuthKeyDto).key } - private _delTempAll!: Statement + private _delTempAll!: ISqliteStatement deleteByDc(dc: number): void { this._del.run(dc) this._delTempAll.run(dc) } - private _delAll!: Statement + private _delAll!: ISqliteStatement deleteAll(): void { this._delAll.run() } diff --git a/packages/sqlite/src/repository/kv.ts b/packages/core/src/storage/sqlite/repository/kv.ts similarity index 84% rename from packages/sqlite/src/repository/kv.ts rename to packages/core/src/storage/sqlite/repository/kv.ts index 45632508..713249b1 100644 --- a/packages/sqlite/src/repository/kv.ts +++ b/packages/core/src/storage/sqlite/repository/kv.ts @@ -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 { __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 { key: string @@ -13,7 +15,7 @@ interface KeyValueDto { } export class SqliteKeyValueRepository implements IKeyValueRepository { - constructor(readonly _driver: SqliteStorageDriver) { + constructor(readonly _driver: BaseSqliteStorageDriver) { _driver.registerMigration('kv', 1, (db) => { db.exec(` create table key_value ( @@ -88,12 +90,12 @@ export class SqliteKeyValueRepository implements IKeyValueRepository { /* eslint-enable @typescript-eslint/no-unsafe-argument */ } - private _set!: Statement + private _set!: ISqliteStatement set(key: string, value: Uint8Array): void { this._driver._writeLater(this._set, [key, value]) } - private _get!: Statement + private _get!: ISqliteStatement get(key: string): Uint8Array | null { const res = this._get.get(key) if (!res) return null @@ -101,12 +103,12 @@ export class SqliteKeyValueRepository implements IKeyValueRepository { return (res as KeyValueDto).value } - private _del!: Statement + private _del!: ISqliteStatement delete(key: string): void { this._del.run(key) } - private _delAll!: Statement + private _delAll!: ISqliteStatement deleteAll(): void { this._delAll.run() } diff --git a/packages/sqlite/src/repository/peers.ts b/packages/core/src/storage/sqlite/repository/peers.ts similarity index 86% rename from packages/sqlite/src/repository/peers.ts rename to packages/core/src/storage/sqlite/repository/peers.ts index 54a527d4..4f5a78a1 100644 --- a/packages/sqlite/src/repository/peers.ts +++ b/packages/core/src/storage/sqlite/repository/peers.ts @@ -1,8 +1,6 @@ -import { Statement } from 'better-sqlite3' - -import { IPeersRepository } from '@mtcute/core' - -import { SqliteStorageDriver } from '../driver.js' +import { IPeersRepository } from '../../../highlevel/storage/repository/peers.js' +import { BaseSqliteStorageDriver } from '../driver.js' +import { ISqliteStatement } from '../types.js' interface PeerDto { id: number @@ -26,7 +24,7 @@ function mapPeerDto(dto: PeerDto): IPeersRepository.PeerInfo { } export class SqlitePeersRepository implements IPeersRepository { - constructor(readonly _driver: SqliteStorageDriver) { + constructor(readonly _driver: BaseSqliteStorageDriver) { _driver.registerMigration('peers', 1, (db) => { db.exec(` create table peers ( @@ -60,7 +58,7 @@ export class SqlitePeersRepository implements IPeersRepository { }) } - private _store!: Statement + private _store!: ISqliteStatement store(peer: IPeersRepository.PeerInfo): void { this._driver._writeLater(this._store, [ peer.id, @@ -73,7 +71,7 @@ export class SqlitePeersRepository implements IPeersRepository { ]) } - private _getById!: Statement + private _getById!: ISqliteStatement getById(id: number): IPeersRepository.PeerInfo | null { const row = this._getById.get(id) if (!row) return null @@ -81,7 +79,7 @@ export class SqlitePeersRepository implements IPeersRepository { return mapPeerDto(row as PeerDto) } - private _getByUsername!: Statement + private _getByUsername!: ISqliteStatement getByUsername(username: string): IPeersRepository.PeerInfo | null { const row = this._getByUsername.get(username) if (!row) return null @@ -89,7 +87,7 @@ export class SqlitePeersRepository implements IPeersRepository { return mapPeerDto(row as PeerDto) } - private _getByPhone!: Statement + private _getByPhone!: ISqliteStatement getByPhone(phone: string): IPeersRepository.PeerInfo | null { const row = this._getByPhone.get(phone) if (!row) return null @@ -97,7 +95,7 @@ export class SqlitePeersRepository implements IPeersRepository { return mapPeerDto(row as PeerDto) } - private _delAll!: Statement + private _delAll!: ISqliteStatement deleteAll(): void { this._delAll.run() } diff --git a/packages/sqlite/src/repository/ref-messages.ts b/packages/core/src/storage/sqlite/repository/ref-messages.ts similarity index 84% rename from packages/sqlite/src/repository/ref-messages.ts rename to packages/core/src/storage/sqlite/repository/ref-messages.ts index af4332af..ff42516c 100644 --- a/packages/sqlite/src/repository/ref-messages.ts +++ b/packages/core/src/storage/sqlite/repository/ref-messages.ts @@ -1,8 +1,7 @@ -import { Statement } from 'better-sqlite3' - import { IReferenceMessagesRepository } from '@mtcute/core' -import { SqliteStorageDriver } from '../driver.js' +import { BaseSqliteStorageDriver } from '../driver.js' +import { ISqliteStatement } from '../types.js' interface ReferenceMessageDto { peer_id: number @@ -11,7 +10,7 @@ interface ReferenceMessageDto { } export class SqliteRefMessagesRepository implements IReferenceMessagesRepository { - constructor(readonly _driver: SqliteStorageDriver) { + constructor(readonly _driver: BaseSqliteStorageDriver) { _driver.registerMigration('ref_messages', 1, (db) => { db.exec(` 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 { this._store.run(peerId, chatId, msgId) } - private _getByPeer!: Statement + private _getByPeer!: ISqliteStatement getByPeer(peerId: number): [number, number] | null { const res = this._getByPeer.get(peerId) if (!res) return null @@ -51,19 +50,19 @@ export class SqliteRefMessagesRepository implements IReferenceMessagesRepository return [res_.chat_id, res_.msg_id] } - private _del!: Statement + private _del!: ISqliteStatement delete(chatId: number, msgIds: number[]): void { for (const msgId of msgIds) { this._driver._writeLater(this._del, [chatId, msgId]) } } - private _delByPeer!: Statement + private _delByPeer!: ISqliteStatement deleteByPeer(peerId: number): void { this._delByPeer.run(peerId) } - private _delAll!: Statement + private _delAll!: ISqliteStatement deleteAll(): void { this._delAll.run() } diff --git a/packages/core/src/storage/sqlite/types.ts b/packages/core/src/storage/sqlite/types.ts new file mode 100644 index 00000000..6527835d --- /dev/null +++ b/packages/core/src/storage/sqlite/types.ts @@ -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 any>(fn: F): F + + prepare(sql: string): ISqliteStatement + + exec(sql: string): void + close(): void +} + +export interface ISqliteStatement { + run(...params: BindParameters): void + get(...params: BindParameters): unknown + all(...params: BindParameters): unknown[] +} diff --git a/packages/create-bot/README.md b/packages/create-bot/README.md index 719436f8..4c7e3440 100644 --- a/packages/create-bot/README.md +++ b/packages/create-bot/README.md @@ -1,6 +1,6 @@ # @mtcute/create-bot -Starter kit for creating bots using `@mtcute/node`. +Starter kit for creating bots using `@mtcute/node` or `@mtcute/bun`. [Learn more](https://mtcute.dev/guide/) @@ -12,7 +12,15 @@ Starter kit for creating bots using `@mtcute/node`. ## Usage +Depending on your preferred package manager, run one of the following commands: ```bash pnpm create @mtcute/bot -# and follow the instructions +# or +yarn create @mtcute/bot +# or +npm create @mtcute/bot +# or +bun create @mtcute/bot ``` + +and follow the instructions diff --git a/packages/create-bot/src/cli.ts b/packages/create-bot/src/cli.ts index 7502bf0f..b84cdb56 100644 --- a/packages/create-bot/src/cli.ts +++ b/packages/create-bot/src/cli.ts @@ -6,7 +6,7 @@ import { readConfig, UserConfigPersisted, writeConfig } from './config.js' import { TELEGRAM_APPS_PAGE } from './constants.js' import { getFeatureChoices } from './features/cli.js' import { MtcuteFeature } from './features/types.js' -import { getPackageManager, PackageManager } from './package-manager.js' +import { PackageManager } from './package-manager.js' interface UserConfigAnswers { reuse?: boolean @@ -101,7 +101,7 @@ export async function askForConfigPersisted(): Promise { return config } -export async function askForConfig(): Promise { +export async function askForConfig(packageManager: PackageManager): Promise { const persisted = await askForConfigPersisted() let allowEmptyBotToken = false @@ -128,7 +128,7 @@ export async function askForConfig(): Promise { }, { type: 'checkbox', - choices: getFeatureChoices(), + choices: getFeatureChoices(packageManager), name: 'features', message: 'Select features:', }, @@ -137,7 +137,7 @@ export async function askForConfig(): Promise { return { ...persisted, name: '', // will be filled later - packageManager: getPackageManager(), + packageManager, botToken: botToken || undefined, features, } diff --git a/packages/create-bot/src/dependencies.ts b/packages/create-bot/src/dependencies.ts index 37130571..e44e77e9 100644 --- a/packages/create-bot/src/dependencies.ts +++ b/packages/create-bot/src/dependencies.ts @@ -1,12 +1,18 @@ import { UserConfig } from './cli.js' import { MtcuteFeature } from './features/types.js' -import { getInstallCommand } from './package-manager.js' +import { getInstallCommand, PackageManager } from './package-manager.js' import { exec } from './utils.js' export function buildDependenciesList(config: UserConfig) { - const dependencies = ['@mtcute/node'] + const dependencies = [] const devDepdenencies = ['dotenv-cli'] + if (config.packageManager === PackageManager.Bun) { + dependencies.push('@mtcute/bun') + } else { + dependencies.push('@mtcute/node') + } + if (config.features.includes(MtcuteFeature.Dispatcher)) { dependencies.push('@mtcute/dispatcher') } diff --git a/packages/create-bot/src/features/cli.ts b/packages/create-bot/src/features/cli.ts index c1f7a463..f079458c 100644 --- a/packages/create-bot/src/features/cli.ts +++ b/packages/create-bot/src/features/cli.ts @@ -1,15 +1,10 @@ import { CheckboxChoiceOptions } from 'inquirer' +import { PackageManager } from '../package-manager.js' import { MtcuteFeature } from './types.js' -export function getFeatureChoices(): CheckboxChoiceOptions[] { - return [ - { - name: ' 🚀 Native addon (better performance)', - short: 'Native addon', - value: MtcuteFeature.NativeAddon, - checked: true, - }, +export function getFeatureChoices(packageMananger: PackageManager): CheckboxChoiceOptions[] { + const arr: CheckboxChoiceOptions[] = [ { name: ' 🌐 Internationalization', short: 'i18n', @@ -21,12 +16,6 @@ export function getFeatureChoices(): CheckboxChoiceOptions[] { value: MtcuteFeature.Dispatcher, checked: true, }, - { - name: ' 🐳 Generate Dockerfile', - short: 'Dockerfile', - value: MtcuteFeature.Docker, - checked: true, - }, { name: ' ✨ Use TypeScript', short: 'TypeScript', @@ -40,4 +29,25 @@ export function getFeatureChoices(): CheckboxChoiceOptions[] { checked: true, }, ] + + if (packageMananger !== PackageManager.Bun) { + arr.unshift({ + name: ' 🚀 Native addon (better performance)', + short: 'Native addon', + value: MtcuteFeature.NativeAddon, + checked: true, + }) + } + + if (packageMananger === PackageManager.Pnpm) { + // todo: add support for dockerfile generation for other package managers + arr.push({ + name: ' 🐳 Generate Dockerfile', + short: 'Dockerfile', + value: MtcuteFeature.Docker, + checked: true, + }) + } + + return arr } diff --git a/packages/create-bot/src/main.ts b/packages/create-bot/src/main.ts index 88f6174d..4cbaf945 100644 --- a/packages/create-bot/src/main.ts +++ b/packages/create-bot/src/main.ts @@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url' import { askForConfig } from './cli.js' import { installDependencies } from './dependencies.js' import { MtcuteFeature } from './features/types.js' -import { getExecCommand } from './package-manager.js' +import { getExecCommand, getPackageManager, PackageManager } from './package-manager.js' import { runTemplater } from './templater.js' import { exec } from './utils.js' @@ -17,7 +17,13 @@ if (!projectName) { process.exit(1) } -const config = await askForConfig() +const packageManager = getPackageManager() + +if (packageManager === PackageManager.Bun) { + console.log(`${colors.red('‼️ Warning:')} ${colors.yellow('Bun')} support is ${colors.bold('experimental')}`) +} + +const config = await askForConfig(packageManager) config.name = projectName const outDir = process.env.TARGET_DIR || join(process.cwd(), projectName) diff --git a/packages/create-bot/template/package.json.hbs b/packages/create-bot/template/package.json.hbs index df814575..8d4a6902 100644 --- a/packages/create-bot/template/package.json.hbs +++ b/packages/create-bot/template/package.json.hbs @@ -15,11 +15,19 @@ "lint:fix": "eslint --fix .", "format": "prettier --write \"src/**/*.ts\"", {{/if}} + {{#if (eq packageManager "bun")}} + {{#if features.typescript}} + "start": "bun ./src/index.ts" + {{else}} + "start": "bun ./src/index.js" + {{/if}} + {{else}} {{#if features.typescript}} "start": "tsc && dotenv node ./dist/index.js", "build": "tsc" {{else}} "start": "dotenv node ./src/index.js" {{/if}} + {{/if}} } } \ No newline at end of file diff --git a/packages/create-bot/template/src/index.js.hbs b/packages/create-bot/template/src/index.js.hbs index 3079fe89..629c923c 100644 --- a/packages/create-bot/template/src/index.js.hbs +++ b/packages/create-bot/template/src/index.js.hbs @@ -2,7 +2,11 @@ {{#if features.dispatcher}} import { Dispatcher, filters } from '@mtcute/dispatcher' {{/if}} +{{#if (eq packageManager "bun")}} +import { TelegramClient } from '@mtcute/bun' +{{else}} import { TelegramClient } from '@mtcute/node' +{{/if}} import * as env from './env.js' {{#if features.i18n}} diff --git a/packages/create-bot/template/src/index.ts.hbs b/packages/create-bot/template/src/index.ts.hbs index 631653c5..db1cf374 100644 --- a/packages/create-bot/template/src/index.ts.hbs +++ b/packages/create-bot/template/src/index.ts.hbs @@ -2,7 +2,11 @@ {{#if features.dispatcher}} import { Dispatcher, filters } from '@mtcute/dispatcher' {{/if}} +{{#if (eq packageManager "bun")}} +import { TelegramClient } from '@mtcute/bun' +{{else}} import { TelegramClient } from '@mtcute/node' +{{/if}} import * as env from './env.js' {{#if features.i18n}} diff --git a/packages/dispatcher/package.json b/packages/dispatcher/package.json index 2061a995..2cc08dbe 100644 --- a/packages/dispatcher/package.json +++ b/packages/dispatcher/package.json @@ -26,13 +26,5 @@ }, "devDependencies": { "@mtcute/test": "workspace:^" - }, - "peerDependencies": { - "@mtcute/sqlite": "workspace:^" - }, - "peerDependenciesMeta": { - "@mtcute/sqlite": { - "optional": true - } } } diff --git a/packages/dispatcher/src/state/providers/sqlite.ts b/packages/dispatcher/src/state/providers/sqlite.ts index 38ebc5de..5b3423db 100644 --- a/packages/dispatcher/src/state/providers/sqlite.ts +++ b/packages/dispatcher/src/state/providers/sqlite.ts @@ -1,5 +1,4 @@ -import { MaybePromise } from '@mtcute/core' -import type { SqliteStorage, SqliteStorageDriver, Statement } from '@mtcute/sqlite' +import { BaseSqliteStorage, BaseSqliteStorageDriver, ISqliteStatement, MaybePromise } from '@mtcute/core' import { IStateStorageProvider } from '../provider.js' import { IStateRepository } from '../repository.js' @@ -15,7 +14,7 @@ interface RateLimitDto { } class SqliteStateRepository implements IStateRepository { - constructor(readonly _driver: SqliteStorageDriver) { + constructor(readonly _driver: BaseSqliteStorageDriver) { _driver.registerMigration('state', 1, (db) => { db.exec(` 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 { this._setState.run(key, state, ttl ? Date.now() + ttl * 1000 : undefined) } - private _getState!: Statement + private _getState!: ISqliteStatement getState(key: string, now: number): MaybePromise { const res_ = this._getState.get(key) if (!res_) return null @@ -69,21 +68,21 @@ class SqliteStateRepository implements IStateRepository { return res.value } - private _deleteState!: Statement + private _deleteState!: ISqliteStatement deleteState(key: string): MaybePromise { this._deleteState.run(key) } - private _deleteOldState!: Statement - private _deleteOldRl!: Statement + private _deleteOldState!: ISqliteStatement + private _deleteOldRl!: ISqliteStatement vacuum(now: number): MaybePromise { this._deleteOldState.run(now) this._deleteOldRl.run(now) } - private _setRl!: Statement - private _getRl!: Statement - private _deleteRl!: Statement + private _setRl!: ISqliteStatement + private _getRl!: ISqliteStatement + private _deleteRl!: ISqliteStatement getRateLimit(key: string, now: number, limit: number, window: number): [number, number] { const val = this._getRl.get(key) as RateLimitDto | undefined @@ -117,9 +116,9 @@ class SqliteStateRepository implements IStateRepository { } 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) } diff --git a/packages/node/package.json b/packages/node/package.json index e88573dc..1724561f 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -2,7 +2,7 @@ "name": "@mtcute/node", "private": true, "version": "0.8.0", - "description": "Meta-package for Node JS", + "description": "Meta-package for Node.js", "author": "alina sireneva ", "license": "MIT", "main": "src/index.ts", @@ -31,11 +31,12 @@ "dependencies": { "@mtcute/core": "workspace:^", "@mtcute/wasm": "workspace:^", - "@mtcute/sqlite": "workspace:^", "@mtcute/markdown-parser": "workspace:^", - "@mtcute/html-parser": "workspace:^" + "@mtcute/html-parser": "workspace:^", + "better-sqlite3": "9.2.2" }, "devDependencies": { - "@mtcute/test": "workspace:^" + "@mtcute/test": "workspace:^", + "@types/better-sqlite3": "7.6.4" } } diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index fa337990..34ac6caf 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -9,11 +9,11 @@ import { TelegramClientOptions, } from '@mtcute/core/client.js' import { setPlatform } from '@mtcute/core/platform.js' -import { SqliteStorage } from '@mtcute/sqlite' +import { NodePlatform } from './common-internals-node/platform.js' import { downloadToFile } from './methods/download-file.js' import { downloadAsNodeStream } from './methods/download-node-stream.js' -import { NodePlatform } from './platform.js' +import { SqliteStorage } from './sqlite/index.js' import { NodeCryptoProvider } from './utils/crypto.js' import { TcpTransport } from './utils/tcp.js' diff --git a/packages/node/src/utils/exit-hook.ts b/packages/node/src/common-internals-node/exit-hook.ts similarity index 100% rename from packages/node/src/utils/exit-hook.ts rename to packages/node/src/common-internals-node/exit-hook.ts diff --git a/packages/node/src/utils/logging.ts b/packages/node/src/common-internals-node/logging.ts similarity index 100% rename from packages/node/src/utils/logging.ts rename to packages/node/src/common-internals-node/logging.ts diff --git a/packages/node/src/platform.ts b/packages/node/src/common-internals-node/platform.ts similarity index 87% rename from packages/node/src/platform.ts rename to packages/node/src/common-internals-node/platform.ts index 60dd7307..49f7ca60 100644 --- a/packages/node/src/platform.ts +++ b/packages/node/src/common-internals-node/platform.ts @@ -2,17 +2,13 @@ import * as os from 'os' import { ICorePlatform } from '@mtcute/core/platform.js' -import { beforeExit } from './utils/exit-hook.js' -import { defaultLoggingHandler } from './utils/logging.js' -import { normalizeFile } from './utils/normalize-file.js' +import { normalizeFile } from '../utils/normalize-file.js' +import { beforeExit } from './exit-hook.js' +import { defaultLoggingHandler } from './logging.js' const BUFFER_BASE64_URL_AVAILABLE = typeof Buffer.isEncoding === 'function' && Buffer.isEncoding('base64url') -const toBuffer = (buf: Uint8Array): Buffer => Buffer.from( - buf.buffer, - buf.byteOffset, - buf.byteLength, -) +const toBuffer = (buf: Uint8Array): Buffer => Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength) export class NodePlatform implements ICorePlatform { // ICorePlatform diff --git a/packages/node/src/common-internals-node/readme.md b/packages/node/src/common-internals-node/readme.md new file mode 100644 index 00000000..db7a5572 --- /dev/null +++ b/packages/node/src/common-internals-node/readme.md @@ -0,0 +1,2 @@ +this folder is for common code across `@mtcute/node` and `@mtcute/bun`. +it is symlinked into `@mtcute/bun` \ No newline at end of file diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index cd8c5fc4..d354f7eb 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -1,9 +1,9 @@ export * from './client.js' -export * from './platform.js' +export * from './common-internals-node/platform.js' +export * from './sqlite/index.js' export * from './utils/crypto.js' export * from './utils/tcp.js' export * from './worker.js' export * from '@mtcute/core' export * from '@mtcute/html-parser' export * from '@mtcute/markdown-parser' -export * from '@mtcute/sqlite' diff --git a/packages/node/src/methods.ts b/packages/node/src/methods.ts index dd3f229e..cef20775 100644 --- a/packages/node/src/methods.ts +++ b/packages/node/src/methods.ts @@ -1,5 +1,3 @@ -/* eslint-disable import/export, simple-import-sort/exports */ -export * from '@mtcute/core/methods.js' - export { downloadToFile } from './methods/download-file.js' export { downloadAsNodeStream } from './methods/download-node-stream.js' +export * from '@mtcute/core/methods.js' diff --git a/packages/node/src/sqlite/driver.ts b/packages/node/src/sqlite/driver.ts new file mode 100644 index 00000000..fcfc4cb7 --- /dev/null +++ b/packages/node/src/sqlite/driver.ts @@ -0,0 +1,44 @@ +import sqlite3, { Options } from 'better-sqlite3' + +import { BaseSqliteStorageDriver, ISqliteDatabase } from '@mtcute/core' + +export interface SqliteStorageDriverOptions { + /** + * By default, WAL mode is enabled, which + * significantly improves performance. + * [Learn more](https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/performance.md) + * + * However, you might encounter some issues, + * and if you do, you can disable WAL by passing `true` + * + * @default false + */ + disableWal?: boolean + + /** + * Additional options to pass to `better-sqlite3` + */ + options?: Options +} + +export class SqliteStorageDriver extends BaseSqliteStorageDriver { + constructor( + readonly filename = ':memory:', + readonly params?: SqliteStorageDriverOptions, + ) { + super() + } + + _createDatabase(): ISqliteDatabase { + const db = sqlite3(this.filename, { + ...this.params?.options, + verbose: this._log.mgr.level >= 5 ? (this._log.verbose as Options['verbose']) : undefined, + }) + + if (!this.params?.disableWal) { + db.pragma('journal_mode = WAL') + } + + return db as ISqliteDatabase + } +} diff --git a/packages/node/src/sqlite/index.ts b/packages/node/src/sqlite/index.ts new file mode 100644 index 00000000..882b228d --- /dev/null +++ b/packages/node/src/sqlite/index.ts @@ -0,0 +1,14 @@ +import { BaseSqliteStorage } from '@mtcute/core' + +import { SqliteStorageDriver, SqliteStorageDriverOptions } from './driver.js' + +export { SqliteStorageDriver } from './driver.js' + +export class SqliteStorage extends BaseSqliteStorage { + constructor( + readonly filename = ':memory:', + readonly params?: SqliteStorageDriverOptions, + ) { + super(new SqliteStorageDriver(filename, params)) + } +} diff --git a/packages/sqlite/test/sqlite.test.ts b/packages/node/src/sqlite/sqlite.test.ts similarity index 93% rename from packages/sqlite/test/sqlite.test.ts rename to packages/node/src/sqlite/sqlite.test.ts index 9b3e93ce..fe992683 100644 --- a/packages/sqlite/test/sqlite.test.ts +++ b/packages/node/src/sqlite/sqlite.test.ts @@ -9,7 +9,7 @@ import { } from '@mtcute/test' if (import.meta.env.TEST_ENV === 'node') { - const { SqliteStorage } = await import('../src/index.js') + const { SqliteStorage } = await import('./index.js') describe('SqliteStorage', () => { const storage = new SqliteStorage(':memory:') diff --git a/packages/node/src/utils/crypto.test.ts b/packages/node/src/utils/crypto.test.ts index 41e32872..ff24a70c 100644 --- a/packages/node/src/utils/crypto.test.ts +++ b/packages/node/src/utils/crypto.test.ts @@ -2,7 +2,7 @@ import { describe } from 'vitest' import { testCryptoProvider } from '@mtcute/test' -if (import.meta.env.TEST_ENV === 'node' || import.meta.env.TEST_ENV === 'bun') { +if (import.meta.env.TEST_ENV === 'node') { describe('NodeCryptoProvider', async () => { const { NodeCryptoProvider } = await import('./crypto.js') diff --git a/packages/node/src/utils/normalize-file.ts b/packages/node/src/utils/normalize-file.ts index 5414ae21..6259615c 100644 --- a/packages/node/src/utils/normalize-file.ts +++ b/packages/node/src/utils/normalize-file.ts @@ -5,7 +5,7 @@ import { Readable } from 'stream' import { UploadFileLike } from '@mtcute/core' -import { nodeStreamToWeb } from '../utils/stream-utils.js' +import { nodeStreamToWeb } from './stream-utils.js' export async function normalizeFile(file: UploadFileLike) { if (typeof file === 'string') { diff --git a/packages/node/src/utils/stream-utils.ts b/packages/node/src/utils/stream-utils.ts index 20ac1b8b..532dddd3 100644 --- a/packages/node/src/utils/stream-utils.ts +++ b/packages/node/src/utils/stream-utils.ts @@ -4,7 +4,7 @@ import { isNodeVersionAfter } from './version.js' export function nodeStreamToWeb(stream: Readable): ReadableStream { if (typeof Readable.toWeb === 'function') { - return Readable.toWeb(stream) + return Readable.toWeb(stream) as unknown as ReadableStream } // otherwise, use a silly little adapter @@ -57,9 +57,12 @@ export function webStreamToNode(stream: ReadableStream): Readable { }, destroy(error, cb) { if (!ended) { - void reader.cancel(error).catch(() => {}).then(() => { - cb(error) - }) + void reader + .cancel(error) + .catch(() => {}) + .then(() => { + cb(error) + }) return } @@ -68,11 +71,13 @@ export function webStreamToNode(stream: ReadableStream): Readable { }, }) - reader.closed.then(() => { - ended = true - }).catch((err) => { - readable.destroy(err as Error) - }) + reader.closed + .then(() => { + ended = true + }) + .catch((err) => { + readable.destroy(err as Error) + }) return readable } diff --git a/packages/node/src/utils/version.ts b/packages/node/src/utils/version.ts index 1e987b06..7911e03d 100644 --- a/packages/node/src/utils/version.ts +++ b/packages/node/src/utils/version.ts @@ -1,5 +1,5 @@ export const NODE_VERSION = typeof process !== 'undefined' && 'node' in process.versions ? process.versions.node : null -export const NODE_VERSION_TUPLE = NODE_VERSION ? NODE_VERSION.split('.').map(Number) : null +export const NODE_VERSION_TUPLE = NODE_VERSION ? /*#__PURE__*/ NODE_VERSION.split('.').map(Number) : null export function isNodeVersionAfter(major: number, minor: number, patch: number): boolean { if (!NODE_VERSION_TUPLE) return true // assume non-node environment is always "after" diff --git a/packages/node/src/worker.ts b/packages/node/src/worker.ts index 443a5712..0c953a52 100644 --- a/packages/node/src/worker.ts +++ b/packages/node/src/worker.ts @@ -14,7 +14,7 @@ import { WorkerMessageHandler, } from '@mtcute/core/worker.js' -import { NodePlatform } from './platform.js' +import { NodePlatform } from './common-internals-node/platform.js' export type { TelegramWorkerOptions, TelegramWorkerPortOptions, WorkerCustomMethods } diff --git a/packages/node/tsconfig.json b/packages/node/tsconfig.json index b9a14bd0..c8886a87 100644 --- a/packages/node/tsconfig.json +++ b/packages/node/tsconfig.json @@ -9,7 +9,6 @@ ], "references": [ { "path": "../core" }, - { "path": "../sqlite" }, { "path": "../dispatcher" }, { "path": "../html-parser" }, { "path": "../markdown-parser" } diff --git a/packages/sqlite/README.md b/packages/sqlite/README.md deleted file mode 100644 index abf522cb..00000000 --- a/packages/sqlite/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# @mtcute/sqlite - -📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_sqlite.html) - -SQLite backed storage for mtcute, built with `better-sqlite3` - -## Usage - -```typescript -import { SqliteStorage } from '@mtcute/sqlite' - -const tg = new TelegramClient({ - // ... - storage: new SqliteStorage('client.session') -}) -``` diff --git a/packages/sqlite/package.json b/packages/sqlite/package.json deleted file mode 100644 index 8604b793..00000000 --- a/packages/sqlite/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@mtcute/sqlite", - "private": true, - "version": "0.8.0", - "description": "SQLite-based storage for mtcute", - "author": "alina sireneva ", - "license": "MIT", - "main": "src/index.ts", - "type": "module", - "sideEffects": false, - "distOnlyFields": { - "exports": { - ".": { - "import": "./esm/index.js", - "require": "./cjs/index.js" - } - } - }, - "scripts": { - "docs": "typedoc", - "build": "pnpm run -w build-package sqlite" - }, - "dependencies": { - "@mtcute/core": "workspace:^", - "@mtcute/tl": "*", - "@mtcute/tl-runtime": "workspace:^", - "better-sqlite3": "9.2.2" - }, - "devDependencies": { - "@mtcute/test": "workspace:^", - "@types/better-sqlite3": "7.6.4" - } -} diff --git a/packages/sqlite/src/index.ts b/packages/sqlite/src/index.ts deleted file mode 100644 index 2fb42852..00000000 --- a/packages/sqlite/src/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IMtStorageProvider, ITelegramStorageProvider } from '@mtcute/core' - -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 type { Statement } from 'better-sqlite3' - -export class SqliteStorage implements IMtStorageProvider, ITelegramStorageProvider { - constructor( - readonly filename = ':memory:', - readonly params?: SqliteStorageDriverOptions, - ) {} - - 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) -} diff --git a/packages/sqlite/test/tsconfig.json b/packages/sqlite/test/tsconfig.json deleted file mode 100644 index 23b6b033..00000000 --- a/packages/sqlite/test/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "include": [ - "." - ], - "references": [ - { "path": "../" } - ] -} diff --git a/packages/sqlite/tsconfig.json b/packages/sqlite/tsconfig.json deleted file mode 100644 index 9ae9f2ef..00000000 --- a/packages/sqlite/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist/esm", - "rootDir": "./src" - }, - "include": [ - "./src" - ], - "references": [ - { "path": "../core" } - ] -} diff --git a/packages/sqlite/typedoc.cjs b/packages/sqlite/typedoc.cjs deleted file mode 100644 index c062faa9..00000000 --- a/packages/sqlite/typedoc.cjs +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - extends: ['../../.config/typedoc/config.base.cjs'], - entryPoints: ['./src/index.ts'], -} diff --git a/packages/tl-runtime/src/reader.ts b/packages/tl-runtime/src/reader.ts index 3189a237..f0df3cb7 100644 --- a/packages/tl-runtime/src/reader.ts +++ b/packages/tl-runtime/src/reader.ts @@ -41,7 +41,7 @@ export class TlBinaryReader { */ constructor( readonly objectsMap: TlReaderMap | undefined, - data: ArrayBuffer, + data: ArrayBuffer | ArrayBufferView, start = 0, ) { if (ArrayBuffer.isView(data)) { @@ -61,7 +61,7 @@ export class TlBinaryReader { * @param data Buffer to read from * @param start Position to start reading from */ - static manual(data: ArrayBuffer, start = 0): TlBinaryReader { + static manual(data: ArrayBuffer | ArrayBufferView, start = 0): TlBinaryReader { return new TlBinaryReader(undefined, data, start) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e62e9818..bc03eb68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,6 +115,28 @@ importers: specifier: 1.4.0 version: 1.4.0(@types/node@20.10.0)(@vitest/browser@1.4.0)(@vitest/ui@1.4.0) + packages/bun: + dependencies: + '@mtcute/core': + specifier: workspace:^ + version: link:../core + '@mtcute/html-parser': + specifier: workspace:^ + version: link:../html-parser + '@mtcute/markdown-parser': + specifier: workspace:^ + version: link:../markdown-parser + '@mtcute/wasm': + specifier: workspace:^ + version: link:../wasm + devDependencies: + '@mtcute/test': + specifier: workspace:^ + version: link:../test + bun-types: + specifier: 1.0.33 + version: 1.0.33 + packages/convert: dependencies: '@mtcute/core': @@ -205,9 +227,6 @@ importers: '@mtcute/core': specifier: workspace:^ version: link:../core - '@mtcute/sqlite': - specifier: workspace:^ - version: link:../sqlite events: specifier: 3.2.0 version: 3.2.0 @@ -284,37 +303,9 @@ importers: '@mtcute/markdown-parser': specifier: workspace:^ version: link:../markdown-parser - '@mtcute/sqlite': - specifier: workspace:^ - version: link:../sqlite '@mtcute/wasm': specifier: workspace:^ version: link:../wasm - devDependencies: - '@mtcute/test': - specifier: workspace:^ - version: link:../test - - packages/socks-proxy: - dependencies: - '@mtcute/node': - specifier: workspace:^ - version: link:../node - ip6: - specifier: 0.2.7 - version: 0.2.7 - - packages/sqlite: - dependencies: - '@mtcute/core': - specifier: workspace:^ - version: link:../core - '@mtcute/tl': - specifier: '*' - version: link:../tl - '@mtcute/tl-runtime': - specifier: workspace:^ - version: link:../tl-runtime better-sqlite3: specifier: 9.2.2 version: 9.2.2 @@ -326,6 +317,15 @@ importers: specifier: 7.6.4 version: 7.6.4 + packages/socks-proxy: + dependencies: + '@mtcute/node': + specifier: workspace:^ + version: link:../node + ip6: + specifier: 0.2.7 + version: 0.2.7 + packages/test: dependencies: '@mtcute/core': @@ -1425,6 +1425,12 @@ packages: dependencies: undici-types: 5.26.5 + /@types/node@20.11.30: + resolution: {integrity: sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==} + dependencies: + undici-types: 5.26.5 + dev: true + /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} dev: true @@ -1445,6 +1451,12 @@ packages: '@types/node': 20.10.0 dev: true + /@types/ws@8.5.10: + resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} + dependencies: + '@types/node': 20.10.0 + dev: true + /@types/ws@8.5.4: resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} dependencies: @@ -2054,6 +2066,13 @@ packages: resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} dev: true + /bun-types@1.0.33: + resolution: {integrity: sha512-L5tBIf9g6rBBkvshqysi5NoLQ9NnhSPU1pfJ9FzqoSfofYdyac3WLUnOIuQ+M5za/sooVUOP2ko+E6Tco0OLIA==} + dependencies: + '@types/node': 20.11.30 + '@types/ws': 8.5.10 + dev: true + /cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'}