diff --git a/.config/vite-utils/collect-test-entrypoints.ts b/.config/vite-utils/collect-test-entrypoints.ts
index 0864f5d1..c9a10ab1 100644
--- a/.config/vite-utils/collect-test-entrypoints.ts
+++ b/.config/vite-utils/collect-test-entrypoints.ts
@@ -6,7 +6,6 @@ import { globSync } from 'glob'
export function collectTestEntrypoints(params: { skipPackages: string[], skipTests: string[] }) {
const files: string[] = []
- // eslint-disable-next-line no-restricted-globals
const packages = resolve(__dirname, '../../packages')
const skipTests = params.skipTests.map(path => resolve(packages, path))
diff --git a/.config/vite.build.ts b/.config/vite.build.ts
index ffe60001..af6aa900 100644
--- a/.config/vite.build.ts
+++ b/.config/vite.build.ts
@@ -1,14 +1,14 @@
///
-import { cpSync, existsSync, writeFileSync } from 'node:fs'
-import { relative, resolve } from 'node:path'
+import { readFileSync } from 'node:fs'
+import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
+// todo
+import { fumanBuild } from '@fuman/build/vite'
import type { ConfigEnv, UserConfig } from 'vite'
import { nodeExternals } from 'rollup-plugin-node-externals'
import dts from 'vite-plugin-dts'
-import { processPackageJson } from './vite-utils/package-json'
-
const rootDir = fileURLToPath(new URL('..', import.meta.url))
export default async (env: ConfigEnv): Promise => {
@@ -16,15 +16,7 @@ export default async (env: ConfigEnv): Promise => {
throw new Error('This config is only for building')
}
- const { packageJson, entrypoints } = processPackageJson(process.cwd())
-
- let customConfig: any
- try {
- const mod = await import(resolve(process.cwd(), 'build.config.js'))
- customConfig = await mod.default()
- } catch (e) {
- if (e.code !== 'ERR_MODULE_NOT_FOUND') throw e
- }
+ const packageJson = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf8'))
const CJS_DEPRECATION_WARNING = `
if (typeof globalThis !== 'undefined' && !globalThis._MTCUTE_CJS_DEPRECATION_WARNED) {
@@ -34,69 +26,19 @@ if (typeof globalThis !== 'undefined' && !globalThis._MTCUTE_CJS_DEPRECATION_WAR
}
`.trim()
- if (customConfig?.preBuild) {
- await customConfig.preBuild()
- }
-
return {
build: {
rollupOptions: {
plugins: [
- ...(customConfig?.rollupPluginsPre ?? []),
- nodeExternals({
- builtinsPrefix: 'ignore',
- exclude: /^@fuman\//,
- }),
{
- name: 'mtcute-finalize',
+ name: 'mtcute-cjs-deprecated',
renderChunk(code, chunk, options) {
if (options.format !== 'cjs') return null
return `${CJS_DEPRECATION_WARNING}\n${code}`
},
- async closeBundle() {
- const packageDir = process.cwd()
- const outDir = resolve(packageDir, 'dist')
-
- customConfig?.finalPackageJson?.(packageJson)
-
- writeFileSync(resolve(outDir, 'package.json'), JSON.stringify(packageJson, null, 4))
- cpSync(resolve(rootDir, 'LICENSE'), resolve(outDir, 'LICENSE'))
- cpSync(resolve(process.cwd(), 'README.md'), resolve(outDir, 'README.md'))
-
- if (existsSync(resolve(outDir, 'chunks/cjs'))) {
- // write {"type":"commonjs"} into chunks/cjs so that node doesn't complain
- const cjsFile = resolve(outDir, 'chunks/cjs/package.json')
- writeFileSync(cjsFile, JSON.stringify({ type: 'commonjs' }, null, 4))
- }
-
- for (const [name, entry] of Object.entries(entrypoints)) {
- const dTsFile = resolve(outDir, `${name}.d.ts`)
- if (!existsSync(dTsFile)) {
- const entryTypings = resolve(outDir, entry.replace('/src/', '/').replace(/\.ts$/, '.d.ts'))
- if (!existsSync(entryTypings)) continue
-
- const relativePath = relative(outDir, entryTypings)
- writeFileSync(dTsFile, `export * from './${relativePath.replace(/\.d\.ts$/, '.js')}'`)
- }
-
- cpSync(dTsFile, dTsFile.replace(/\.d\.ts$/, '.d.cts'))
- }
-
- await customConfig?.final?.({ outDir, packageDir })
- },
},
- ...(customConfig?.rollupPluginsPost ?? []),
],
- output: {
- minifyInternalExports: false,
- chunkFileNames: 'chunks/[format]/[hash].js',
- },
- external: customConfig?.external,
- },
- lib: {
- entry: entrypoints as any,
- formats: customConfig?.buildCjs === false ? ['es'] : ['es', 'cjs'],
},
minify: false,
outDir: 'dist',
@@ -104,10 +46,17 @@ if (typeof globalThis !== 'undefined' && !globalThis._MTCUTE_CJS_DEPRECATION_WAR
target: 'es2022',
},
plugins: [
- ...(customConfig?.vitePlugins ?? []),
+ nodeExternals({
+ builtinsPrefix: 'ignore',
+ }),
+ fumanBuild({
+ root: rootDir,
+ autoSideEffectsFalse: true,
+ }),
dts({
// broken; see https://github.com/qmhc/vite-plugin-dts/issues/321, https://github.com/microsoft/rushstack/issues/3557
// rollupTypes: true,
+ insertTypesEntry: true,
}),
],
}
diff --git a/.config/vite.bun.ts b/.config/vite.bun.ts
index 7f7cfef7..5d1b6327 100644
--- a/.config/vite.bun.ts
+++ b/.config/vite.bun.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-globals */
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
@@ -78,15 +77,6 @@ export default defineConfig({
return code
},
},
- {
- name: 'fix-events',
- transform(code) {
- if (!code.includes('events')) return code
- return code.replace(/^import (.+?) from ['"]events['"]/gms, (_, name) => {
- return `import ${name} from 'node:events'`
- })
- },
- },
{
name: 'fix-wasm-load',
async transform(code) {
diff --git a/.config/vite.deno.ts b/.config/vite.deno.ts
index 1d202639..bc7a717c 100644
--- a/.config/vite.deno.ts
+++ b/.config/vite.deno.ts
@@ -6,7 +6,6 @@ import { fixupCjs } from './vite-utils/fixup-cjs'
import { testSetup } from './vite-utils/test-setup-plugin'
import { collectTestEntrypoints } from './vite-utils/collect-test-entrypoints'
-// eslint-disable-next-line no-restricted-globals
const POLYFILLS = resolve(__dirname, 'vite-utils/polyfills-deno.ts')
export default defineConfig({
diff --git a/.npmrc b/.npmrc
index 41583e36..b98d11ca 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1 +1,2 @@
@jsr:registry=https://npm.jsr.io
+@fuman:registry=https://npm.tei.su
diff --git a/build.config.js b/build.config.js
new file mode 100644
index 00000000..a88b449e
--- /dev/null
+++ b/build.config.js
@@ -0,0 +1,68 @@
+/** @type {import('@fuman/build').RootConfig} */
+export default {
+ jsr: {
+ exclude: ['**/*.{test,bench,test-utils}.ts', '**/__fixtures__/**'],
+ sourceDir: 'src',
+ transformCode: (path, code) => {
+ // add shims for node-specific APIs and replace NodeJS.* types
+ // pretty fragile, but it works for now
+ // todo: remove this god awfulness and use `declare const` in-place instead
+
+ const typesToReplace = {
+ 'NodeJS\\.Timeout': 'number',
+ 'NodeJS\\.Immediate': 'number',
+ }
+ const nodeSpecificApis = {
+ setImmediate: '(cb: (...args: any[]) => void, ...args: any[]) => number',
+ clearImmediate: '(id: number) => void',
+ Buffer:
+ '{ '
+ + 'concat: (...args: any[]) => Uint8Array, '
+ + 'from: (data: any, encoding?: string) => { toString(encoding?: string): string }, '
+ + ' }',
+ SharedWorker: ['type', 'never'],
+ WorkerGlobalScope:
+ '{ '
+ + ' new (): typeof WorkerGlobalScope, '
+ + ' postMessage: (message: any, transfer?: Transferable[]) => void, '
+ + ' addEventListener: (type: "message", listener: (ev: MessageEvent) => void) => void, '
+ + ' }',
+ process: '{ ' + 'hrtime: { bigint: () => bigint }, ' + '}',
+ }
+
+ for (const [name, decl_] of Object.entries(nodeSpecificApis)) {
+ if (code.includes(name)) {
+ if (name === 'Buffer' && code.includes('node:buffer')) continue
+
+ const isType = Array.isArray(decl_) && decl_[0] === 'type'
+ const decl = isType ? decl_[1] : decl_
+
+ if (isType) {
+ code = `declare type ${name} = ${decl};\n${code}`
+ } else {
+ code = `declare const ${name}: ${decl};\n${code}`
+ }
+ }
+ }
+
+ for (const [oldType, newType] of Object.entries(typesToReplace)) {
+ if (code.match(oldType)) {
+ code = code.replace(new RegExp(oldType, 'g'), newType)
+ }
+ }
+
+ return code
+ },
+ },
+ versioning: {
+ exclude: [
+ '**/*.test.ts',
+ '**/*.test-utils.ts',
+ '**/__fixtures__/**',
+ '**/*.md',
+ 'typedoc.cjs',
+ '{scripts,dist,tests,private}/**',
+ ],
+ },
+ viteConfig: '.config/vite.build.ts',
+}
diff --git a/eslint.config.js b/eslint.config.js
index ccfaef2e..dccdf0a9 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -87,6 +87,7 @@ export default antfu({
'ts/switch-exhaustiveness-check': 'off',
'ts/restrict-template-expressions': 'off',
'ts/method-signature-style': 'off',
+ 'style/indent-binary-ops': 'off',
},
}, {
ignores: [
diff --git a/package.json b/package.json
index a5c8eb44..1f67b102 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"homepage": "https://mtcute.dev",
"repository": {
"type": "git",
- "url": "https://github.com/mtcute/mtcute"
+ "url": "git+https://github.com/mtcute/mtcute.git"
},
"keywords": [
"telegram",
@@ -26,7 +26,7 @@
"packages/*"
],
"scripts": {
- "postinstall": "node scripts/validate-deps-versions.js && node scripts/remove-jsr-sourcefiles.js",
+ "postinstall": "fuman-build validate-workspace-deps && node scripts/remove-jsr-sourcefiles.js",
"test": "vitest --config .config/vite.ts run",
"test:dev": "vitest --config .config/vite.ts watch",
"test:ui": "vitest --config .config/vite.ts --ui",
@@ -42,12 +42,13 @@
"lint:fix": "eslint --fix .",
"publish-all": "node scripts/publish.js all",
"docs": "typedoc --options .config/typedoc/config.cjs",
- "build-package": "node scripts/build-package.js",
+ "build-package": "tsx scripts/build-package.js",
"build-package-vite": "node scripts/build-package-vite.js"
},
"devDependencies": {
"@antfu/eslint-config": "2.26.0",
- "@fuman/jsr": "workspace:^",
+ "@fuman/build": "0.0.1",
+ "@fuman/utils": "0.0.1",
"@types/deno": "npm:@teidesu/deno-types@1.46.3",
"@types/node": "20.10.0",
"@types/ws": "8.5.4",
diff --git a/packages/bun/build.config.js b/packages/bun/build.config.js
index a039873e..ec8ff221 100644
--- a/packages/bun/build.config.js
+++ b/packages/bun/build.config.js
@@ -1,4 +1,13 @@
+/** @type {import('@fuman/build/vite').CustomBuildConfig} */
export default () => ({
- buildCjs: false,
- external: ['bun', 'bun:sqlite'],
+ viteConfig: {
+ build: {
+ lib: {
+ formats: ['es'],
+ },
+ rollupOptions: {
+ external: ['bun', 'bun:sqlite'],
+ },
+ },
+ },
})
diff --git a/packages/bun/package.json b/packages/bun/package.json
index b47f133d..2864fc21 100644
--- a/packages/bun/package.json
+++ b/packages/bun/package.json
@@ -12,20 +12,20 @@
"./utils.js": "./src/utils.ts",
"./methods.js": "./src/methods.ts"
},
- "scripts": {
- "docs": "typedoc",
- "build": "pnpm run -w build-package bun"
- },
"dependencies": {
"@mtcute/core": "workspace:^",
"@mtcute/html-parser": "workspace:^",
"@mtcute/markdown-parser": "workspace:^",
"@mtcute/wasm": "workspace:^",
- "@fuman/bun": "workspace:^",
- "@fuman/net": "workspace:^",
- "@fuman/io": "workspace:^"
+ "@fuman/utils": "0.0.1",
+ "@fuman/bun": "0.0.1",
+ "@fuman/net": "0.0.1",
+ "@fuman/io": "0.0.1"
},
"devDependencies": {
"@mtcute/test": "workspace:^"
+ },
+ "fuman": {
+ "jsr": "skip"
}
}
diff --git a/packages/bun/src/client.ts b/packages/bun/src/client.ts
index 88b0254a..fce0d6ad 100644
--- a/packages/bun/src/client.ts
+++ b/packages/bun/src/client.ts
@@ -11,6 +11,7 @@ import {
BaseTelegramClient as BaseTelegramClientBase,
TelegramClient as TelegramClientBase,
} from '@mtcute/core/client.js'
+import { unknownToError } from '@fuman/utils'
import { downloadToFile } from './methods/download-file.js'
import { downloadAsNodeStream } from './methods/download-node-stream.js'
@@ -127,7 +128,7 @@ export class TelegramClient extends TelegramClientBase {
this.start(params)
.then(then)
- .catch(err => this.emitError(err))
+ .catch(err => this.onError.emit(unknownToError(err)))
}
downloadToFile(
diff --git a/packages/bun/src/utils/crypto.ts b/packages/bun/src/utils/crypto.ts
index 02fd327c..b1887219 100644
--- a/packages/bun/src/utils/crypto.ts
+++ b/packages/bun/src/utils/crypto.ts
@@ -12,6 +12,7 @@ import {
ige256Encrypt,
initSync,
} from '@mtcute/wasm'
+import { u8 } from '@fuman/utils'
// we currently prefer wasm for ctr because bun mostly uses browserify polyfills for node:crypto
// which are slow AND semi-broken
@@ -52,20 +53,20 @@ export class BunCryptoProvider extends BaseCryptoProvider implements ICryptoProv
algo = 'sha512',
): Promise {
return new Promise((resolve, reject) =>
- pbkdf2(password, salt, iterations, keylen, algo, (err: Error | null, buf: Uint8Array) =>
- err !== null ? reject(err) : resolve(buf)),
+ pbkdf2(password, salt, iterations, keylen, algo, (err: Error | null, buf: Buffer) =>
+ err !== null ? reject(err) : resolve(buf as unknown as Uint8Array)),
)
}
sha1(data: Uint8Array): Uint8Array {
- const res = new Uint8Array(Bun.SHA1.byteLength)
+ const res = u8.alloc(Bun.SHA1.byteLength)
Bun.SHA1.hash(data, res)
return res
}
sha256(data: Uint8Array): Uint8Array {
- const res = new Uint8Array(Bun.SHA256.byteLength)
+ const res = u8.alloc(Bun.SHA256.byteLength)
Bun.SHA256.hash(data, res)
return res
@@ -90,7 +91,7 @@ export class BunCryptoProvider extends BaseCryptoProvider implements ICryptoProv
// telegram accepts both zlib and gzip, but zlib is faster and has less overhead, so we use it here
return deflateSync(data, {
maxOutputLength: maxSize,
- })
+ }) as unknown as Uint8Array
// hot path, avoid additional runtime checks
} catch (e: any) {
if (e.code === 'ERR_BUFFER_TOO_LARGE') {
@@ -102,7 +103,7 @@ export class BunCryptoProvider extends BaseCryptoProvider implements ICryptoProv
}
gunzip(data: Uint8Array): Uint8Array {
- return gunzipSync(data)
+ return gunzipSync(data) as unknown as Uint8Array
}
randomFill(buf: Uint8Array): void {
diff --git a/packages/bun/src/worker.ts b/packages/bun/src/worker.ts
index a3edd1a3..0d7360cc 100644
--- a/packages/bun/src/worker.ts
+++ b/packages/bun/src/worker.ts
@@ -14,7 +14,7 @@ import {
TelegramWorkerPort as TelegramWorkerPortBase,
} from '@mtcute/core/worker.js'
-import { BunPlatform } from './platform'
+import { BunPlatform } from './platform.js'
export type { TelegramWorkerOptions, WorkerCustomMethods }
diff --git a/packages/convert/package.json b/packages/convert/package.json
index 7a4b2f4d..66a0000f 100644
--- a/packages/convert/package.json
+++ b/packages/convert/package.json
@@ -8,13 +8,10 @@
"license": "MIT",
"sideEffects": false,
"exports": "./src/index.ts",
- "scripts": {
- "build": "pnpm run -w build-package convert"
- },
"dependencies": {
"@mtcute/core": "workspace:^",
- "@fuman/utils": "workspace:^",
- "@fuman/net": "workspace:^"
+ "@fuman/utils": "0.0.1",
+ "@fuman/net": "0.0.1"
},
"devDependencies": {
"@mtcute/test": "workspace:^"
diff --git a/packages/convert/src/pyrogram/serialize.ts b/packages/convert/src/pyrogram/serialize.ts
index 19e1184c..9d073a45 100644
--- a/packages/convert/src/pyrogram/serialize.ts
+++ b/packages/convert/src/pyrogram/serialize.ts
@@ -1,5 +1,5 @@
import { Long, MtArgumentError } from '@mtcute/core'
-import { base64, typed } from '@fuman/utils'
+import { base64, typed, u8 } from '@fuman/utils'
import type { PyrogramSession } from './types.js'
@@ -13,32 +13,32 @@ export function serializePyrogramSession(session: PyrogramSession): string {
const userIdLong = Long.fromNumber(session.userId, true)
- let u8: Uint8Array
+ let buf: Uint8Array
if (session.apiId === undefined) {
// old format
- u8 = new Uint8Array(SESSION_STRING_SIZE_OLD)
- const dv = typed.toDataView(u8)
+ buf = u8.alloc(SESSION_STRING_SIZE_OLD)
+ const dv = typed.toDataView(buf)
dv.setUint8(0, session.dcId)
dv.setUint8(1, session.isTest ? 1 : 0)
- u8.set(session.authKey, 2)
+ buf.set(session.authKey, 2)
dv.setUint32(258, userIdLong.high)
dv.setUint32(262, userIdLong.low)
dv.setUint8(266, session.isBot ? 1 : 0)
} else {
- u8 = new Uint8Array(SESSION_STRING_SIZE)
- const dv = typed.toDataView(u8)
+ buf = u8.alloc(SESSION_STRING_SIZE)
+ const dv = typed.toDataView(buf)
dv.setUint8(0, session.dcId)
dv.setUint32(1, session.apiId)
dv.setUint8(5, session.isTest ? 1 : 0)
- u8.set(session.authKey, 6)
+ buf.set(session.authKey, 6)
dv.setUint32(262, userIdLong.high)
dv.setUint32(266, userIdLong.low)
dv.setUint8(270, session.isBot ? 1 : 0)
}
- return base64.encode(u8, true)
+ return base64.encode(buf, true)
}
diff --git a/packages/convert/src/telethon/serialize.ts b/packages/convert/src/telethon/serialize.ts
index 8bb5a27b..133375b3 100644
--- a/packages/convert/src/telethon/serialize.ts
+++ b/packages/convert/src/telethon/serialize.ts
@@ -1,5 +1,5 @@
import { MtArgumentError } from '@mtcute/core'
-import { base64, typed } from '@fuman/utils'
+import { base64, typed, u8 } from '@fuman/utils'
import { ip } from '@fuman/net'
import type { TelethonSession } from './types.js'
@@ -10,26 +10,26 @@ export function serializeTelethonSession(session: TelethonSession): string {
}
const ipSize = session.ipv6 ? 16 : 4
- const u8 = new Uint8Array(259 + ipSize)
- const dv = typed.toDataView(u8)
+ const buf = u8.alloc(259 + ipSize)
+ const dv = typed.toDataView(buf)
dv.setUint8(0, session.dcId)
let pos
if (session.ipv6) {
- u8.subarray(1, 17).set(ip.toBytesV6(ip.parseV6(session.ipAddress)))
+ buf.subarray(1, 17).set(ip.toBytesV6(ip.parseV6(session.ipAddress)))
pos = 17
} else {
- u8.subarray(1, 5).set(ip.parseV4(session.ipAddress).parts)
+ buf.subarray(1, 5).set(ip.parseV4(session.ipAddress).parts)
pos = 5
}
dv.setUint16(pos, session.port)
pos += 2
- u8.set(session.authKey, pos)
+ buf.set(session.authKey, pos)
- let b64 = base64.encode(u8, true)
+ let b64 = base64.encode(buf, true)
while (b64.length % 4 !== 0) b64 += '=' // for some reason telethon uses padding
return `1${b64}`
diff --git a/packages/core/build.config.js b/packages/core/build.config.js
index 430ffc71..957cddcc 100644
--- a/packages/core/build.config.js
+++ b/packages/core/build.config.js
@@ -5,6 +5,7 @@ import * as fs from 'node:fs'
const KNOWN_DECORATORS = ['memoizeGetters', 'makeInspectable']
+/** @type {import('@fuman/build/vite').CustomBuildConfig} */
export default () => {
const networkManagerId = fileURLToPath(new URL('./src/network/network-manager.ts', import.meta.url))
const highlevelTypesDir = fileURLToPath(new URL('./src/highlevel/types', import.meta.url))
@@ -18,7 +19,7 @@ export default () => {
)
return {
- rollupPluginsPre: [
+ pluginsPre: [
{
name: 'mtcute-core-build-plugin',
transform(code, id) {
@@ -70,7 +71,7 @@ export default () => {
},
},
],
- finalJsr({ outDir }) {
+ finalizeJsr({ outDir }) {
const networkMgrFile = resolve(outDir, 'network/network-manager.ts')
const code = fs.readFileSync(networkMgrFile, 'utf8')
diff --git a/packages/core/package.json b/packages/core/package.json
index 067763f3..4e4e1ff6 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -15,14 +15,13 @@
"./methods.js": "./src/highlevel/methods.ts"
},
"scripts": {
- "build": "pnpm run -w build-package core",
"gen-client": "node ./scripts/generate-client.cjs",
"gen-updates": "node ./scripts/generate-updates.cjs"
},
"dependencies": {
- "@fuman/io": "workspace:^",
- "@fuman/net": "workspace:^",
- "@fuman/utils": "workspace:^",
+ "@fuman/io": "0.0.1",
+ "@fuman/net": "0.0.1",
+ "@fuman/utils": "0.0.1",
"@mtcute/file-id": "workspace:^",
"@mtcute/tl": "workspace:^",
"@mtcute/tl-runtime": "workspace:^",
diff --git a/packages/core/scripts/generate-client.cjs b/packages/core/scripts/generate-client.cjs
index 53f222cb..abd994d2 100644
--- a/packages/core/scripts/generate-client.cjs
+++ b/packages/core/scripts/generate-client.cjs
@@ -579,8 +579,10 @@ withParams(params: RpcCallOptions): this\n`)
it.type = { kind: ts.SyntaxKind.StringKeyword }
} else if (
it.initializer.kind === ts.SyntaxKind.NumericLiteral
- || (it.initializer.kind === ts.SyntaxKind.Identifier
- && (it.initializer.escapedText === 'NaN' || it.initializer.escapedText === 'Infinity'))
+ || (
+ it.initializer.kind === ts.SyntaxKind.Identifier
+ && (it.initializer.escapedText === 'NaN' || it.initializer.escapedText === 'Infinity')
+ )
) {
it.type = { kind: ts.SyntaxKind.NumberKeyword }
} else {
@@ -723,8 +725,6 @@ withParams(params: RpcCallOptions): this\n`)
'call',
'importSession',
'exportSession',
- 'onError',
- 'emitError',
'handleClientUpdate',
'getApiCrenetials',
'getPoolSize',
diff --git a/packages/core/src/highlevel/base.test.ts b/packages/core/src/highlevel/base.test.ts
index 02df3604..5d509110 100644
--- a/packages/core/src/highlevel/base.test.ts
+++ b/packages/core/src/highlevel/base.test.ts
@@ -8,7 +8,7 @@ describe('BaseTelegramClient', () => {
const session = await client.exportSession()
expect(session).toMatchInlineSnapshot(
- `"AwQAAAAXAgIADjE0OS4xNTQuMTY3LjUwALsBAAAXAgICDzE0OS4xNTQuMTY3LjIyMrsBAAD-AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"`,
+ '"AwQAAAAXAgIADjE0OS4xNTQuMTY3LjUwALsBAAAXAgICDzE0OS4xNTQuMTY3LjIyMrsBAAD-AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"',
)
})
})
diff --git a/packages/core/src/highlevel/base.ts b/packages/core/src/highlevel/base.ts
index b4b8f62c..ffb8904a 100644
--- a/packages/core/src/highlevel/base.ts
+++ b/packages/core/src/highlevel/base.ts
@@ -290,18 +290,7 @@ export class BaseTelegramClient implements ITelegramClient {
})
}
- /**
- * Register an error handler for the client
- *
- * @param handler Error handler.
- */
- onError(handler: (err: unknown) => void): void {
- this.mt.onError(handler)
- }
-
- emitError(err: unknown): void {
- this.mt.emitError(err)
- }
+ onError: Emitter = new Emitter()
handleClientUpdate(updates: tl.TypeUpdates, noDispatch?: boolean): void {
this.updates?.handleClientUpdate(updates, noDispatch)
diff --git a/packages/core/src/highlevel/client.ts b/packages/core/src/highlevel/client.ts
index 49d46408..cbb4ff1a 100644
--- a/packages/core/src/highlevel/client.ts
+++ b/packages/core/src/highlevel/client.ts
@@ -6584,12 +6584,6 @@ TelegramClient.prototype.importSession = function (...args) {
TelegramClient.prototype.exportSession = function (...args) {
return this._client.exportSession(...args)
}
-TelegramClient.prototype.onError = function (...args) {
- return this._client.onError(...args)
-}
-TelegramClient.prototype.emitError = function (...args) {
- return this._client.emitError(...args)
-}
TelegramClient.prototype.handleClientUpdate = function (...args) {
return this._client.handleClientUpdate(...args)
}
diff --git a/packages/core/src/highlevel/client.types.ts b/packages/core/src/highlevel/client.types.ts
index cd014f5a..531451e4 100644
--- a/packages/core/src/highlevel/client.types.ts
+++ b/packages/core/src/highlevel/client.types.ts
@@ -52,13 +52,12 @@ export interface ITelegramClient {
): Promise
importSession(session: string | StringSessionData, force?: boolean): Promise
exportSession(): Promise
- onError(handler: (err: unknown) => void): void
- emitError(err: unknown): void
handleClientUpdate(updates: tl.TypeUpdates, noDispatch?: boolean): void
onServerUpdate: Emitter
onRawUpdate: Emitter
onConnectionState: Emitter
+ onError: Emitter
getApiCrenetials(): Promise<{ id: number, hash: string }>
// todo - this is only used for file dl/ul, which should probably be moved
diff --git a/packages/core/src/highlevel/managers/app-config-manager.ts b/packages/core/src/highlevel/managers/app-config-manager.ts
index fe1bfcdd..101c5bf0 100644
--- a/packages/core/src/highlevel/managers/app-config-manager.ts
+++ b/packages/core/src/highlevel/managers/app-config-manager.ts
@@ -1,36 +1,41 @@
import type { tl } from '@mtcute/tl'
+import { AsyncResource, asNonNull } from '@fuman/utils'
import { MtTypeAssertionError } from '../../types/errors.js'
-import { Reloadable } from '../../utils/reloadable.js'
import { tlJsonToJson } from '../../utils/tl-json.js'
import type { BaseTelegramClient } from '../base.js'
import type { AppConfigSchema } from '../types/misc/app-config.js'
export class AppConfigManager {
- constructor(private client: BaseTelegramClient) {}
+ private _resource
+ constructor(private client: BaseTelegramClient) {
+ this._resource = new AsyncResource({
+ fetcher: async ({ current }) => {
+ const res = await this.client.call({
+ _: 'help.getAppConfig',
+ hash: current?.hash ?? 0,
+ })
- private _reloadable = new Reloadable({
- reload: this._reload.bind(this),
- getExpiresAt: () => 3_600_000,
- disableAutoReload: true,
- })
+ if (res._ === 'help.appConfigNotModified') {
+ return {
+ data: asNonNull(current),
+ expiresIn: 3_600_000,
+ }
+ }
- private async _reload(old?: tl.help.RawAppConfig) {
- const res = await this.client.call({
- _: 'help.getAppConfig',
- hash: old?.hash ?? 0,
+ return {
+ data: res,
+ expiresIn: 3_600_000,
+ }
+ },
})
-
- if (res._ === 'help.appConfigNotModified') return old!
-
- return res
}
private _object?: AppConfigSchema
async get(): Promise {
- if (!this._reloadable.isStale && this._object) return this._object
+ if (!this._resource.isStale && this._object) return this._object
- const obj = tlJsonToJson((await this._reloadable.get()).config)
+ const obj = tlJsonToJson((await this._resource.get()).config)
if (!obj || typeof obj !== 'object') {
throw new MtTypeAssertionError('appConfig', 'object', typeof obj)
diff --git a/packages/core/src/highlevel/methods/_init.ts b/packages/core/src/highlevel/methods/_init.ts
index a9c1b44a..2d0e4a3d 100644
--- a/packages/core/src/highlevel/methods/_init.ts
+++ b/packages/core/src/highlevel/methods/_init.ts
@@ -67,6 +67,7 @@ function _initializeClient(this: TelegramClient, opts: TelegramClientOptions) {
Object.defineProperty(this, 'onServerUpdate', { value: this._client.onServerUpdate })
Object.defineProperty(this, 'onRawUpdate', { value: this._client.onServerUpdate })
Object.defineProperty(this, 'onConnectionState', { value: this._client.onConnectionState })
+ Object.defineProperty(this, 'onError', { value: this._client.onError })
if (!opts.disableUpdates) {
const skipConversationUpdates = opts.skipConversationUpdates ?? true
diff --git a/packages/core/src/highlevel/methods/auth/run.ts b/packages/core/src/highlevel/methods/auth/run.ts
index a1e8e33c..f4649f19 100644
--- a/packages/core/src/highlevel/methods/auth/run.ts
+++ b/packages/core/src/highlevel/methods/auth/run.ts
@@ -1,3 +1,5 @@
+import { unknownToError } from '@fuman/utils'
+
import type { ITelegramClient } from '../../client.types.js'
import type { User } from '../../types/index.js'
@@ -22,5 +24,5 @@ export function run(
): void {
start(client, params)
.then(then)
- .catch(err => client.emitError(err))
+ .catch(err => client.onError.emit(unknownToError(err)))
}
diff --git a/packages/core/src/highlevel/methods/auth/sign-in-qr.ts b/packages/core/src/highlevel/methods/auth/sign-in-qr.ts
index 15307768..b0be5a39 100644
--- a/packages/core/src/highlevel/methods/auth/sign-in-qr.ts
+++ b/packages/core/src/highlevel/methods/auth/sign-in-qr.ts
@@ -4,10 +4,11 @@ import { Deferred, base64 } from '@fuman/utils'
import type { MaybePromise } from '../../../types/utils.js'
import { sleepWithAbort } from '../../../utils/misc-utils.js'
import { assertTypeIs } from '../../../utils/type-assertions.js'
-import type { ITelegramClient, ServerUpdateHandler } from '../../client.types.js'
+import type { ITelegramClient } from '../../client.types.js'
import type { MaybeDynamic } from '../../types/index.js'
import { User } from '../../types/index.js'
import { resolveMaybeDynamic } from '../../utils/misc-utils.js'
+import type { RawUpdateInfo } from '../../updates/types.js'
import { checkPassword } from './check-password.js'
@@ -53,26 +54,19 @@ export async function signInQr(
let waiter: Deferred | undefined
- // crutch – we need to wait for the updateLoginToken update.
- // we replace the server update handler temporarily because:
- // - updates manager may be disabled, in which case `onUpdate` will never be called
- // - even if the updates manager is enabled, it won't start until we're logged in
- //
- // todo: how can we make this more clean?
- const originalHandler = client.getServerUpdateHandler()
-
- const onUpdate: ServerUpdateHandler = (upd) => {
- if (upd._ === 'updateShort' && upd.update._ === 'updateLoginToken') {
+ // todo: we should probably make this into an await-able function
+ const onUpdate = ({ update }: RawUpdateInfo) => {
+ if (update._ === 'updateLoginToken') {
onQrScanned?.()
waiter?.resolve()
- client.onServerUpdate(originalHandler)
+ client.onRawUpdate.remove(onUpdate)
}
}
- client.onServerUpdate(onUpdate)
+ client.onRawUpdate.add(onUpdate)
abortSignal?.addEventListener('abort', () => {
- client.onServerUpdate(originalHandler)
+ client.onRawUpdate.remove(onUpdate)
waiter?.reject(abortSignal.reason)
})
@@ -180,6 +174,6 @@ export async function signInQr(
return new User(self)
} finally {
- client.onServerUpdate(originalHandler)
+ client.onRawUpdate.remove(onUpdate)
}
}
diff --git a/packages/core/src/highlevel/methods/files/upload-file.ts b/packages/core/src/highlevel/methods/files/upload-file.ts
index f29b5211..7fe5aeb3 100644
--- a/packages/core/src/highlevel/methods/files/upload-file.ts
+++ b/packages/core/src/highlevel/methods/files/upload-file.ts
@@ -1,6 +1,6 @@
import type { tl } from '@mtcute/tl'
import type { IReadable } from '@fuman/io'
-import { read } from '@fuman/io'
+import { read, webReadableToFuman } from '@fuman/io'
import { AsyncLock } from '@fuman/utils'
import { MtArgumentError } from '../../../types/errors.js'
@@ -175,7 +175,7 @@ export async function uploadFile(
}
if (file instanceof ReadableStream) {
- file = read.async.fromWeb(file)
+ file = webReadableToFuman(file)
} else if (!(typeof file === 'object' && 'read' in file)) { // IReadable
throw new MtArgumentError('Could not convert input `file` to stream!')
}
diff --git a/packages/core/src/highlevel/storage/service/current-user.ts b/packages/core/src/highlevel/storage/service/current-user.ts
index 9d3cf159..a5ff38b2 100644
--- a/packages/core/src/highlevel/storage/service/current-user.ts
+++ b/packages/core/src/highlevel/storage/service/current-user.ts
@@ -1,5 +1,6 @@
import type { tl } from '@mtcute/tl'
import { TlBinaryReader, TlBinaryWriter, TlSerializationCounter } from '@mtcute/tl-runtime'
+import { u8 } from '@fuman/utils'
import type { IKeyValueRepository } from '../../../storage/repository/key-value.js'
import type { ServiceOptions } from '../../../storage/service/base.js'
@@ -18,7 +19,7 @@ export interface CurrentUserInfo {
const KV_CURRENT_USER = 'current_user'
function serialize(info: CurrentUserInfo | null): Uint8Array {
- if (!info) return new Uint8Array(0)
+ if (!info) return u8.alloc(0)
const hasUsernames = info.usernames.length > 0
diff --git a/packages/core/src/highlevel/storage/service/updates.ts b/packages/core/src/highlevel/storage/service/updates.ts
index 38181a31..2a475102 100644
--- a/packages/core/src/highlevel/storage/service/updates.ts
+++ b/packages/core/src/highlevel/storage/service/updates.ts
@@ -1,4 +1,4 @@
-import { typed } from '@fuman/utils'
+import { typed, u8 } from '@fuman/utils'
import type { IKeyValueRepository } from '../../../storage/repository/key-value.js'
import type { ServiceOptions } from '../../../storage/service/base.js'
@@ -28,7 +28,7 @@ export class UpdatesStateService extends BaseService {
}
private async _setInt(key: string, val: number): Promise {
- const buf = new Uint8Array(4)
+ const buf = u8.alloc(4)
typed.toDataView(buf).setInt32(0, val, true)
await this._kv.set(key, buf)
@@ -73,7 +73,7 @@ export class UpdatesStateService extends BaseService {
}
async setChannelPts(channelId: number, pts: number): Promise {
- const buf = new Uint8Array(4)
+ const buf = u8.alloc(4)
typed.toDataView(buf).setUint32(0, pts, true)
await this._kv.set(KV_CHANNEL_PREFIX + channelId, buf)
diff --git a/packages/core/src/highlevel/types/conversation.ts b/packages/core/src/highlevel/types/conversation.ts
index c99fe3b6..8eb2e2ee 100644
--- a/packages/core/src/highlevel/types/conversation.ts
+++ b/packages/core/src/highlevel/types/conversation.ts
@@ -1,5 +1,5 @@
import type { tl } from '@mtcute/tl'
-import { AsyncLock, Deferred, Deque, timers } from '@fuman/utils'
+import { AsyncLock, Deferred, Deque, timers, unknownToError } from '@fuman/utils'
import { MtArgumentError, MtTimeoutError } from '../../types/errors.js'
import type { MaybePromise } from '../../types/utils.js'
@@ -564,7 +564,7 @@ export class Conversation {
this._queuedNewMessage.popFront()
}
} catch (e: unknown) {
- this.client.emitError(e)
+ this.client.onError.emit(unknownToError(e))
}
this._lastMessage = this._lastReceivedMessage = msg.id
@@ -594,7 +594,7 @@ export class Conversation {
this._pendingEditMessage.delete(msg.id)
}
})().catch((e) => {
- this.client.emitError(e)
+ this.client.onError.emit(unknownToError(e))
})
}
diff --git a/packages/core/src/highlevel/updates/manager.ts b/packages/core/src/highlevel/updates/manager.ts
index e73b46db..ee2c2e08 100644
--- a/packages/core/src/highlevel/updates/manager.ts
+++ b/packages/core/src/highlevel/updates/manager.ts
@@ -1,6 +1,6 @@
import { tl } from '@mtcute/tl'
import Long from 'long'
-import { AsyncLock, ConditionVariable, Deque, timers } from '@fuman/utils'
+import { AsyncLock, ConditionVariable, Deque, timers, unknownToError } from '@fuman/utils'
import { MtArgumentError } from '../../types/errors.js'
import type { MaybePromise } from '../../types/utils.js'
@@ -195,7 +195,7 @@ export class UpdatesManager {
notifyLoggedIn(self: CurrentUserInfo): void {
this.auth = self
- this.startLoop().catch(err => this.client.emitError(err))
+ this.startLoop().catch(err => this.client.onError.emit(unknownToError(err)))
}
notifyLoggedOut(): void {
@@ -238,7 +238,7 @@ export class UpdatesManager {
this.updatesLoopActive = true
timers.clearInterval(this.keepAliveInterval)
this.keepAliveInterval = timers.setInterval(this._onKeepAlive, KEEP_ALIVE_INTERVAL)
- this._loop().catch(err => this.client.emitError(err))
+ this._loop().catch(err => this.client.onError.emit(unknownToError(err)))
if (this.catchUpOnStart) {
this.catchUp()
@@ -1245,20 +1245,20 @@ export class UpdatesManager {
// we just needed to apply new pts values
return
case 'updateDcOptions': {
- const config = client.mt.network.config.getNow()
+ const config = client.mt.network.config.getCached()
if (config) {
client.mt.network.config.setData({
...config,
dcOptions: upd.dcOptions,
- })
+ }, config.expires * 1000)
} else {
- client.mt.network.config.update(true).catch(err => client.emitError(err))
+ client.mt.network.config.update(true).catch(err => client.onError.emit(unknownToError(err)))
}
break
}
case 'updateConfig':
- client.mt.network.config.update(true).catch(err => client.emitError(err))
+ client.mt.network.config.update(true).catch(err => client.onError.emit(unknownToError(err)))
break
case 'updateUserName':
// todo
diff --git a/packages/core/src/highlevel/utils/voice-utils.ts b/packages/core/src/highlevel/utils/voice-utils.ts
index 8243efee..7162671c 100644
--- a/packages/core/src/highlevel/utils/voice-utils.ts
+++ b/packages/core/src/highlevel/utils/voice-utils.ts
@@ -1,4 +1,4 @@
-import { typed } from '@fuman/utils'
+import { typed, u8 } from '@fuman/utils'
/**
* Decode 5-bit encoded voice message waveform into
@@ -49,7 +49,7 @@ export function decodeWaveform(wf: Uint8Array): number[] {
export function encodeWaveform(wf: number[]): Uint8Array {
const bitsCount = wf.length * 5
const bytesCount = ~~((bitsCount + 7) / 8)
- const result = new Uint8Array(bytesCount + 1)
+ const result = u8.alloc(bytesCount + 1)
const dv = typed.toDataView(result)
// Write each 0-31 unsigned char as 5 bit to result.
diff --git a/packages/core/src/highlevel/worker/port.ts b/packages/core/src/highlevel/worker/port.ts
index 8c2688ba..3b321eb4 100644
--- a/packages/core/src/highlevel/worker/port.ts
+++ b/packages/core/src/highlevel/worker/port.ts
@@ -1,18 +1,20 @@
import type { tl } from '@mtcute/tl'
+import { Emitter } from '@fuman/utils'
import type { RpcCallOptions } from '../../network/network-manager.js'
import type { MustEqual } from '../../types/utils.js'
import { LogManager } from '../../utils/logger.js'
-import type { ConnectionState, ITelegramClient, ServerUpdateHandler } from '../client.types.js'
+import type { ConnectionState, ITelegramClient } from '../client.types.js'
import { PeersIndex } from '../types/peers/peers-index.js'
-import type { RawUpdateHandler } from '../updates/types.js'
import type { ICorePlatform } from '../../types/platform'
+import type { RawUpdateInfo } from '../updates/types.js'
import { AppConfigManagerProxy } from './app-config.js'
import { WorkerInvoker } from './invoker.js'
import type { ClientMessageHandler, SendFn, SomeWorker, WorkerCustomMethods } from './protocol.js'
import { deserializeResult } from './protocol.js'
import { TelegramStorageProxy } from './storage.js'
+import { deserializeError } from './errors.js'
export interface TelegramWorkerPortOptions {
worker: SomeWorker
@@ -104,33 +106,10 @@ export abstract class TelegramWorkerPort imp
abstract connectToWorker(worker: SomeWorker, handler: ClientMessageHandler): [SendFn, () => void]
- private _serverUpdatesHandler: ServerUpdateHandler = () => {}
- onServerUpdate(handler: ServerUpdateHandler): void {
- this._serverUpdatesHandler = handler
- }
-
- getServerUpdateHandler(): ServerUpdateHandler {
- return this._serverUpdatesHandler
- }
-
- private _errorHandler: (err: unknown) => void = () => {}
- onError(handler: (err: unknown) => void): void {
- this._errorHandler = handler
- }
-
- emitError(err: unknown): void {
- this._errorHandler(err)
- }
-
- private _updateHandler: RawUpdateHandler = () => {}
- onUpdate(handler: RawUpdateHandler): void {
- this._updateHandler = handler
- }
-
- private _connectionStateHandler: (state: ConnectionState) => void = () => {}
- onConnectionState(handler: (state: ConnectionState) => void): void {
- this._connectionStateHandler = handler
- }
+ onServerUpdate: Emitter = new Emitter()
+ onRawUpdate: Emitter = new Emitter()
+ onConnectionState: Emitter = new Emitter()
+ onError: Emitter = new Emitter()
private _onMessage: ClientMessageHandler = (message) => {
switch (message.type) {
@@ -138,22 +117,22 @@ export abstract class TelegramWorkerPort imp
this.log.handler(message.color, message.level, message.tag, message.fmt, message.args)
break
case 'server_update':
- this._serverUpdatesHandler(deserializeResult(message.update))
+ this.onServerUpdate.emit(deserializeResult(message.update))
break
case 'conn_state':
- this._connectionStateHandler(message.state)
+ this.onConnectionState.emit(message.state)
break
case 'update': {
const peers = new PeersIndex(deserializeResult(message.users), deserializeResult(message.chats))
peers.hasMin = message.hasMin
- this._updateHandler(deserializeResult(message.update), peers)
+ this.onRawUpdate.emit({ update: deserializeResult(message.update), peers })
break
}
case 'result':
this._invoker.handleResult(message)
break
case 'error':
- this.emitError(message.error)
+ this.onError.emit(deserializeError(message.error))
break
case 'stop':
this._abortController.abort()
diff --git a/packages/core/src/highlevel/worker/protocol.ts b/packages/core/src/highlevel/worker/protocol.ts
index b4a6d5e0..346302d8 100644
--- a/packages/core/src/highlevel/worker/protocol.ts
+++ b/packages/core/src/highlevel/worker/protocol.ts
@@ -31,7 +31,7 @@ export type WorkerOutboundMessage =
chats: SerializedResult