This commit is contained in:
alina sireneva 2024-04-23 22:53:59 +03:00 committed by GitHub
commit 7ad3ddbc0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
134 changed files with 1811 additions and 646 deletions

View file

@ -285,6 +285,12 @@ module.exports = {
'no-restricted-imports': 'off',
'import/no-relative-packages': 'off', // common-internals is symlinked from node
}
},
{
files: ['e2e/deno/**'],
rules: {
'import/no-unresolved': 'off',
}
}
],
settings: {

View file

@ -1,3 +1,4 @@
**/node_modules
**/private
**/dist
/e2e

View file

@ -90,7 +90,7 @@ jobs:
API_ID: ${{ secrets.TELEGRAM_API_ID }}
API_HASH: ${{ secrets.TELEGRAM_API_HASH }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: cd e2e && ./cli.sh ci
run: cd e2e/node && ./cli.sh ci
- name: Publish to canary NPM
if: github.repository == 'mtcute/mtcute' # do not run on forks
continue-on-error: true
@ -98,4 +98,18 @@ jobs:
NPM_TOKEN: ${{ secrets.CANARY_NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REGISTRY: 'https://npm.tei.su'
run: cd e2e && ./cli.sh ci-publish
run: cd e2e/node && ./cli.sh ci-publish
e2e-deno:
runs-on: ubuntu-latest
needs: [test-node, test-web, test-bun]
permissions:
contents: read
actions: write
steps:
- uses: actions/checkout@v4
- name: Run end-to-end tests under Deno
env:
API_ID: ${{ secrets.TELEGRAM_API_ID }}
API_HASH: ${{ secrets.TELEGRAM_API_HASH }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: cd e2e/deno && ./cli.sh ci

3
e2e/deno/.dockerignore Normal file
View file

@ -0,0 +1,3 @@
/.jsr-data
/dist
/deno.lock

View file

@ -1,3 +1,5 @@
# obtain these values from my.telegram.org
API_ID=
API_HASH=
GITHUB_TOKEN=

3
e2e/deno/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/.jsr-data
.env
/deno.lock

26
e2e/deno/Dockerfile.build Normal file
View file

@ -0,0 +1,26 @@
FROM denoland/deno:bin-1.42.4 as deno-bin
FROM node:20
WORKDIR /app
COPY --from=deno-bin /deno /bin/deno
# todo: remove once 1.42.5 is out
RUN deno upgrade --canary --version=2f5a6a8514ad8eadce1a0a9f1a7a419692e337ef
RUN corepack enable && \
corepack prepare pnpm@8.7.1 --activate
COPY ../.. /app/
RUN pnpm install --frozen-lockfile && \
pnpm -C packages/tl run gen-code
RUN apt update && apt install -y socat
ENV REGISTRY="http://jsr/"
ENV E2E="1"
ENV JSR="1"
ENV JSR_TOKEN="token"
ENTRYPOINT [ "node", "/app/scripts/publish.js" ]
CMD [ "all" ]

3
e2e/deno/Dockerfile.jsr Normal file
View file

@ -0,0 +1,3 @@
FROM ghcr.io/teidesu/jsr-api:latest
RUN apt update && apt install -y curl

10
e2e/deno/Dockerfile.test Normal file
View file

@ -0,0 +1,10 @@
FROM denoland/deno:1.42.4
WORKDIR /app
RUN apt update && apt install -y socat
COPY ./ /app/
ENV DOCKER="1"
ENTRYPOINT [ "./cli.sh", "run" ]

30
e2e/deno/README.md Normal file
View file

@ -0,0 +1,30 @@
# mtcute e2e tests (Deno edition)
This directory contains end-to-end tests for mtcute under Deno.
They are made for 2 purposes:
- Ensure published packages work as expected and can properly be imported
- Ensure that the library works with the actual Telegram API
To achieve the first goal, we use a local JSR instance container where we publish the package,
and then install it from there in another container
## Setting up
Before running the tests, you need to copy `.env.example` to `.env` and fill in the values
## Running tests
```bash
# first start a local jsr instance
./cli.sh start
# push all packages to the local registry
./cli.sh update
# pushing a particular package is not supported due to jsr limitations
# run the tests
./cli.sh run
# or in docker
./cli.sh run-docker
```

77
e2e/deno/cli.sh Executable file
View file

@ -0,0 +1,77 @@
#!/bin/bash
set -eau
method=$1
shift
case "$method" in
"start")
docker compose up -d --wait jsr
node ./init-server.js
;;
"update")
# unpublish all packages
if [ -d .jsr-data/gcs/modules/@mtcute ]; then
rm -rf .jsr-data/gcs/modules/@mtcute
docker compose exec jsr-db psql registry -U user -c "delete from publishing_tasks;"
docker compose exec jsr-db psql registry -U user -c "delete from package_files;"
docker compose exec jsr-db psql registry -U user -c "delete from npm_tarballs;"
docker compose exec jsr-db psql registry -U user -c "delete from package_version_dependencies;"
docker compose exec jsr-db psql registry -U user -c "delete from package_versions;"
docker compose exec jsr-db psql registry -U user -c "delete from packages;"
fi
# publish all packages
docker compose run --rm --build build all
# clear cache
if command -v deno &> /dev/null; then
rm -rf $(deno info --json | jq .denoDir -r)/deps
fi
if [ -f deno.lock ]; then
rm deno.lock
fi
;;
"clean")
docker compose down
rm -rf .jsr-data
;;
"stop")
docker compose down
;;
"run")
if [ -f .env ]; then
source .env
fi
if [ -n "$DOCKER" ]; then
# running behind a socat proxy seems to fix some of the docker networking issues (thx kamillaova)
socat TCP-LISTEN:4873,fork,reuseaddr TCP4:jsr:80 &
socat_pid=$!
trap "kill $socat_pid" EXIT
fi
export JSR_URL=http://localhost:4873
if [ -z "$@" ]; then
deno test -A tests/**/*.ts
else
deno test -A $@
fi
;;
"run-docker")
source .env
docker compose run --rm --build test $@
;;
"ci")
set -eaux
mkdir .jsr-data
./cli.sh start
./cli.sh update
docker compose run --rm --build test
;;
*)
echo "Unknown command"
;;
esac

9
e2e/deno/deno.json Normal file
View file

@ -0,0 +1,9 @@
{
"imports": {
"@mtcute/web": "jsr:@mtcute/web@*",
"@mtcute/wasm": "jsr:@mtcute/wasm@*",
"@mtcute/tl": "jsr:@mtcute/tl@*",
"@mtcute/tl-runtime": "jsr:@mtcute/tl-runtime@*",
"@mtcute/core": "jsr:@mtcute/core@*"
}
}

View file

@ -0,0 +1,78 @@
version: "3"
services:
# jsr (based on https://github.com/teidesu/docker-images/blob/main/jsr/docker-compose.yaml)
jsr-db:
image: postgres:15
command: postgres -c 'max_connections=1000'
restart: always
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: registry
healthcheck:
test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
interval: 5s
retries: 20
start_period: 5s
volumes:
- ./.jsr-data/db:/var/lib/postgresql/data
jsr-gcs:
image: fsouza/fake-gcs-server:latest
command: -scheme http -filesystem-root=/gcs-data -port 4080
volumes:
- ./.jsr-data/gcs:/gcs-data
jsr-api:
depends_on:
jsr-db:
condition: service_healthy
jsr-gcs:
condition: service_started
healthcheck:
test: "curl --fail http://localhost:8001/sitemap.xml || exit 1"
interval: 5s
retries: 20
start_period: 5s
build:
context: .
dockerfile: Dockerfile.jsr
environment:
- "DATABASE_URL=postgres://user:password@jsr-db/registry"
- "GITHUB_CLIENT_ID=fake"
- "GITHUB_CLIENT_SECRET=fake"
- "GCS_ENDPOINT=http://jsr-gcs:4080"
- "MODULES_BUCKET=modules"
- "PUBLISHING_BUCKET=publishing"
- "DOCS_BUCKET=docs"
- "NPM_BUCKET=npm"
- "REGISTRY_URL=http://localhost:4873"
- "NPM_URL=http://example.com/unused"
jsr:
depends_on:
jsr-api:
condition: service_healthy
image: nginx:1.21
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
ports:
- "4873:80"
# our stuff
build:
build:
context: ../..
dockerfile: e2e/deno/Dockerfile.build
environment:
- GITHUB_TOKEN=${GITHUB_TOKEN}
depends_on:
- jsr
test:
build:
context: .
dockerfile: Dockerfile.test
environment:
- API_ID=${API_ID}
- API_HASH=${API_HASH}
depends_on:
- jsr
networks:
mtcute-e2e: {}

67
e2e/deno/init-server.js Normal file
View file

@ -0,0 +1,67 @@
/* eslint-disable no-console */
const { execSync } = require('child_process')
function getDockerContainerIp(name) {
const containerId = execSync(`docker compose ps -q ${name}`).toString().trim()
const ip = execSync(`docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ${containerId}`)
.toString()
.trim()
return ip
}
for (const stmt of [
"delete from tokens where user_id = '00000000-0000-0000-0000-000000000000';",
"insert into tokens (hash, user_id, type, expires_at) values ('3c469e9d6c5875d37a43f353d4f88e61fcf812c66eee3457465a40b0da4153e0', '00000000-0000-0000-0000-000000000000', 'web', current_date + interval '100' year);",
"update users set is_staff = true, scope_limit = 99999 where id = '00000000-0000-0000-0000-000000000000';",
]) {
execSync(`docker compose exec jsr-db psql registry -U user -c "${stmt}"`)
}
console.log('[i] Initialized database')
const GCS_URL = `http://${getDockerContainerIp('jsr-gcs')}:4080/`
const API_URL = `http://${getDockerContainerIp('jsr-api')}:8001/`
async function createBucket(name) {
try {
const resp = await fetch(`${GCS_URL}storage/v1/b`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
})
await resp.text()
return resp.ok || resp.status === 409
} catch (e) {
console.log(e)
return false
}
}
(async () => {
for (const bucket of ['modules', 'docs', 'publishing', 'npm']) {
const ok = await createBucket(bucket)
console.log(`[i] Created bucket ${bucket}: ${ok}`)
}
// create @mtcute scope if it doesn't exist
const resp = await fetch(`${API_URL}api/scopes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Cookie: 'token=token',
},
body: JSON.stringify({ scope: 'mtcute' }),
})
if (resp.status !== 200 && resp.status !== 409) {
throw new Error(`Failed to create scope: ${resp.statusText} ${await resp.text()}`)
}
if (resp.status === 200) {
console.log('[i] Created scope mtcute')
}
})()

29
e2e/deno/nginx.conf Normal file
View file

@ -0,0 +1,29 @@
events {}
http {
upstream gcs {
server jsr-gcs:4080;
}
upstream api {
server jsr-api:8001;
}
error_log /error.log debug;
server {
listen 80;
location ~ ^/(@.*)$ {
proxy_pass http://gcs/storage/v1/b/modules/o/$1?alt=media;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
proxy_pass http://api;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}

View file

@ -0,0 +1,21 @@
import { assertEquals } from 'https://deno.land/std@0.223.0/assert/mod.ts'
import { BaseTelegramClient } from '@mtcute/core/client.js'
import { getApiParams } from '../../utils.ts'
Deno.test('@mtcute/core', async (t) => {
await t.step('connects to test DC and makes help.getNearestDc', async () => {
const tg = new BaseTelegramClient({
...getApiParams(),
})
await tg.connect()
const config = await tg.call({ _: 'help.getNearestDc' })
await tg.close()
assertEquals(typeof config, 'object')
assertEquals(config._, 'nearestDc')
assertEquals(config.thisDc, 2)
})
})

View file

@ -0,0 +1,73 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { assertEquals } from 'https://deno.land/std@0.223.0/assert/mod.ts'
import { Long } from '@mtcute/core'
import { setPlatform } from '@mtcute/core/platform.js'
import { TlBinaryReader, TlBinaryWriter, TlSerializationCounter } from '@mtcute/tl-runtime'
import { WebPlatform } from '@mtcute/web'
// here we primarily want to check that everything imports properly,
// and that the code is actually executable. The actual correctness
// of the implementation is covered tested by unit tests
const p = new WebPlatform()
setPlatform(p)
Deno.test('encodings', () => {
assertEquals(p.hexEncode(new Uint8Array([1, 2, 3, 4, 5])), '0102030405')
})
Deno.test('TlBinaryReader', () => {
const map = {
'85337187': function (r: any) {
const ret: any = {}
ret._ = 'mt_resPQ'
ret.nonce = r.int128()
ret.serverNonce = r.int128()
ret.pq = r.bytes()
ret.serverPublicKeyFingerprints = r.vector(r.long)
return ret
},
}
const data =
'000000000000000001c8831ec97ae55140000000632416053e0549828cca27e966b301a48fece2fca5cf4d33f4a11ea877ba4aa5739073300817ed48941a08f98100000015c4b51c01000000216be86c022bb4c3'
const buf = p.hexDecode(data)
const r = new TlBinaryReader(map, buf, 8)
assertEquals(r.long().toString(16), '51e57ac91e83c801')
assertEquals(r.uint(), 64)
const obj: any = r.object()
assertEquals(obj._, 'mt_resPQ')
})
Deno.test('TlBinaryWriter', () => {
const map = {
mt_resPQ: function (w: any, obj: any) {
w.uint(85337187)
w.bytes(obj.pq)
w.vector(w.long, obj.serverPublicKeyFingerprints)
},
_staticSize: {} as any,
}
const obj = {
_: 'mt_resPQ',
pq: p.hexDecode('17ED48941A08F981'),
serverPublicKeyFingerprints: [Long.fromString('c3b42b026ce86b21', 16)],
}
assertEquals(TlSerializationCounter.countNeededBytes(map, obj), 32)
const w = TlBinaryWriter.alloc(map, 48)
w.long(Long.ZERO)
w.long(Long.fromString('51E57AC91E83C801', true, 16)) // messageId
w.object(obj)
assertEquals(
p.hexEncode(w.result()),
'000000000000000001c8831ec97ae551632416050817ed48941a08f98100000015c4b51c01000000216be86c022bb4c3',
)
})

View file

@ -0,0 +1,43 @@
import { assertEquals } from 'https://deno.land/std@0.223.0/assert/mod.ts'
import { Long } from '@mtcute/core'
import { setPlatform } from '@mtcute/core/platform.js'
import { tl } from '@mtcute/tl'
import { __tlReaderMap } from '@mtcute/tl/binary/reader.js'
import { __tlWriterMap } from '@mtcute/tl/binary/writer.js'
import { TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime'
import { WebPlatform } from '@mtcute/web'
// here we primarily want to check that @mtcute/tl correctly works with @mtcute/tl-runtime
const p = new WebPlatform()
setPlatform(p)
Deno.test('@mtcute/tl', async (t) => {
await t.step('writers map works with TlBinaryWriter', () => {
const obj = {
_: 'inputPeerUser',
userId: 123,
accessHash: Long.fromNumber(456),
}
assertEquals(
p.hexEncode(TlBinaryWriter.serializeObject(__tlWriterMap, obj)),
'4ca5e8dd7b00000000000000c801000000000000',
)
})
await t.step('readers map works with TlBinaryReader', () => {
const buf = p.hexDecode('4ca5e8dd7b00000000000000c801000000000000')
// eslint-disable-next-line
const obj = TlBinaryReader.deserializeObject<any>(__tlReaderMap, buf)
assertEquals(obj._, 'inputPeerUser')
assertEquals(obj.userId, 123)
assertEquals(obj.accessHash.toString(), '456')
})
await t.step('correctly checks for combinator types', () => {
assertEquals(tl.isAnyInputUser({ _: 'inputUserEmpty' }), true)
})
})

View file

@ -0,0 +1,25 @@
import { assertEquals } from 'https://deno.land/std@0.223.0/assert/mod.ts'
import { ige256Decrypt, ige256Encrypt } from '@mtcute/wasm'
import { WebCryptoProvider, WebPlatform } from '@mtcute/web'
await new WebCryptoProvider().initialize()
const platform = new WebPlatform()
Deno.test('@mtcute/wasm', async (t) => {
const key = platform.hexDecode('5468697320697320616E20696D706C655468697320697320616E20696D706C65')
const iv = platform.hexDecode('6D656E746174696F6E206F6620494745206D6F646520666F72204F70656E5353')
const data = platform.hexDecode('99706487a1cde613bc6de0b6f24b1c7aa448c8b9c3403e3467a8cad89340f53b')
const dataEnc = platform.hexDecode('792ea8ae577b1a66cb3bd92679b8030ca54ee631976bd3a04547fdcb4639fa69')
await t.step('should work with Buffers', () => {
assertEquals(ige256Encrypt(data, key, iv), dataEnc)
assertEquals(ige256Decrypt(dataEnc, key, iv), data)
})
await t.step('should work with Uint8Arrays', () => {
assertEquals(ige256Encrypt(data, key, iv), dataEnc)
assertEquals(ige256Decrypt(dataEnc, key, iv), data)
})
})

42
e2e/deno/utils.ts Normal file
View file

@ -0,0 +1,42 @@
import { MaybePromise, MemoryStorage } from '@mtcute/core'
import { setPlatform } from '@mtcute/core/platform.js'
import { LogManager, sleep } from '@mtcute/core/utils.js'
import { WebCryptoProvider, WebPlatform, WebSocketTransport } from '@mtcute/web'
export const getApiParams = (storage?: string) => {
if (storage) throw new Error('unsupported yet')
if (!Deno.env.has('API_ID') || !Deno.env.has('API_HASH')) {
throw new Error('API_ID and API_HASH env variables must be set')
}
setPlatform(new WebPlatform())
return {
apiId: parseInt(Deno.env.get('API_ID')!),
apiHash: Deno.env.get('API_HASH')!,
testMode: true,
storage: new MemoryStorage(),
logLevel: LogManager.VERBOSE,
transport: () => new WebSocketTransport(),
crypto: new WebCryptoProvider(),
}
}
export async function waitFor(condition: () => MaybePromise<void>, timeout = 5000): Promise<void> {
const start = Date.now()
let lastError
while (Date.now() - start < timeout) {
try {
await condition()
return
} catch (e) {
lastError = e
await sleep(100)
}
}
throw lastError
}

5
e2e/node/.env.example Normal file
View file

@ -0,0 +1,5 @@
# obtain these values from my.telegram.org
API_ID=
API_HASH=
GITHUB_TOKEN=

View file

@ -5,7 +5,7 @@ RUN apk add python3 make g++ && \
corepack enable && \
corepack prepare pnpm@8.7.1 --activate
COPY ../ /app/
COPY ../.. /app/
RUN pnpm install --frozen-lockfile && \
pnpm -C packages/tl run gen-code && \

View file

@ -12,8 +12,8 @@ services:
- mtcute-e2e
build:
build:
context: ..
dockerfile: e2e/Dockerfile.build
context: ../..
dockerfile: e2e/node/Dockerfile.build
environment:
- GITHUB_TOKEN=${GITHUB_TOKEN}
networks:

View file

@ -18,7 +18,9 @@ execSync(`npm config set //${REGISTRY.replace(/^https?:\/\//, '')}/:_authToken $
const commit = CURRENT_COMMIT.slice(0, 7)
const myPkgJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'))
const packages = Object.keys(myPkgJson.dependencies).filter((x) => x.startsWith('@mtcute/')).map((x) => x.slice('@mtcute/'.length))
const packages = Object.keys(myPkgJson.dependencies)
.filter((x) => x.startsWith('@mtcute/'))
.map((x) => x.slice('@mtcute/'.length))
const workDir = path.join(__dirname, 'temp')
fs.mkdirSync(workDir, { recursive: true })

View file

@ -35,6 +35,7 @@
"devDependencies": {
"@commitlint/cli": "17.6.5",
"@commitlint/config-conventional": "17.6.5",
"@teidesu/slow-types-compiler": "1.0.2",
"@types/node": "20.10.0",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "6.4.0",
@ -62,8 +63,8 @@
"semver": "7.5.1",
"ts-node": "10.9.1",
"tsconfig-paths": "4.2.0",
"typedoc": "0.25.3",
"typescript": "5.1.6",
"typedoc": "0.25.12",
"typescript": "5.4.3",
"vite": "5.1.6",
"vite-plugin-node-polyfills": "0.21.0",
"vitest": "1.4.0"

View file

@ -16,12 +16,6 @@
".": "./src/index.ts",
"./utils.js": "./src/utils.ts"
},
"distOnlyFields": {
"exports": {
".": "./index.js",
"./utils.js": "./utils.js"
}
},
"dependencies": {
"@mtcute/core": "workspace:^",
"@mtcute/wasm": "workspace:^",

View file

@ -11,14 +11,7 @@
"scripts": {
"build": "pnpm run -w build-package convert"
},
"distOnlyFields": {
"exports": {
".": {
"import": "./esm/index.js",
"require": "./cjs/index.js"
}
}
},
"exports": "./src/index.ts",
"dependencies": {
"@mtcute/core": "workspace:^"
},

View file

@ -1,20 +1,27 @@
const KNOWN_DECORATORS = ['memoizeGetters', 'makeInspectable']
module.exports = ({ path, glob, transformFile, packageDir, outDir }) => ({
module.exports = ({ path, glob, transformFile, packageDir, outDir, jsr }) => ({
esmOnlyDirectives: true,
esmImportDirectives: true,
final() {
const version = require(path.join(packageDir, 'package.json')).version
const replaceVersion = (content) => content.replace('%VERSION%', version)
transformFile(path.join(outDir, 'cjs/network/network-manager.js'), replaceVersion)
transformFile(path.join(outDir, 'esm/network/network-manager.js'), replaceVersion)
if (jsr) {
transformFile(path.join(outDir, 'network/network-manager.ts'), replaceVersion)
} else {
transformFile(path.join(outDir, 'cjs/network/network-manager.js'), replaceVersion)
transformFile(path.join(outDir, 'esm/network/network-manager.js'), replaceVersion)
}
if (jsr) return
// make decorators properly tree-shakeable
// very fragile, but it works for now :D
// skip for jsr for now because types aren't resolved correctly and it breaks everything (TODO: fix this)
const decoratorsRegex = new RegExp(
`(${KNOWN_DECORATORS.join('|')})\\((.+?)\\);`,
'gs',
`(${KNOWN_DECORATORS.join('|')})\\((.+?)\\)(?:;|$)`,
'gsm',
)
const replaceDecorators = (content, file) => {
@ -57,7 +64,9 @@ module.exports = ({ path, glob, transformFile, packageDir, outDir }) => ({
return content + '\n' + customExports.join('\n') + '\n'
}
for (const f of glob.sync(path.join(outDir, 'esm/highlevel/types/**/*.js'))) {
const globSrc = path.join(outDir, jsr ? 'highlevel/types/**/*.ts' : 'esm/highlevel/types/**/*.js')
for (const f of glob.sync(globSrc)) {
transformFile(f, replaceDecorators)
}
},

View file

@ -21,34 +21,6 @@
"./methods.js": "./src/highlevel/methods.ts",
"./platform.js": "./src/platform.ts"
},
"distOnlyFields": {
"exports": {
".": {
"import": "./esm/index.js",
"require": "./cjs/index.js"
},
"./utils.js": {
"import": "./esm/utils/index.js",
"require": "./cjs/utils/index.js"
},
"./methods.js": {
"import": "./esm/highlevel/methods.js",
"require": "./cjs/highlevel/methods.js"
},
"./platform.js": {
"import": "./esm/platform.js",
"require": "./cjs/platform.js"
},
"./client.js": {
"import": "./esm/highlevel/client.js",
"require": "./cjs/highlevel/client.js"
},
"./worker.js": {
"import": "./esm/highlevel/worker/index.js",
"require": "./cjs/highlevel/worker/index.js"
}
}
},
"dependencies": {
"@mtcute/tl": "workspace:^",
"@mtcute/tl-runtime": "workspace:^",

View file

@ -33,7 +33,18 @@ export class BaseTelegramClient implements ITelegramClient {
private _serverUpdatesHandler: (updates: tl.TypeUpdates) => void = () => {}
private _connectionStateHandler: (state: ConnectionState) => void = () => {}
readonly log
readonly mt
readonly crypto
readonly storage
constructor(readonly params: BaseTelegramClientOptions) {
this.log = this.params.logger ?? new LogManager('client')
this.mt = new MtClient({
...this.params,
logger: this.log.create('mtproto'),
})
if (!params.disableUpdates && params.updates !== false) {
this.updates = new UpdatesManager(this, params.updates)
this._serverUpdatesHandler = this.updates.handleUpdate.bind(this.updates)
@ -56,18 +67,13 @@ export class BaseTelegramClient implements ITelegramClient {
this._connectionStateHandler('offline')
}
})
}
readonly log = this.params.logger ?? new LogManager('client')
readonly mt = new MtClient({
...this.params,
logger: this.log.create('mtproto'),
})
readonly crypto = this.mt.crypto
readonly storage = new TelegramStorageManager(this.mt.storage, {
provider: this.params.storage,
...this.params.storageOptions,
})
this.crypto = this.mt.crypto
this.storage = new TelegramStorageManager(this.mt.storage, {
provider: this.params.storage,
...this.params.storageOptions,
})
}
readonly appConfig = new AppConfigManager(this)
private _prepare = asyncResettable(async () => {

View file

@ -2285,7 +2285,10 @@ export interface TelegramClient extends ITelegramClient {
*
* @param params File download parameters
*/
downloadAsNodeStream(location: FileDownloadLocation, params?: FileDownloadParameters): import('stream').Readable
downloadAsNodeStream(
location: FileDownloadLocation,
params?: FileDownloadParameters,
): import('node:stream').Readable
/**
* Download a file and return it as a readable stream,
* streaming file contents.

View file

@ -66,7 +66,7 @@ export async function startTest(
let dcId = await client.getPrimaryDcId()
if (params.dcId) {
if (!availableDcs.find((dc) => dc.id === params!.dcId)) {
if (!availableDcs.find((dc) => dc.id === params.dcId)) {
throw new MtArgumentError(`DC ID is invalid (${dcId})`)
}
dcId = params.dcId
@ -85,7 +85,7 @@ export async function startTest(
code: () => code,
codeSentCallback: (sent) => {
for (let i = 0; i < sent.length; i++) {
code += phone![5]
code += phone[5]
}
},
})

View file

@ -13,4 +13,4 @@ declare function downloadAsNodeStream(
client: ITelegramClient,
location: FileDownloadLocation,
params?: FileDownloadParameters,
): import('stream').Readable
): import('node:stream').Readable

View file

@ -1,3 +1,4 @@
import { ServiceOptions } from '../../storage/service/base.js'
import { StorageManager } from '../../storage/storage.js'
import { PublicPart } from '../../types/utils.js'
import { ITelegramStorageProvider } from './provider.js'
@ -17,26 +18,40 @@ export interface TelegramStorageManagerExtraOptions {
}
export class TelegramStorageManager {
private provider
readonly updates
readonly self: PublicPart<CurrentUserService>
readonly refMsgs
readonly peers: PublicPart<PeersService>
constructor(
private mt: StorageManager,
private options: TelegramStorageManagerOptions & TelegramStorageManagerExtraOptions,
) {}
) {
this.provider = this.options.provider
private provider = this.options.provider
const serviceOptions: ServiceOptions = {
driver: this.mt.driver,
readerMap: this.mt.options.readerMap,
writerMap: this.mt.options.writerMap,
log: this.mt.log,
}
readonly updates = new UpdatesStateService(this.provider.kv, this.mt._serviceOptions)
readonly self: PublicPart<CurrentUserService> = new CurrentUserService(this.provider.kv, this.mt._serviceOptions)
readonly refMsgs = new RefMessagesService(
this.options.refMessages ?? {},
this.provider.refMessages,
this.mt._serviceOptions,
)
readonly peers: PublicPart<PeersService> = new PeersService(
this.options.peers ?? {},
this.provider.peers,
this.refMsgs,
this.mt._serviceOptions,
)
this.updates = new UpdatesStateService(this.provider.kv, serviceOptions)
this.self = new CurrentUserService(this.provider.kv, serviceOptions)
this.refMsgs = new RefMessagesService(
this.options.refMessages ?? {},
this.provider.refMessages,
serviceOptions,
)
this.peers = new PeersService(
this.options.peers ?? {},
this.provider.peers,
this.refMsgs,
serviceOptions,
)
}
async clear(withAuthKeys = false) {
await this.provider.peers.deleteAll()

View file

@ -1,5 +1,5 @@
/* eslint-disable no-restricted-imports */
import type { ReadStream } from 'fs'
import type { ReadStream } from 'node:fs'
import { tdFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl'

View file

@ -12,13 +12,14 @@ import { StoriesStealthMode } from './stealth-mode.js'
* Returned by {@link TelegramClient.getAllStories}
*/
export class AllStories {
/** Peers index */
readonly _peers
constructor(
/** Raw TL object */
readonly raw: tl.stories.RawAllStories,
) {}
/** Peers index */
readonly _peers = PeersIndex.from(this.raw)
) {
this._peers = PeersIndex.from(this.raw)
}
/** Whether there are more stories to fetch */
get hasMore(): boolean {

View file

@ -129,9 +129,10 @@ export class StoryRepost {
* List of story viewers.
*/
export class StoryViewersList {
constructor(readonly raw: tl.stories.RawStoryViewsList) {}
readonly _peers = PeersIndex.from(this.raw)
readonly _peers: PeersIndex
constructor(readonly raw: tl.stories.RawStoryViewsList) {
this._peers = PeersIndex.from(this.raw)
}
/** Next offset for pagination */
get next(): string | undefined {

View file

@ -100,7 +100,7 @@ export class UpdatesManager {
pendingQtsUpdatesPostponed = new SortedLinkedList<PendingUpdate>((a, b) => a.qtsBefore! - b.qtsBefore!)
pendingUnorderedUpdates = new Deque<PendingUpdate>()
noDispatchEnabled = !this.params.disableNoDispatch
noDispatchEnabled
// channel id or 0 => msg id
noDispatchMsg = new Map<number, Set<number>>()
// channel id or 0 => pts
@ -128,14 +128,14 @@ export class UpdatesManager {
// whether to catch up channels from the locally stored pts
catchingUp = false
catchUpOnStart = this.params.catchUp ?? false
catchUpOnStart
cpts = new Map<number, number>()
cptsMod = new Map<number, number>()
channelDiffTimeouts = new Map<number, NodeJS.Timeout>()
channelsOpened = new Map<number, number>()
log = this.client.log.create('updates')
log
private _handler: RawUpdateHandler = () => {}
private _onCatchingUp: (catchingUp: boolean) => void = () => {}
@ -157,6 +157,9 @@ export class UpdatesManager {
this.hasTimedoutPostponed = true
this.updatesLoopCv.notify()
})
this.log = client.log.create('updates')
this.catchUpOnStart = params.catchUp ?? false
this.noDispatchEnabled = !params.disableNoDispatch
}
setHandler(handler: RawUpdateHandler): void {

View file

@ -32,27 +32,28 @@ export function isProbablyPlainText(buf: Uint8Array): boolean {
}
// from https://github.com/telegramdesktop/tdesktop/blob/bec39d89e19670eb436dc794a8f20b657cb87c71/Telegram/SourceFiles/ui/image/image.cpp#L225
const JPEG_HEADER = () => getPlatform().hexDecode(
'ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e1928' +
'2321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a0aad' +
'aad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2d2d3c35' +
'3c76414176f8a58ca5f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f' +
'8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8ffc0001108000000' +
'0003012200021101031101ffc4001f0000010501010101010100000000000000000' +
'102030405060708090a0bffc400b5100002010303020403050504040000017d0102' +
'0300041105122131410613516107227114328191a1082342b1c11552d1f02433627' +
'282090a161718191a25262728292a3435363738393a434445464748494a53545556' +
'5758595a636465666768696a737475767778797a838485868788898a92939495969' +
'798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4' +
'd5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf1f2f3f4f5f6f7f8f9faffc4001f0100030' +
'101010101010101010000000000000102030405060708090a0bffc400b511000201' +
'0204040304070504040001027700010203110405213106124151076171132232810' +
'8144291a1b1c109233352f0156272d10a162434e125f11718191a262728292a3536' +
'3738393a434445464748494a535455565758595a636465666768696a73747576777' +
'8797a82838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5' +
'b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f' +
'3f4f5f6f7f8f9faffda000c03010002110311003f00',
)
const JPEG_HEADER = () =>
getPlatform().hexDecode(
'ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e1928' +
'2321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a0aad' +
'aad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2d2d3c35' +
'3c76414176f8a58ca5f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f' +
'8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8ffc0001108000000' +
'0003012200021101031101ffc4001f0000010501010101010100000000000000000' +
'102030405060708090a0bffc400b5100002010303020403050504040000017d0102' +
'0300041105122131410613516107227114328191a1082342b1c11552d1f02433627' +
'282090a161718191a25262728292a3435363738393a434445464748494a53545556' +
'5758595a636465666768696a737475767778797a838485868788898a92939495969' +
'798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4' +
'd5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf1f2f3f4f5f6f7f8f9faffc4001f0100030' +
'101010101010101010000000000000102030405060708090a0bffc400b511000201' +
'0204040304070504040001027700010203110405213106124151076171132232810' +
'8144291a1b1c109233352f0156272d10a162434e125f11718191a262728292a3536' +
'3738393a434445464748494a535455565758595a636465666768696a73747576777' +
'8797a82838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5' +
'b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f' +
'3f4f5f6f7f8f9faffda000c03010002110311003f00',
)
let JPEG_HEADER_BYTES: Uint8Array | null = null
const JPEG_FOOTER = new Uint8Array([0xff, 0xd9])
@ -131,5 +132,5 @@ export function svgPathToFile(path: string): Uint8Array {
export function extractFileName(path: string): string {
if (path.startsWith('file:')) path = path.slice(5)
return path.split(/[\\/]/).pop()!
return path.split(/[\\/]/).pop()!.split('?')[0]
}

View file

@ -95,7 +95,7 @@ export function batchedQuery<T, U, K extends string | number>(params: {
}
return new Promise<U | null>((resolve, reject) => {
arr!.push([item, resolve, reject])
arr.push([item, resolve, reject])
})
}

View file

@ -3,10 +3,13 @@ import { AppConfigManager } from '../managers/app-config-manager.js'
import { WorkerInvoker } from './invoker.js'
export class AppConfigManagerProxy implements PublicPart<AppConfigManager> {
constructor(readonly invoker: WorkerInvoker) {}
readonly get: AppConfigManager['get']
readonly getField
private _bind = this.invoker.makeBinder<AppConfigManager>('app-config')
constructor(readonly invoker: WorkerInvoker) {
const bind = invoker.makeBinder<AppConfigManager>('app-config')
readonly get = this._bind('get')
readonly getField = this._bind('getField')
this.get = bind('get')
this.getField = bind('getField')
}
}

View file

@ -14,12 +14,68 @@ export interface TelegramWorkerPortOptions {
}
export abstract class TelegramWorkerPort<Custom extends WorkerCustomMethods> implements ITelegramClient {
constructor(readonly options: TelegramWorkerPortOptions) {}
readonly log
private _connection
private _invoker
readonly storage
readonly appConfig
// bound methods
readonly prepare
private _connect
readonly close
readonly notifyLoggedIn
readonly notifyLoggedOut
readonly notifyChannelOpened
readonly notifyChannelClosed
readonly call
readonly importSession
readonly exportSession
readonly handleClientUpdate
readonly getApiCrenetials
readonly getPoolSize
readonly getPrimaryDcId
readonly computeSrpParams
readonly computeNewPasswordHash
readonly startUpdatesLoop
readonly stopUpdatesLoop
constructor(readonly options: TelegramWorkerPortOptions) {
this.log = new LogManager('worker')
this._connection = this.connectToWorker(this.options.worker, this._onMessage)
this._invoker = new WorkerInvoker(this._connection[0])
this.storage = new TelegramStorageProxy(this._invoker)
this.appConfig = new AppConfigManagerProxy(this._invoker)
const bind = this._invoker.makeBinder<ITelegramClient>('client')
this.prepare = bind('prepare')
this._connect = bind('connect')
this.close = bind('close')
this.notifyLoggedIn = bind('notifyLoggedIn')
this.notifyLoggedOut = bind('notifyLoggedOut')
this.notifyChannelOpened = bind('notifyChannelOpened')
this.notifyChannelClosed = bind('notifyChannelClosed')
this.call = bind('call')
this.importSession = bind('importSession')
this.exportSession = bind('exportSession')
this.handleClientUpdate = bind('handleClientUpdate', true)
this.getApiCrenetials = bind('getApiCrenetials')
this.getPoolSize = bind('getPoolSize')
this.getPrimaryDcId = bind('getPrimaryDcId')
this.computeSrpParams = bind('computeSrpParams')
this.computeNewPasswordHash = bind('computeNewPasswordHash')
this.startUpdatesLoop = bind('startUpdatesLoop')
this.stopUpdatesLoop = bind('stopUpdatesLoop')
}
abstract connectToWorker(worker: SomeWorker, handler: ClientMessageHandler): [SendFn, () => void]
readonly log = new LogManager('worker')
private _serverUpdatesHandler: (updates: tl.TypeUpdates) => void = () => {}
onServerUpdate(handler: (updates: tl.TypeUpdates) => void): void {
this._serverUpdatesHandler = handler
@ -69,13 +125,6 @@ export abstract class TelegramWorkerPort<Custom extends WorkerCustomMethods> imp
}
}
private _connection = this.connectToWorker(this.options.worker, this._onMessage)
private _invoker = new WorkerInvoker(this._connection[0])
private _bind = this._invoker.makeBinder<ITelegramClient>('client')
readonly storage = new TelegramStorageProxy(this._invoker)
readonly appConfig = new AppConfigManagerProxy(this._invoker)
private _destroyed = false
destroy(terminate = false): void {
if (this._destroyed) return
@ -91,26 +140,8 @@ export abstract class TelegramWorkerPort<Custom extends WorkerCustomMethods> imp
return this._invoker.invoke('custom', method as string, args) as Promise<ReturnType<Custom[T]>>
}
readonly prepare = this._bind('prepare')
private _connect = this._bind('connect')
async connect(): Promise<void> {
await this._connect()
await this.storage.self.fetch() // force cache self locally
}
readonly close = this._bind('close')
readonly notifyLoggedIn = this._bind('notifyLoggedIn')
readonly notifyLoggedOut = this._bind('notifyLoggedOut')
readonly notifyChannelOpened = this._bind('notifyChannelOpened')
readonly notifyChannelClosed = this._bind('notifyChannelClosed')
readonly call = this._bind('call')
readonly importSession = this._bind('importSession')
readonly exportSession = this._bind('exportSession')
readonly handleClientUpdate = this._bind('handleClientUpdate', true)
readonly getApiCrenetials = this._bind('getApiCrenetials')
readonly getPoolSize = this._bind('getPoolSize')
readonly getPrimaryDcId = this._bind('getPrimaryDcId')
readonly computeSrpParams = this._bind('computeSrpParams')
readonly computeNewPasswordHash = this._bind('computeNewPasswordHash')
readonly startUpdatesLoop = this._bind('startUpdatesLoop')
readonly stopUpdatesLoop = this._bind('stopUpdatesLoop')
}

View file

@ -1,4 +1,4 @@
import type { Worker as NodeWorker } from 'worker_threads'
import type { Worker as NodeWorker } from 'node:worker_threads'
import { tl } from '@mtcute/tl'

View file

@ -8,25 +8,32 @@ import { TelegramStorageManager } from '../storage/storage.js'
import { WorkerInvoker } from './invoker.js'
class CurrentUserServiceProxy implements PublicPart<CurrentUserService> {
constructor(private _invoker: WorkerInvoker) {}
private _bind = this._invoker.makeBinder<CurrentUserService>('storage-self')
private _store
private _storeFrom
private _fetch
private _update
constructor(invoker: WorkerInvoker) {
const bind = invoker.makeBinder<CurrentUserService>('storage-self')
this._store = bind('store')
this._storeFrom = bind('storeFrom')
this._fetch = bind('fetch')
this._update = bind('update')
}
private _cached?: CurrentUserInfo | null
private _store = this._bind('store')
async store(info: CurrentUserInfo | null): Promise<void> {
await this._store(info)
this._cached = info
}
private _storeFrom = this._bind('storeFrom')
async storeFrom(user: tl.TypeUser): Promise<CurrentUserInfo> {
this._cached = await this._storeFrom(user)
return this._cached
}
private _fetch = this._bind('fetch')
async fetch(): Promise<CurrentUserInfo | null> {
if (this._cached) return this._cached
@ -45,7 +52,6 @@ class CurrentUserServiceProxy implements PublicPart<CurrentUserService> {
return this._cached
}
private _update = this._bind('update')
async update(params: Parameters<CurrentUserService['update']>[0]): Promise<void> {
await this._update(params)
this._cached = await this._fetch()
@ -53,28 +59,41 @@ class CurrentUserServiceProxy implements PublicPart<CurrentUserService> {
}
class PeersServiceProxy implements PublicPart<PeersService> {
constructor(private _invoker: WorkerInvoker) {}
private _bind = this._invoker.makeBinder<PeersService>('storage-peers')
readonly updatePeersFrom
readonly store
readonly getById
readonly getByPhone
readonly getByUsername
readonly getCompleteById
readonly updatePeersFrom = this._bind('updatePeersFrom')
readonly store = this._bind('store')
readonly getById = this._bind('getById')
readonly getByPhone = this._bind('getByPhone')
readonly getByUsername = this._bind('getByUsername')
readonly getCompleteById = this._bind('getCompleteById')
constructor(private _invoker: WorkerInvoker) {
const bind = this._invoker.makeBinder<PeersService>('storage-peers')
this.updatePeersFrom = bind('updatePeersFrom')
this.store = bind('store')
this.getById = bind('getById')
this.getByPhone = bind('getByPhone')
this.getByUsername = bind('getByUsername')
this.getCompleteById = bind('getCompleteById')
}
}
export class TelegramStorageProxy implements PublicPart<TelegramStorageManager> {
constructor(private _invoker: WorkerInvoker) {}
readonly self
readonly peers
private _bind = this._invoker.makeBinder<TelegramStorageManager>('storage')
readonly clear
constructor(private _invoker: WorkerInvoker) {
const bind = this._invoker.makeBinder<TelegramStorageManager>('storage')
this.self = new CurrentUserServiceProxy(this._invoker)
this.peers = new PeersServiceProxy(this._invoker)
this.clear = bind('clear')
}
// todo - remove once we move these to updates manager
readonly updates = null as never
readonly refMsgs = null as never
readonly self = new CurrentUserServiceProxy(this._invoker)
readonly peers = new PeersServiceProxy(this._invoker)
readonly clear = this._bind('clear')
}

View file

@ -351,7 +351,7 @@ export class MtClient extends EventEmitter {
*/
async close(): Promise<void> {
this._config.destroy()
this.network.destroy()
await this.network.destroy()
await this.storage.save()
await this.storage.destroy?.()

View file

@ -91,9 +91,9 @@ export type PendingMessage =
export class MtprotoSession {
_sessionId = randomLong()
_authKey = new AuthKey(this._crypto, this.log, this._readerMap)
_authKeyTemp = new AuthKey(this._crypto, this.log, this._readerMap)
_authKeyTempSecondary = new AuthKey(this._crypto, this.log, this._readerMap)
_authKey: AuthKey
_authKeyTemp: AuthKey
_authKeyTempSecondary: AuthKey
_timeOffset = 0
_lastMessageId = Long.ZERO
@ -139,6 +139,10 @@ export class MtprotoSession {
readonly _salts: ServerSaltManager,
) {
this.log.prefix = `[SESSION ${this._sessionId.toString(16)}] `
this._authKey = new AuthKey(_crypto, log, _readerMap)
this._authKeyTemp = new AuthKey(_crypto, log, _readerMap)
this._authKeyTempSecondary = new AuthKey(_crypto, log, _readerMap)
}
get hasPendingMessages(): boolean {

View file

@ -116,7 +116,9 @@ export class MultiSessionConnection extends EventEmitter {
// destroy extra connections
for (let i = this._connections.length - 1; i >= this._count; i--) {
this._connections[i].removeAllListeners()
this._connections[i].destroy()
this._connections[i].destroy().catch((err) => {
this._log.warn('error destroying connection: %s', err)
})
}
this._connections.splice(this._count)
@ -199,8 +201,8 @@ export class MultiSessionConnection extends EventEmitter {
}
_destroyed = false
destroy(): void {
this._connections.forEach((conn) => conn.destroy())
async destroy(): Promise<void> {
await Promise.all(this._connections.map((conn) => conn.destroy()))
this._sessions.forEach((sess) => sess.reset())
this.removeAllListeners()

View file

@ -188,53 +188,16 @@ export interface RpcCallOptions {
*/
export class DcConnectionManager {
private _salts = new ServerSaltManager()
private __baseConnectionParams = (): SessionConnectionParams => ({
crypto: this.manager.params.crypto,
initConnection: this.manager._initConnectionParams,
transportFactory: this.manager._transportFactory,
dc: this._dcs.media,
testMode: this.manager.params.testMode,
reconnectionStrategy: this.manager._reconnectionStrategy,
layer: this.manager.params.layer,
disableUpdates: this.manager.params.disableUpdates,
readerMap: this.manager.params.readerMap,
writerMap: this.manager.params.writerMap,
usePfs: this.manager.params.usePfs,
isMainConnection: false,
isMainDcConnection: this.isPrimary,
inactivityTimeout: this.manager.params.inactivityTimeout ?? 60_000,
enableErrorReporting: this.manager.params.enableErrorReporting,
salts: this._salts,
})
private _log = this.manager._log.create('dc-manager')
private _log
/** Main connection pool */
main: MultiSessionConnection
/** Upload connection pool */
upload = new MultiSessionConnection(
this.__baseConnectionParams(),
this.manager._connectionCount('upload', this.dcId, this.manager.params.isPremium),
this._log,
'UPLOAD',
)
upload: MultiSessionConnection
/** Download connection pool */
download = new MultiSessionConnection(
this.__baseConnectionParams(),
this.manager._connectionCount('download', this.dcId, this.manager.params.isPremium),
this._log,
'DOWNLOAD',
)
download: MultiSessionConnection
/** Download connection pool (for small files) */
downloadSmall = new MultiSessionConnection(
this.__baseConnectionParams(),
this.manager._connectionCount('downloadSmall', this.dcId, this.manager.params.isPremium),
this._log,
'DOWNLOAD_SMALL',
)
downloadSmall: MultiSessionConnection
private get _mainConnectionCount() {
if (!this.isPrimary) return 1
@ -252,9 +215,29 @@ export class DcConnectionManager {
/** Whether this DC is the primary one */
public isPrimary = false,
) {
this._log = this.manager._log.create('dc-manager')
this._log.prefix = `[DC ${dcId}] `
const mainParams = this.__baseConnectionParams()
const baseConnectionParams = (): SessionConnectionParams => ({
crypto: this.manager.params.crypto,
initConnection: this.manager._initConnectionParams,
transportFactory: this.manager._transportFactory,
dc: this._dcs.media,
testMode: this.manager.params.testMode,
reconnectionStrategy: this.manager._reconnectionStrategy,
layer: this.manager.params.layer,
disableUpdates: this.manager.params.disableUpdates,
readerMap: this.manager.params.readerMap,
writerMap: this.manager.params.writerMap,
usePfs: this.manager.params.usePfs,
isMainConnection: false,
isMainDcConnection: this.isPrimary,
inactivityTimeout: this.manager.params.inactivityTimeout ?? 60_000,
enableErrorReporting: this.manager.params.enableErrorReporting,
salts: this._salts,
})
const mainParams = baseConnectionParams()
mainParams.isMainConnection = true
mainParams.dc = _dcs.main
@ -263,6 +246,24 @@ export class DcConnectionManager {
}
this.main = new MultiSessionConnection(mainParams, this._mainConnectionCount, this._log, 'MAIN')
this.upload = new MultiSessionConnection(
baseConnectionParams(),
this.manager._connectionCount('upload', this.dcId, this.manager.params.isPremium),
this._log,
'UPLOAD',
)
this.download = new MultiSessionConnection(
baseConnectionParams(),
this.manager._connectionCount('download', this.dcId, this.manager.params.isPremium),
this._log,
'DOWNLOAD',
)
this.downloadSmall = new MultiSessionConnection(
baseConnectionParams(),
this.manager._connectionCount('downloadSmall', this.dcId, this.manager.params.isPremium),
this._log,
'DOWNLOAD_SMALL',
)
this._setupMulti('main')
this._setupMulti('upload')
@ -412,11 +413,11 @@ export class DcConnectionManager {
return true
}
destroy() {
this.main.destroy()
this.upload.destroy()
this.download.destroy()
this.downloadSmall.destroy()
async destroy() {
await this.main.destroy()
await this.upload.destroy()
await this.download.destroy()
await this.downloadSmall.destroy()
this._salts.destroy()
}
}
@ -425,8 +426,8 @@ export class DcConnectionManager {
* Class that manages all connections to Telegram servers.
*/
export class NetworkManager {
readonly _log = this.params.log.create('network')
readonly _storage = this.params.storage
readonly _log
readonly _storage
readonly _initConnectionParams: tl.RawInitConnectionRequest
readonly _transportFactory: TransportFactory
@ -465,6 +466,9 @@ export class NetworkManager {
this._onConfigChanged = this._onConfigChanged.bind(this)
config.onReload(this._onConfigChanged)
this._log = params.log.create('network')
this._storage = params.storage
}
private async _findDcOptions(dcId: number): Promise<DcOptions> {
@ -857,9 +861,9 @@ export class NetworkManager {
return this._primaryDc.dcId
}
destroy(): void {
async destroy(): Promise<void> {
for (const dc of this._dcConnections.values()) {
dc.destroy()
await dc.destroy()
}
this.config.offReload(this._onConfigChanged)
this._resetOnNetworkChange?.()

View file

@ -69,7 +69,9 @@ export abstract class PersistentConnection extends EventEmitter {
changeTransport(factory: TransportFactory): void {
if (this._transport) {
this._transport.close()
Promise.resolve(this._transport.close()).catch((err) => {
this.log.warn('error closing previous transport: %s', err)
})
}
this._transport = factory()
@ -149,7 +151,9 @@ export abstract class PersistentConnection extends EventEmitter {
)
if (wait === false) {
this.destroy()
this.destroy().catch((err) => {
this.log.warn('error destroying connection: %s', err)
})
return
}
@ -192,7 +196,9 @@ export abstract class PersistentConnection extends EventEmitter {
// if we are already connected
if (this.isConnected) {
this._shouldReconnectImmediately = true
this._transport.close()
Promise.resolve(this._transport.close()).catch((err) => {
this.log.error('error closing transport: %s', err)
})
return
}
@ -201,12 +207,12 @@ export abstract class PersistentConnection extends EventEmitter {
this.connect()
}
disconnectManual(): void {
async disconnectManual(): Promise<void> {
this._disconnectedManually = true
this._transport.close()
await this._transport.close()
}
destroy(): void {
async destroy(): Promise<void> {
if (this._reconnectionTimeout != null) {
clearTimeout(this._reconnectionTimeout)
}
@ -214,7 +220,7 @@ export abstract class PersistentConnection extends EventEmitter {
clearTimeout(this._inactivityTimeout)
}
this._transport.close()
await this._transport.close()
this._transport.removeAllListeners()
this._destroyed = true
}
@ -229,7 +235,9 @@ export abstract class PersistentConnection extends EventEmitter {
this.log.info('disconnected because of inactivity for %d', this.params.inactivityTimeout)
this._inactive = true
this._inactivityTimeout = null
this._transport.close()
Promise.resolve(this._transport.close()).catch((err) => {
this.log.warn('error closing transport: %s', err)
})
}
setInactivityTimeout(timeout?: number): void {

View file

@ -70,7 +70,7 @@ function makeNiceStack(error: tl.RpcError, stack: string, method?: string) {
* A connection to a single DC.
*/
export class SessionConnection extends PersistentConnection {
readonly params!: SessionConnectionParams
declare readonly params: SessionConnectionParams
private _flushTimer = new EarlyTimer()
private _queuedDestroySession: Long[] = []
@ -78,7 +78,7 @@ export class SessionConnection extends PersistentConnection {
// waitForMessage
private _pendingWaitForUnencrypted: [ControllablePromise<Uint8Array>, NodeJS.Timeout][] = []
private _usePfs = this.params.usePfs ?? false
private _usePfs
private _isPfsBindingPending = false
private _isPfsBindingPendingInBackground = false
private _pfsUpdateTimeout?: NodeJS.Timeout
@ -102,9 +102,12 @@ export class SessionConnection extends PersistentConnection {
this._crypto = params.crypto
this._salts = params.salts
this._handleRawMessage = this._handleRawMessage.bind(this)
this._usePfs = this.params.usePfs ?? false
this._online = getPlatform().isOnline?.() ?? true
}
private _online = getPlatform().isOnline?.() ?? true
private _online
getAuthKey(temp = false): Uint8Array | null {
const key = temp ? this._session._authKeyTemp : this._session._authKey
@ -152,8 +155,8 @@ export class SessionConnection extends PersistentConnection {
this.reset()
}
destroy(): void {
super.destroy()
async destroy(): Promise<void> {
await super.destroy()
this.reset(true)
}
@ -1459,7 +1462,9 @@ export class SessionConnection extends PersistentConnection {
if (online) {
this.reconnect()
} else {
this.disconnectManual()
this.disconnectManual().catch((err) => {
this.log.warn('error while disconnecting: %s', err)
})
}
}

View file

@ -48,7 +48,7 @@ export interface ITelegramTransport extends EventEmitter {
*/
connect(dc: BasicDcOption, testMode: boolean): void
/** call to close existing connection to some DC */
close(): void
close(): MaybePromise<void>
/** send a message */
send(data: Uint8Array): Promise<void>

View file

@ -8,13 +8,14 @@ interface AuthKeysState {
}
export class MemoryAuthKeysRepository implements IAuthKeysRepository {
constructor(readonly _driver: MemoryStorageDriver) {}
readonly state = this._driver.getState<AuthKeysState>('authKeys', () => ({
authKeys: new Map(),
authKeysTemp: new Map(),
authKeysTempExpiry: new Map(),
}))
readonly state
constructor(readonly _driver: MemoryStorageDriver) {
this.state = this._driver.getState<AuthKeysState>('authKeys', () => ({
authKeys: new Map(),
authKeysTemp: new Map(),
authKeysTempExpiry: new Map(),
}))
}
set(dc: number, key: Uint8Array | null): void {
if (key) {

View file

@ -2,9 +2,10 @@ import { IKeyValueRepository } from '../../repository/key-value.js'
import { MemoryStorageDriver } from '../driver.js'
export class MemoryKeyValueRepository implements IKeyValueRepository {
constructor(readonly _driver: MemoryStorageDriver) {}
readonly state = this._driver.getState<Map<string, Uint8Array>>('kv', () => new Map())
readonly state
constructor(readonly _driver: MemoryStorageDriver) {
this.state = this._driver.getState<Map<string, Uint8Array>>('kv', () => new Map())
}
set(key: string, value: Uint8Array): void {
this.state.set(key, value)

View file

@ -8,13 +8,14 @@ interface PeersState {
}
export class MemoryPeersRepository implements IPeersRepository {
constructor(readonly _driver: MemoryStorageDriver) {}
readonly state = this._driver.getState<PeersState>('peers', () => ({
entities: new Map(),
usernameIndex: new Map(),
phoneIndex: new Map(),
}))
readonly state
constructor(readonly _driver: MemoryStorageDriver) {
this.state = this._driver.getState<PeersState>('peers', () => ({
entities: new Map(),
usernameIndex: new Map(),
phoneIndex: new Map(),
}))
}
store(peer: IPeersRepository.PeerInfo): void {
const old = this.state.entities.get(peer.id)

View file

@ -6,11 +6,12 @@ interface RefMessagesState {
}
export class MemoryRefMessagesRepository implements IReferenceMessagesRepository {
constructor(readonly _driver: MemoryStorageDriver) {}
readonly state = this._driver.getState<RefMessagesState>('refMessages', () => ({
refs: new Map(),
}))
readonly state
constructor(readonly _driver: MemoryStorageDriver) {
this.state = this._driver.getState<RefMessagesState>('refMessages', () => ({
refs: new Map(),
}))
}
store(peerId: number, chatId: number, msgId: number): void {
if (!this.state.refs.has(peerId)) {

View file

@ -10,10 +10,15 @@ export { BaseSqliteStorageDriver }
export * from './types.js'
export class BaseSqliteStorage implements IMtStorageProvider, ITelegramStorageProvider {
constructor(readonly driver: BaseSqliteStorageDriver) {}
readonly authKeys
readonly kv
readonly refMessages
readonly peers
readonly authKeys = new SqliteAuthKeysRepository(this.driver)
readonly kv = new SqliteKeyValueRepository(this.driver)
readonly refMessages = new SqliteRefMessagesRepository(this.driver)
readonly peers = new SqlitePeersRepository(this.driver)
constructor(readonly driver: BaseSqliteStorageDriver) {
this.authKeys = new SqliteAuthKeysRepository(this.driver)
this.kv = new SqliteKeyValueRepository(this.driver)
this.refMessages = new SqliteRefMessagesRepository(this.driver)
this.peers = new SqlitePeersRepository(this.driver)
}
}

View file

@ -8,8 +8,7 @@ interface PeerDto {
usernames: string
updated: number
phone: string | null
// eslint-disable-next-line no-restricted-globals
complete: Buffer
complete: Uint8Array
}
function mapPeerDto(dto: PeerDto): IPeersRepository.PeerInfo {

View file

@ -1,5 +1,4 @@
import { IReferenceMessagesRepository } from '@mtcute/core'
import { IReferenceMessagesRepository } from '../../../highlevel/storage/repository/ref-messages.js'
import { BaseSqliteStorageDriver } from '../driver.js'
import { ISqliteStatement } from '../types.js'

View file

@ -40,23 +40,30 @@ export interface StorageManagerExtraOptions {
}
export class StorageManager {
constructor(readonly options: StorageManagerOptions & StorageManagerExtraOptions) {}
readonly provider
readonly driver
readonly log
readonly dcs
readonly salts
readonly keys
readonly provider = this.options.provider
readonly driver = this.provider.driver
readonly log = this.options.log.create('storage')
constructor(readonly options: StorageManagerOptions & StorageManagerExtraOptions) {
this.provider = this.options.provider
this.driver = this.provider.driver
this.log = this.options.log.create('storage')
readonly _serviceOptions: ServiceOptions = {
driver: this.driver,
readerMap: this.options.readerMap,
writerMap: this.options.writerMap,
log: this.log,
const serviceOptions: ServiceOptions = {
driver: this.driver,
readerMap: this.options.readerMap,
writerMap: this.options.writerMap,
log: this.log,
}
this.dcs = new DefaultDcsService(this.provider.kv, serviceOptions)
this.salts = new FutureSaltsService(this.provider.kv, serviceOptions)
this.keys = new AuthKeysService(this.provider.authKeys, this.salts, serviceOptions)
}
readonly dcs = new DefaultDcsService(this.provider.kv, this._serviceOptions)
readonly salts = new FutureSaltsService(this.provider.kv, this._serviceOptions)
readonly keys = new AuthKeysService(this.provider.authKeys, this.salts, this._serviceOptions)
private _cleanupRestore?: () => void
private _load = asyncResettable(async () => {

Some files were not shown because too many files have changed in this diff Show more