From dd9c4c4be292724a7d40990491d63f3607fa8ab2 Mon Sep 17 00:00:00 2001 From: Mateusz Russak Date: Mon, 9 Feb 2026 11:32:20 +0100 Subject: [PATCH 1/8] ci: trigger docker build from release workflow (#271) Fix GITHUB_TOKEN limitation: release.yml now calls docker.yml directly via workflow_call so Docker builds are triggered automatically on release. --- .github/workflows/docker.yml | 11 ++++++++++- .github/workflows/release.yml | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e336d26d..78c2a3ad 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -4,6 +4,12 @@ on: push: tags: - v* + workflow_call: + inputs: + version: + required: true + type: string + workflow_dispatch: permissions: contents: write @@ -73,6 +79,9 @@ jobs: uses: docker/metadata-action@v5 with: images: ${{ env.IMAGE_NAME }} + tags: | + type=ref,event=tag + type=raw,value=${{ inputs.version }},enable=${{ inputs.version != '' }} - name: Build and push Docker image id: push @@ -85,7 +94,7 @@ jobs: push: true platforms: linux/amd64,linux/arm64 build-args: | - APP_VERSION=${{ github.ref_name }} + APP_VERSION=${{ inputs.version || github.ref_name }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7daa14d9..b3baf315 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + outputs: + version: ${{ steps.version.outputs.result }} steps: - name: Extract version from labels @@ -47,3 +49,10 @@ jobs: }); core.info(`Created release ${version}`); + + docker: + needs: release + uses: ./.github/workflows/docker.yml + with: + version: ${{ needs.release.outputs.version }} + secrets: inherit From 7c048d36115998b75de3cad7744ca6537ca0fa10 Mon Sep 17 00:00:00 2001 From: Mateusz Russak Date: Mon, 9 Feb 2026 21:59:18 +0100 Subject: [PATCH 2/8] chore(types): remove any types from backend core (#272) Removes ~25 any type annotations from backend core, infra, and inter layers, replacing them with proper types (unknown, Record, BusMessage, JsonWebKey, PromiseLike signatures). Part of the ongoing type safety cleanup. --- BACKLOG.md | 7 +++- deno/server/core/bus.ts | 26 ++++++++----- deno/server/core/command.ts | 35 ++++++----------- deno/server/core/core.ts | 17 +++++++-- deno/server/core/message/create.ts | 9 ++--- deno/server/core/query.ts | 7 +++- deno/server/core/serializer.ts | 15 +++++--- deno/server/core/webhooks.ts | 6 +-- deno/server/infra/repo/channelRepo.ts | 2 +- deno/server/infra/repo/serializer.ts | 38 +++++++++++-------- deno/server/infra/repo/userRepo.ts | 5 ++- deno/server/inter/cli/mod.ts | 12 ++++-- deno/server/inter/http/mod.ts | 2 +- .../inter/http/routes/channel/postChannel.ts | 2 +- .../inter/http/routes/channel/putDirect.ts | 2 +- .../inter/http/routes/messages/create.ts | 2 +- .../inter/http/routes/mobile/notifications.ts | 3 +- deno/server/inter/http/routes/users/create.ts | 2 +- 18 files changed, 112 insertions(+), 80 deletions(-) diff --git a/BACKLOG.md b/BACKLOG.md index 71d9787a..cfcb53ca 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -116,9 +116,12 @@ Create standard story template with: ### 2.1 Type Safety (HIGH PRIORITY) - [ ] Fix pre-existing TypeScript errors (17 errors in base) -- [ ] Remove `any` types - 20 instances in app/src +- [x] Remove `any` types from backend core (serializers, bus, command/query, repos, webhooks) — PR #267 +- [ ] Remove `any` types from storage, encryption, config modules +- [ ] Remove `any` types from API module (`deno/api/`) +- [ ] Remove `any` types from frontend (models, components, contexts) +- [ ] Remove `any` types from test files and helpers - [ ] Fix `style: any` unused prop in `ActionButton.tsx:10` -- [ ] Add proper typing to API module (`deno/api/mod.ts` - `payload: any`, `document: any`) - [ ] Remove `@ts-ignore` comments in `deno/api/files.ts` (5+ instances) - [ ] Standardize Props interface naming diff --git a/deno/server/core/bus.ts b/deno/server/core/bus.ts index a1a04f18..dddf8850 100644 --- a/deno/server/core/bus.ts +++ b/deno/server/core/bus.ts @@ -1,12 +1,14 @@ import { EntityId } from "../types.ts"; import { serialize } from "./serializer.ts"; -type Listeners = { [key: string]: ((...args: any[]) => void)[] }; +type BusMessage = Record; +type BusCallback = (msg: BusMessage) => void; +type Listeners = { [key: string]: BusCallback[] }; class Emitter { listeners: Listeners = {}; - on = (ev: string, cb: (...args: any[]) => void) => { + on = (ev: string, cb: BusCallback) => { this.listeners[ev] = this.listeners[ev] ?? []; this.listeners[ev].push(cb); return () => { @@ -14,10 +16,10 @@ class Emitter { }; }; - emit = (ev: string, ...args: any[]) => { + emit = (ev: string, msg: BusMessage) => { (this.listeners[ev] ?? []).forEach((cb) => { try { - cb(...serialize(args)); + cb(serialize(msg)); } catch (e) { console.error(e); } @@ -45,7 +47,11 @@ export class Bus { {}, ); - group = (userIds: (EntityId | string)[], msg: any, senderId?: EntityId) => { + group = ( + userIds: (EntityId | string)[], + msg: BusMessage, + senderId?: EntityId, + ) => { this.internalBus.emit("notif", { ...msg, _target: "group", @@ -57,7 +63,7 @@ export class Bus { }); }; - direct = (userId: EntityId | string, msg: any) => { + direct = (userId: EntityId | string, msg: BusMessage) => { this.internalBus.emit(userId.toString(), { ...msg, _target: "direct" }); this.internalBus.emit("notif", { ...msg, @@ -66,12 +72,12 @@ export class Bus { }); }; - broadcast = (msg: any) => { + broadcast = (msg: BusMessage) => { this.internalBus.emit("all", { ...msg, _target: "broadcast" }); this.internalBus.emit("notif", { ...msg, _target: "broadcast" }); }; - on = (userId: EntityId | string, cb: (...args: any[]) => void) => { + on = (userId: EntityId | string, cb: BusCallback) => { const a = this.internalBus.on(userId.toString(), cb); const b = this.internalBus.on("all", cb); return () => { @@ -80,8 +86,8 @@ export class Bus { }; }; - notif = (msg: any) => + notif = (msg: BusMessage) => this.internalBus.emit("notif", { ...msg, _target: "notif" }); - onNotif = (cb: (...args: any[]) => void) => this.internalBus.on("notif", cb); + onNotif = (cb: BusCallback) => this.internalBus.on("notif", cb); } diff --git a/deno/server/core/command.ts b/deno/server/core/command.ts index 295cc3ee..e78791f2 100644 --- a/deno/server/core/command.ts +++ b/deno/server/core/command.ts @@ -2,21 +2,7 @@ import * as v from "valibot"; import { EntityId } from "../types.ts"; import type { Core } from "./core.ts"; import { AppError } from "./errors.ts"; - -function serialize(obj: A): any { - if (obj instanceof EntityId) { - return obj.toString(); - } - if (Array.isArray(obj)) { - return obj.map(serialize); - } - if (typeof obj === "object") { - for (const key in obj) { - obj[key] = serialize(obj[key]); - } - } - return obj; -} +import { serialize } from "./serializer.ts"; export type Definition< T extends string, @@ -69,10 +55,12 @@ export function createCommand< internal() { return exec(body, core); }, - then( - onfulfilled: (value: void | EntityId | string | null) => any, - onrejected: (reason: any) => any, - ) { + then( + onfulfilled: ( + value: void | EntityId | string | null, + ) => TResult1 | PromiseLike, + onrejected: (reason: unknown) => TResult2 | PromiseLike, + ): PromiseLike { return exec(body, core).then((r) => serialize(r)).then( onfulfilled, onrejected, @@ -94,10 +82,11 @@ export type CommandDirectory = { export function buildCommandCollection( events: T, ): CommandDirectory { - return events.reduce((acc, curr) => { - acc[curr.type] = curr; - return acc; - }, {} as any); + const result: Record = {}; + for (const curr of events) { + result[curr.type] = curr; + } + return result as CommandDirectory; } export type EventFrom = T extends Command diff --git a/deno/server/core/core.ts b/deno/server/core/core.ts index 6bd74a2e..5b656d36 100644 --- a/deno/server/core/core.ts +++ b/deno/server/core/core.ts @@ -125,9 +125,20 @@ export class Core { } } - dispatch = (evt: EventFrom) => ( - (commands[evt.type] as any).handler(evt.body, this) - ); + dispatch = (evt: EventFrom) => { + // Type assertion needed: evt.type and evt.body are correlated through + // the discriminated union, but TypeScript can't prove it for indexed access + type AnyCommand = typeof commands[keyof typeof commands]; + const cmd = commands[evt.type] as AnyCommand; + // deno-lint-ignore no-explicit-any + return (cmd.handler as ( + body: any, + core: Core, + ) => ReturnType)( + evt.body, + this, + ); + }; close = async () => { this.events.dispatch({ diff --git a/deno/server/core/message/create.ts b/deno/server/core/message/create.ts index 8bdf7001..d3769d69 100644 --- a/deno/server/core/message/create.ts +++ b/deno/server/core/message/create.ts @@ -5,7 +5,7 @@ import { AccessDenied, InvalidMessage, ResourceNotFound } from "../errors.ts"; import { flatten } from "./flatten.ts"; import { ChannelType, EntityId } from "../../types.ts"; -function filterUndefined(data: any) { +function filterUndefined(data: Record) { return Object.fromEntries( Object.entries(data).filter(([, v]) => v !== undefined), ); @@ -79,7 +79,7 @@ export default createCommand({ userId: msg.userId, links: msg.links, mentions: msg.mentions, - attachments: msg.attachments?.map((file: any) => ({ + attachments: msg.attachments?.map((file) => ({ id: file.id, fileName: file.fileName, contentType: file.contentType, @@ -104,9 +104,8 @@ export default createCommand({ type: "channel:join", body: { channelId: msg.channelId.toString(), - userIds: msg.mentions.filter((m: any) => - !channel.users.some((u) => u.eq(m)) - ).map((u) => u.toString()), + userIds: msg.mentions.filter((m) => !channel.users.some((u) => u.eq(m))) + .map((u) => u.toString()), }, }).internal(); diff --git a/deno/server/core/query.ts b/deno/server/core/query.ts index e78a5919..f36563c1 100644 --- a/deno/server/core/query.ts +++ b/deno/server/core/query.ts @@ -6,7 +6,7 @@ import { serialize } from "./serializer.ts"; export type Event = { type: string; - body: any; + body: unknown; }; export type Definition = { type: string; @@ -38,7 +38,10 @@ export function createQuery( async internal() { return await exec(body); }, - then(onfulfilled: (value: B) => any, onrejected: (reason: any) => any) { + then( + onfulfilled: (value: B) => TResult1 | PromiseLike, + onrejected: (reason: unknown) => TResult2 | PromiseLike, + ): PromiseLike { return exec(body).then((ret) => serialize(ret)).then( onfulfilled, onrejected, diff --git a/deno/server/core/serializer.ts b/deno/server/core/serializer.ts index 30692136..3e86d4d4 100644 --- a/deno/server/core/serializer.ts +++ b/deno/server/core/serializer.ts @@ -1,16 +1,21 @@ import { EntityId } from "../types.ts"; -export function serialize(obj: A): any { +function recursiveSerialize(obj: unknown): unknown { if (obj instanceof EntityId) { return obj.toString(); } if (Array.isArray(obj)) { - return obj.map(serialize); + return obj.map(recursiveSerialize); } - if (typeof obj === "object") { - for (const key in obj) { - obj[key] = serialize(obj[key]); + if (typeof obj === "object" && obj !== null) { + const record = obj as Record; + for (const key in record) { + record[key] = recursiveSerialize(record[key]); } } return obj; } + +export function serialize(obj: A): A { + return recursiveSerialize(obj) as A; +} diff --git a/deno/server/core/webhooks.ts b/deno/server/core/webhooks.ts index 11d306a3..b28562a1 100644 --- a/deno/server/core/webhooks.ts +++ b/deno/server/core/webhooks.ts @@ -7,15 +7,15 @@ export class Webhooks { this.core.bus.onNotif(this.handleEvent.bind(this)); } - async handleEvent(event: any) { + async handleEvent(event: Record) { for (const webhook of (this.config?.webhooks ?? [])) { - if (!webhook.events || webhook.events.includes(event.type)) { + if (!webhook.events || webhook.events.includes(event.type as string)) { await this.send(webhook, event); } } } - async send(webhook: Webhook, event: any) { + async send(webhook: Webhook, event: Record) { try { const body = JSON.stringify({ type: event.type, event }); const res = await fetch(webhook.url, { diff --git a/deno/server/infra/repo/channelRepo.ts b/deno/server/infra/repo/channelRepo.ts index b2154f00..42282eba 100644 --- a/deno/server/infra/repo/channelRepo.ts +++ b/deno/server/infra/repo/channelRepo.ts @@ -9,7 +9,7 @@ export class ChannelRepo extends Repo { override makeQuery(data: ChannelQuery) { const { userId, users, usersCount, ...rest } = serialize(data); const query = { ...rest }; - const userQuery: any = {}; + const userQuery: Record = {}; if (userId) userQuery["$elemMatch"] = { $eq: userId }; if (users) userQuery["$all"] = users; if (usersCount) userQuery["$size"] = usersCount; diff --git a/deno/server/infra/repo/serializer.ts b/deno/server/infra/repo/serializer.ts index 0dd1f3ab..6de128c5 100644 --- a/deno/server/infra/repo/serializer.ts +++ b/deno/server/infra/repo/serializer.ts @@ -1,7 +1,8 @@ import { EntityId } from "../../types.ts"; import { ObjectId } from "./db.ts"; -function recursiveDeserialize(obj: any): any { +// deno-lint-ignore no-explicit-any +function recursiveDeserialize(obj: unknown): any { if (obj instanceof EntityId) { return EntityId.from(obj); } @@ -11,41 +12,46 @@ function recursiveDeserialize(obj: any): any { if (Array.isArray(obj)) { return obj.map(recursiveDeserialize); } - if (typeof obj === "object") { - for (const key in obj) { - obj[key] = recursiveDeserialize(obj[key]); + if (typeof obj === "object" && obj !== null) { + const record = obj as Record; + for (const key in record) { + record[key] = recursiveDeserialize(record[key]); } - if (obj && obj._id) { - obj.id = obj._id; - delete obj._id; + if (record._id) { + record.id = record._id; + delete record._id; } } return obj; } -export function deserialize(data: any) { +// deno-lint-ignore no-explicit-any +export function deserialize(data: unknown): any { return recursiveDeserialize(data); } -function recursiveSerialize(obj: any): any { +// deno-lint-ignore no-explicit-any +function recursiveSerialize(obj: unknown): any { if (obj instanceof EntityId) { return new ObjectId(obj.value); } if (Array.isArray(obj)) { return obj.map(recursiveSerialize); } - if (typeof obj === "object") { - for (const key in obj) { - obj[key] = recursiveSerialize(obj[key]); + if (typeof obj === "object" && obj !== null) { + const record = obj as Record; + for (const key in record) { + record[key] = recursiveSerialize(record[key]); } - if (obj && obj.id) { - obj._id = obj.id; - delete obj.id; + if (record.id) { + record._id = record.id; + delete record.id; } } return obj; } -export function serialize(data: any) { +// deno-lint-ignore no-explicit-any +export function serialize(data: unknown): any { return recursiveSerialize(data); } diff --git a/deno/server/infra/repo/userRepo.ts b/deno/server/infra/repo/userRepo.ts index 4d1c4c39..a009f1d4 100644 --- a/deno/server/infra/repo/userRepo.ts +++ b/deno/server/infra/repo/userRepo.ts @@ -23,7 +23,10 @@ export class UserRepo extends Repo { ); } - async upgrade(query: UserQuery, data: { publicKey: any; secrets: Secret }) { + async upgrade( + query: UserQuery, + data: { publicKey: JsonWebKey; secrets: Secret }, + ) { const { db } = await this.connect(); await db.collection(this.COLLECTION) .updateOne( diff --git a/deno/server/inter/cli/mod.ts b/deno/server/inter/cli/mod.ts index 5b87dd19..ab3634bc 100644 --- a/deno/server/inter/cli/mod.ts +++ b/deno/server/inter/cli/mod.ts @@ -3,9 +3,15 @@ import { Command } from "@cliffy/command"; const run = new Command() .arguments("") .description("Runs the chat server.") - .action((options: any, source: string, destination?: string) => { - console.log("clone command called"); - }); + .action( + ( + _options: Record, + source: string, + _destination?: string, + ) => { + console.log("clone command called"); + }, + ); await new Command() .name("chat") diff --git a/deno/server/inter/http/mod.ts b/deno/server/inter/http/mod.ts index 5a5a2ba9..0c029a82 100644 --- a/deno/server/inter/http/mod.ts +++ b/deno/server/inter/http/mod.ts @@ -33,7 +33,7 @@ export class HttpInterface extends Planigale { schema.addKeyword({ keyword: "requireAny", type: "object", - validate: (keys: string[], data: any) => { + validate: (keys: string[], data: Record) => { if (keys.some((key) => key in data)) { return true; } diff --git a/deno/server/inter/http/routes/channel/postChannel.ts b/deno/server/inter/http/routes/channel/postChannel.ts index eef079cd..ad6c61f6 100644 --- a/deno/server/inter/http/routes/channel/postChannel.ts +++ b/deno/server/inter/http/routes/channel/postChannel.ts @@ -39,7 +39,7 @@ export default (core: Core) => }, }); const channel = await core.channel.get({ - id: channelId, + id: channelId!, userId: req.state.user.id, }); return Res.json(channel); diff --git a/deno/server/inter/http/routes/channel/putDirect.ts b/deno/server/inter/http/routes/channel/putDirect.ts index 1eabfa36..cafca278 100644 --- a/deno/server/inter/http/routes/channel/putDirect.ts +++ b/deno/server/inter/http/routes/channel/putDirect.ts @@ -26,7 +26,7 @@ export default (core: Core) => }, }); const channel = await core.channel.get({ - id: channelId, + id: channelId!, userId: req.state.user.id, }); return Res.json(channel); diff --git a/deno/server/inter/http/routes/messages/create.ts b/deno/server/inter/http/routes/messages/create.ts index fd62de26..d56502d1 100644 --- a/deno/server/inter/http/routes/messages/create.ts +++ b/deno/server/inter/http/routes/messages/create.ts @@ -54,7 +54,7 @@ export default (core: Core) => body: { ...req.body, userId, channelId }, }); - const msg = await core.message.get({ userId, messageId: id }); + const msg = await core.message.get({ userId, messageId: id! }); return Response.json(msg); }, }); diff --git a/deno/server/inter/http/routes/mobile/notifications.ts b/deno/server/inter/http/routes/mobile/notifications.ts index e92d88ba..60fdf225 100644 --- a/deno/server/inter/http/routes/mobile/notifications.ts +++ b/deno/server/inter/http/routes/mobile/notifications.ts @@ -42,7 +42,8 @@ export default (core: Core) => }, HEARTBEAT_INTERVAL_MS); // Subscribe to bus events for this user - const off = core.bus.on(userId, async (msg: BusMessage) => { + const off = core.bus.on(userId, async (raw) => { + const msg = raw as BusMessage; // Only process message events for notifications if (msg.type !== "message") { return; diff --git a/deno/server/inter/http/routes/users/create.ts b/deno/server/inter/http/routes/users/create.ts index 7e4aba53..17475e47 100644 --- a/deno/server/inter/http/routes/users/create.ts +++ b/deno/server/inter/http/routes/users/create.ts @@ -56,7 +56,7 @@ export default (core: Core) => secrets: req.body.secrets, }, }); - const user: DbUser | null = await core.user.get({ id: createdId }); + const user: DbUser | null = await core.user.get({ id: createdId! }); if (!user) { throw new InternalServerError( new Error("User not created, but no error thrown"), From b30390f231dcddc5e8fb32aecd4c5bd1a183c89b Mon Sep 17 00:00:00 2001 From: Mateusz Russak Date: Mon, 9 Feb 2026 22:46:07 +0100 Subject: [PATCH 3/8] chore(types): remove any types from storage, encryption, and support modules (#273) Remove any types from storage, encryption, config, migration, and utility modules. Part 2 of the ongoing type safety cleanup (follows PR #272 which covered backend core). --- BACKLOG.md | 14 ++++++++------ deno/config/types.ts | 2 +- deno/encryption/mod.ts | 8 ++++---- deno/migrate/mod.ts | 2 +- deno/server/app.ts | 4 +--- deno/storage/src/core/mod.ts | 14 ++++++++++++-- deno/storage/src/core/streams.ts | 30 +++++++++--------------------- deno/tools/cache.ts | 6 +++--- 8 files changed, 39 insertions(+), 41 deletions(-) diff --git a/BACKLOG.md b/BACKLOG.md index cfcb53ca..dc23b8ee 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -116,11 +116,11 @@ Create standard story template with: ### 2.1 Type Safety (HIGH PRIORITY) - [ ] Fix pre-existing TypeScript errors (17 errors in base) -- [x] Remove `any` types from backend core (serializers, bus, command/query, repos, webhooks) — PR #267 -- [ ] Remove `any` types from storage, encryption, config modules -- [ ] Remove `any` types from API module (`deno/api/`) -- [ ] Remove `any` types from frontend (models, components, contexts) -- [ ] Remove `any` types from test files and helpers +- [x] Remove `any` types from backend core (serializers, bus, command/query, repos, HTTP/CLI) — PR #272 +- [x] Remove `any` types from storage, encryption, config, migrate, tools modules — PR #273 +- [ ] Remove `any` types from API module (`deno/api/`) — ~30 instances +- [ ] Remove `any` types from frontend (`app/src/`) — ~20 instances +- [ ] Remove `any` types from test files — ~98 instances across 19 files - [ ] Fix `style: any` unused prop in `ActionButton.tsx:10` - [ ] Remove `@ts-ignore` comments in `deno/api/files.ts` (5+ instances) - [ ] Standardize Props interface naming @@ -291,7 +291,7 @@ Create standard story template with: ## Notes - Created: 2026-02-06 -- Last updated: 2026-02-08 +- Last updated: 2026-02-09 - Progress: - ✅ **Section 1: Storybook Cleanup - COMPLETE** - PR #249: Config fixes @@ -302,6 +302,8 @@ Create standard story template with: - PR #254: Message story image fix - 🔄 **Section 2: Code Cleanup - IN PROGRESS** - PR #255: Dependency updates (npm packages, @std alignment, ESLint React version) + - PR #272: Remove `any` types from backend core + - PR #273: Remove `any` types from storage, encryption, config, migrate, tools - 🔄 **Section 3: Architecture Refactoring - IN PROGRESS** - ✅ 3.7 Documentation: ARCHITECTURE.md, CONVENTIONS.md, 14 ADRs, CLAUDE.md - PR #256: Architecture docs diff --git a/deno/config/types.ts b/deno/config/types.ts index 715b74dc..c3d75a17 100644 --- a/deno/config/types.ts +++ b/deno/config/types.ts @@ -8,7 +8,7 @@ export type Config = { port: number; databaseUrl: string; cors: (string | RegExp)[]; - plugins?: ((app: any, core: any) => Promise | any)[]; + plugins?: ((app: unknown, core: unknown) => Promise | void)[]; webhooks?: { url: string; events?: string[]; diff --git a/deno/encryption/mod.ts b/deno/encryption/mod.ts index 75b744f8..f323e4c7 100644 --- a/deno/encryption/mod.ts +++ b/deno/encryption/mod.ts @@ -36,7 +36,7 @@ export function encryptor(jwk: JsonWebKey) { ]); return { - encrypt: async (message: any) => { + encrypt: async (message: unknown) => { const iv = crypto.getRandomValues(new Uint8Array(12)); // GCM uses 12 bytes IV const encoded = new TextEncoder().encode(JSON.stringify(message)); const keyy = await key; @@ -70,7 +70,7 @@ export function encryptor(jwk: JsonWebKey) { }; } export async function encrypt( - message: any, + message: unknown, encryptionKey: JsonWebKey | CryptoKey, ) { const key = importKey(encryptionKey); @@ -88,7 +88,7 @@ export async function encrypt( }; } -export async function decrypt( +export async function decrypt( data: { encrypted: string; _iv: string }, encryptionKey: JsonWebKey | CryptoKey, ): Promise { @@ -291,7 +291,7 @@ export async function prepareRegistration( }; } -export async function decryptSessionSecrets( +export async function decryptSessionSecrets( email: string, password: string, secrets: EncryptedData, diff --git a/deno/migrate/mod.ts b/deno/migrate/mod.ts index 1509b36b..2474773c 100644 --- a/deno/migrate/mod.ts +++ b/deno/migrate/mod.ts @@ -13,7 +13,7 @@ export class Database { databaseUrl: string; - promises: Promise[] = []; + promises: Promise[] = []; connected = false; diff --git a/deno/server/app.ts b/deno/server/app.ts index d6f54b25..6d369a42 100644 --- a/deno/server/app.ts +++ b/deno/server/app.ts @@ -7,9 +7,7 @@ const core = new Core({ }); const http = new HttpInterface(core); await Promise.all( - config.plugins?.map(( - plugin: (app: HttpInterface, core: Core) => Promise | any, - ) => plugin(http, core)) ?? [], + config.plugins?.map((plugin) => plugin(http, core)) ?? [], ); http.onClose(async () => { diff --git a/deno/storage/src/core/mod.ts b/deno/storage/src/core/mod.ts index ba4fa690..8432b626 100644 --- a/deno/storage/src/core/mod.ts +++ b/deno/storage/src/core/mod.ts @@ -10,8 +10,18 @@ type ScalingOpts = { height?: number; }; +interface FileService { + upload( + stream: ReadableStream, + options: FileOpts, + ): Promise; + get(id: string): Promise; + remove(id: string): Promise; + exists(id: string): Promise; +} + class Files { - _sharp: any; + _sharp: typeof sharp | null | undefined = undefined; async getSharp() { if (this._sharp === undefined) { try { @@ -28,7 +38,7 @@ class Files { static getFileId = (id: string, width = 0, height = 0) => `${id}-${width}x${height}`; - private service: any; + private service!: FileService; constructor(config: Config) { this.init(config.storage); diff --git a/deno/storage/src/core/streams.ts b/deno/storage/src/core/streams.ts index ba8a6d5b..72327526 100644 --- a/deno/storage/src/core/streams.ts +++ b/deno/storage/src/core/streams.ts @@ -1,43 +1,34 @@ import { Readable } from "node:stream"; -interface ListenerInterface { - data: (chunk: any) => void; - end: (chunk: any) => void; - close: (err: any) => void; - error: (err: any) => void; -} - export function toWebStream(nodeStream: Readable) { let destroyed = false; - const listeners = {} as ListenerInterface; + // deno-lint-ignore no-explicit-any + const listeners: Record void> = {}; - function start(controller: any) { + function start(controller: ReadableStreamDefaultController) { listeners.data = onData; listeners.end = onData; listeners.end = onDestroy; listeners.close = onDestroy; listeners.error = onDestroy; for (const name in listeners) { - nodeStream.on(name, listeners[name as keyof ListenerInterface]); + nodeStream.on(name, listeners[name]); } nodeStream.pause(); - function onData(chunk: any) { + function onData(chunk: Uint8Array) { if (destroyed) return; controller.enqueue(new Uint8Array(chunk)); nodeStream.pause(); } - function onDestroy(err: any) { + function onDestroy(err?: Error) { if (destroyed) return; destroyed = true; for (const name in listeners) { - nodeStream.removeListener( - name, - listeners[name as keyof ListenerInterface], - ); + nodeStream.removeListener(name, listeners[name]); } if (err) controller.error(err); @@ -54,10 +45,7 @@ export function toWebStream(nodeStream: Readable) { destroyed = true; for (const name in listeners) { - nodeStream.removeListener( - name, - listeners[name as keyof ListenerInterface], - ); + nodeStream.removeListener(name, listeners[name]); } nodeStream.push(null); @@ -75,7 +63,7 @@ class NodeReadable extends Readable { private reader: ReadableStreamDefaultReader; - private pendingRead?: Promise; + private pendingRead?: Promise>; constructor(stream: ReadableStream) { super(); diff --git a/deno/tools/cache.ts b/deno/tools/cache.ts index 0a4a71cb..974d6075 100644 --- a/deno/tools/cache.ts +++ b/deno/tools/cache.ts @@ -1,6 +1,6 @@ import { mergeRanges, Range } from "./range.ts"; -export class CacheEntry extends Range { +export class CacheEntry extends Range { data: T; timestamp: number; @@ -10,7 +10,7 @@ export class CacheEntry extends Range { this.timestamp = new Date().getTime(); } - static sort(repo: CacheEntry[]) { + static sort(repo: CacheEntry[]) { return [...repo].sort((a, b) => a.from - b.from); } @@ -24,7 +24,7 @@ export type CacheQuery = { to?: number; }; -export class Cache { +export class Cache { repo: CacheEntry[] = []; merge = (a: CacheEntry, b: CacheEntry): CacheEntry => ( From 28856e5a42c9503aeef799d8a07222a4edbb2e8e Mon Sep 17 00:00:00 2001 From: Mateusz Russak Date: Wed, 11 Feb 2026 00:10:35 +0100 Subject: [PATCH 4/8] chore(types): remove any types from API module (#274) Remove any types from the shared API client module (deno/api/). Part 3 of the type safety cleanup, following PR #272 (backend core) and PR #273 (storage/encryption/support). --- BACKLOG.md | 6 ++++- deno/api/auth.ts | 9 +++++-- deno/api/files.ts | 6 ++--- deno/api/messageTypes.ts | 4 ++-- deno/api/mod.ts | 51 ++++++++++++++++++++++++++-------------- 5 files changed, 50 insertions(+), 26 deletions(-) diff --git a/BACKLOG.md b/BACKLOG.md index dc23b8ee..634abdd8 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -118,7 +118,7 @@ Create standard story template with: - [ ] Fix pre-existing TypeScript errors (17 errors in base) - [x] Remove `any` types from backend core (serializers, bus, command/query, repos, HTTP/CLI) — PR #272 - [x] Remove `any` types from storage, encryption, config, migrate, tools modules — PR #273 -- [ ] Remove `any` types from API module (`deno/api/`) — ~30 instances +- [x] Remove `any` types from API module (`deno/api/`) — PR #274 - [ ] Remove `any` types from frontend (`app/src/`) — ~20 instances - [ ] Remove `any` types from test files — ~98 instances across 19 files - [ ] Fix `style: any` unused prop in `ActionButton.tsx:10` @@ -304,6 +304,10 @@ Create standard story template with: - PR #255: Dependency updates (npm packages, @std alignment, ESLint React version) - PR #272: Remove `any` types from backend core - PR #273: Remove `any` types from storage, encryption, config, migrate, tools +<<<<<<< HEAD + - PR #274: Remove `any` types from API module +======= +>>>>>>> origin/dev - 🔄 **Section 3: Architecture Refactoring - IN PROGRESS** - ✅ 3.7 Documentation: ARCHITECTURE.md, CONVENTIONS.md, 14 ADRs, CLAUDE.md - PR #256: Architecture docs diff --git a/deno/api/auth.ts b/deno/api/auth.ts index 5c755312..e5731dc5 100644 --- a/deno/api/auth.ts +++ b/deno/api/auth.ts @@ -8,10 +8,15 @@ import * as enc from "@quack/encryption"; import type API from "./mod.ts"; export class ApiError extends Error { - payload: any; + payload: Record; url: string; status: number; - constructor(msg: string, status: number, url: string, payload: any) { + constructor( + msg: string, + status: number, + url: string, + payload: Record, + ) { super(msg); this.status = status; this.url = url; diff --git a/deno/api/files.ts b/deno/api/files.ts index 946b091c..abd8ac58 100644 --- a/deno/api/files.ts +++ b/deno/api/files.ts @@ -55,12 +55,12 @@ export class FilesAPI { delete this.aborts[args.clientId]; resolve(data); }, { once: true }); - xhr.upload.addEventListener("progress", (e: any) => { + xhr.upload.addEventListener("progress", (e: ProgressEvent) => { if (e.lengthComputable) { args.onProgress?.((e.loaded / e.total) * 100); } }); - xhr.addEventListener("error", (e: any) => { + xhr.addEventListener("error", (e: Event) => { delete this.aborts[args.clientId]; reject(e); }, { once: true }); @@ -120,7 +120,7 @@ export class FilesAPI { return new Promise((resolve, reject) => { (async () => { try { - const chunks: any[] = []; + const chunks: BlobPart[] = []; // @ts-ignore This is only for browsers for await (const chunk of stream) { chunks.push(chunk.value); diff --git a/deno/api/messageTypes.ts b/deno/api/messageTypes.ts index 83589a54..9f057ab5 100644 --- a/deno/api/messageTypes.ts +++ b/deno/api/messageTypes.ts @@ -22,7 +22,7 @@ export type MessageBodyButton = { button: string; _action: string; _style: string; - _payload: any; + _payload: Record; }; export type MessageBodyWrap = { wrap: MessageBody }; export type MessageBodyColumn = { column: MessageBody; _width: number }; @@ -101,7 +101,7 @@ export type MessageData = { favicons: string[]; charset: string; }[]; - parsingErrors?: any[]; + parsingErrors?: Array<{ message: string; path?: string }>; attachments?: Array<{ // TODO make this a separate entity id: string; fileName: string; diff --git a/deno/api/mod.ts b/deno/api/mod.ts index d305b853..6bc4dcae 100644 --- a/deno/api/mod.ts +++ b/deno/api/mod.ts @@ -15,7 +15,16 @@ import { FilesAPI } from "./files.ts"; export * from "./types.ts"; -declare const document: any; +export type ApiResponse = { + type: "response"; + status: "ok"; + seqId?: string; + data: Record[]; +}; + +declare const document: { + addEventListener: (type: string, listener: () => void) => void; +} | undefined; const isDeno = typeof window === "undefined"; @@ -36,10 +45,15 @@ async function waitBeforeRetry(retry: number) { } export class ApiError extends Error { - payload: any; + payload: Record; url: string; status: number; - constructor(msg: string, status: number, url: string, payload: any) { + constructor( + msg: string, + status: number, + url: string, + payload: Record, + ) { super(msg); this.status = status; this.url = url; @@ -50,7 +64,7 @@ export class ApiError extends Error { class API extends EventTarget { baseUrl: string; - _http: any; + _http: unknown; _token: string | undefined; userId: string | undefined; @@ -76,7 +90,7 @@ class API extends EventTarget { reconnectTimeout: number | undefined; - set token(value: any) { + set token(value: string | undefined) { if (typeof value === "string" && value.trim() !== "") { this._token = value; this.tokenInit(); @@ -183,7 +197,7 @@ class API extends EventTarget { async fetchWithCredentials( url: string, opts: RequestInit = {}, - ): Promise { + ): Promise { return await this.fetch( `${this.baseUrl}${url}`, this.token @@ -211,11 +225,11 @@ class API extends EventTarget { url: string, opts: { seqId?: string; - mapFn?: (i: any) => any; + mapFn?: (i: Record) => Record; retry?: number; retries?: number; } & RequestInit = {}, - ): Promise { + ): Promise { const retries = opts?.retries ?? 5; const retry = opts?.retry ?? 0; const res = await this.fetchWithCredentials(url, opts); @@ -333,7 +347,7 @@ class API extends EventTarget { ...Object.fromEntries( Object.entries(query).filter(([_, v]) => typeof v !== "undefined"), ), - } as any); + } as Record); return await this.getResource( `/api/channels/${channelId}/messages?${params.toString()}`, ); @@ -439,7 +453,7 @@ class API extends EventTarget { appId?: string; clientId: string; action: string; - payload: any; + payload?: Record; }, ): Promise { const res = await this.fetchWithCredentials(`/api/interactions`, { @@ -449,7 +463,7 @@ class API extends EventTarget { await res.body?.cancel(); } - async sendMessage(msg: Partial): Promise { + async sendMessage(msg: Partial): Promise> { return await new Promise((resolve, reject) => { const data = { ...msg }; let timeoutId: number | null = setTimeout(() => { @@ -489,7 +503,7 @@ class API extends EventTarget { }); } - async sendCommand(cmd: Partial): Promise { + async sendCommand(cmd: Partial): Promise> { const data = { ...cmd }; const res = await this.fetchWithCredentials( "/api/commands/execute", @@ -521,13 +535,13 @@ class API extends EventTarget { case "channels:load": { return this.callApi("/api/channels", { seqId: msg.seqId, - mapFn: (i: any) => ({ type: "channel", ...i }), + mapFn: (i: Record) => ({ type: "channel", ...i }), }); } case "user:getAll": { return this.callApi("/api/users", { seqId: msg.seqId, - mapFn: (i: any) => ({ type: "user", ...i }), + mapFn: (i: Record) => ({ type: "user", ...i }), }); } case "user:get": { @@ -536,7 +550,7 @@ class API extends EventTarget { case "emoji:getAll": { return this.callApi("/api/emojis", { seqId: msg.seqId, - mapFn: (i: any) => ({ type: "emoji", ...i }), + mapFn: (i: Record) => ({ type: "emoji", ...i }), }); } case "channel:create": { @@ -565,6 +579,7 @@ class API extends EventTarget { }, ); + if (createRes instanceof ApiErrorResponse) return createRes; return await this.callApi(`/api/messages/${createRes.data[0].id}`, { method: "GET", }); @@ -599,19 +614,19 @@ class API extends EventTarget { `/api/channels/${msg.channelId}/messages?q=${msg.text}`, { seqId: msg.seqId, - mapFn: (i: any) => ({ type: "search", ...i }), + mapFn: (i: Record) => ({ type: "search", ...i }), }, ); case "readReceipt:getOwn": { return this.callApi("/api/read-receipts", { seqId: msg.seqId, - mapFn: (i: any) => ({ type: "badge", ...i }), + mapFn: (i: Record) => ({ type: "badge", ...i }), }); } case "readReceipt:getChannel": { return this.callApi(`/api/channels/${msg.channelId}/read-receipts`, { seqId: msg.seqId, - mapFn: (i: any) => ({ type: "badge", ...i }), + mapFn: (i: Record) => ({ type: "badge", ...i }), }); } case "readReceipt:update": { From 62e71a3db6908c37ae58d862c93fd8650537aa08 Mon Sep 17 00:00:00 2001 From: Mateusz Russak Date: Wed, 11 Feb 2026 00:33:22 +0100 Subject: [PATCH 5/8] chore(types): remove any types from frontend code (#275) Remove any types from frontend code: client, plugins, utils, models, and components. Part 4 of the type safety cleanup series. Includes fix aligning parsingErrors type with SerializeError shape. --- BACKLOG.md | 6 +++--- app/src/js/components/contexts/tooltip.tsx | 4 ++-- app/src/js/components/molecules/ActionButton.tsx | 4 ++-- app/src/js/components/molecules/NavChannels.tsx | 2 +- app/src/js/components/molecules/NavUsers.tsx | 10 +++++----- app/src/js/components/molecules/UserMention.tsx | 2 +- app/src/js/components/organisms/Search.tsx | 2 +- app/src/js/components/pages/ErrorPage.tsx | 2 +- app/src/js/components/pages/Register.tsx | 5 +++-- app/src/js/core/client.ts | 4 +++- app/src/js/core/models/files.ts | 4 ++-- app/src/js/core/models/input.ts | 1 + app/src/js/core/models/message.ts | 2 +- app/src/js/core/plugins.ts | 8 ++++---- app/src/js/utils.ts | 2 +- 15 files changed, 31 insertions(+), 27 deletions(-) diff --git a/BACKLOG.md b/BACKLOG.md index 634abdd8..a3a01c11 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -115,13 +115,13 @@ Create standard story template with: ## 2. Code Cleanup ### 2.1 Type Safety (HIGH PRIORITY) -- [ ] Fix pre-existing TypeScript errors (17 errors in base) +- [ ] Fix pre-existing TypeScript errors (16 errors in base) - [x] Remove `any` types from backend core (serializers, bus, command/query, repos, HTTP/CLI) — PR #272 - [x] Remove `any` types from storage, encryption, config, migrate, tools modules — PR #273 - [x] Remove `any` types from API module (`deno/api/`) — PR #274 -- [ ] Remove `any` types from frontend (`app/src/`) — ~20 instances +- [x] Remove `any` types from frontend — client, plugins, utils, models, components — PR #275 - [ ] Remove `any` types from test files — ~98 instances across 19 files -- [ ] Fix `style: any` unused prop in `ActionButton.tsx:10` +- [x] Fix `style: any` prop in `ActionButton.tsx` — typed as `string` (PR #275) - [ ] Remove `@ts-ignore` comments in `deno/api/files.ts` (5+ instances) - [ ] Standardize Props interface naming diff --git a/app/src/js/components/contexts/tooltip.tsx b/app/src/js/components/contexts/tooltip.tsx index ef771525..1c017286 100644 --- a/app/src/js/components/contexts/tooltip.tsx +++ b/app/src/js/components/contexts/tooltip.tsx @@ -7,7 +7,7 @@ export type TooltipContextType = { content: React.ReactNode, parent: React.ReactNode | HTMLElement, ) => void; - hide: (p: any) => void; + hide: (p: React.ReactNode | HTMLElement) => void; }; export const TooltipContext = createContext( @@ -67,7 +67,7 @@ export const TooltipProvider = ({ children }: TooltipContextProps) => { [setContent, setPos, setParent], ); - const hide = useCallback((p: any) => { + const hide = useCallback((p: React.ReactNode | HTMLElement) => { if (parent === p) { setParent(null); } diff --git a/app/src/js/components/molecules/ActionButton.tsx b/app/src/js/components/molecules/ActionButton.tsx index d5c39806..95657c21 100644 --- a/app/src/js/components/molecules/ActionButton.tsx +++ b/app/src/js/components/molecules/ActionButton.tsx @@ -7,8 +7,8 @@ import { observer } from "mobx-react-lite"; type ActionButtonProps = { children: React.ReactNode; action: string; - style: any; - payload: any; + style?: string; + payload?: Record; }; export const ActionButton = observer( diff --git a/app/src/js/components/molecules/NavChannels.tsx b/app/src/js/components/molecules/NavChannels.tsx index 4e7f43be..b89c3c0e 100644 --- a/app/src/js/components/molecules/NavChannels.tsx +++ b/app/src/js/components/molecules/NavChannels.tsx @@ -76,7 +76,7 @@ export const NavChannels = observer(({ icon }: NavChannelsProps) => { className={{ active: id === c.id }} key={c.id} icon={icon ?? "hash"} - badge={badges.getForChannel(c.id as any)} + badge={badges.getForChannel(String(c.id))} onClick={() => { if (isMobile()) { hideSidebar(); diff --git a/app/src/js/components/molecules/NavUsers.tsx b/app/src/js/components/molecules/NavUsers.tsx index 7c9f3fef..32f3aa82 100644 --- a/app/src/js/components/molecules/NavUsers.tsx +++ b/app/src/js/components/molecules/NavUsers.tsx @@ -118,7 +118,7 @@ export const NavUserButton = ({ const NavUserContainer = observer( ({ user, badges }: { user: User; badges: ReadReceiptsModel }) => { const app = useApp(); - const channel = app.channels.getDirect(user.id as any); + const channel = app.channels.getDirect(String(user.id)); let navigate = (_path: string) => {}; try { navigate = useNavigate(); @@ -128,11 +128,11 @@ const NavUserContainer = observer( return ( { - const channel = await client.api.putDirectChannel(user.id as any); + const channel = await client.api.putDirectChannel(String(user.id)); if (isMobile()) { hideSidebar(); } @@ -152,7 +152,7 @@ export const NavUsers = observer(() => { {users && users.map((user) => ( diff --git a/app/src/js/components/molecules/UserMention.tsx b/app/src/js/components/molecules/UserMention.tsx index 0b185014..75a2a69d 100644 --- a/app/src/js/components/molecules/UserMention.tsx +++ b/app/src/js/components/molecules/UserMention.tsx @@ -26,7 +26,7 @@ export const UserMentionBase = observer(({ user }: UserMentionBaseProps) => { data-id={user.id} href="#" > - @{user?.name || (user.id as any)} + @{user?.name || String(user.id)} ); }); diff --git a/app/src/js/components/organisms/Search.tsx b/app/src/js/components/organisms/Search.tsx index 1283e86b..c6c840d5 100644 --- a/app/src/js/components/organisms/Search.tsx +++ b/app/src/js/components/organisms/Search.tsx @@ -143,7 +143,7 @@ export const SearchResults = observer(
{ gotoMessage(msg); }} diff --git a/app/src/js/components/pages/ErrorPage.tsx b/app/src/js/components/pages/ErrorPage.tsx index 2361c5a1..6bce314b 100644 --- a/app/src/js/components/pages/ErrorPage.tsx +++ b/app/src/js/components/pages/ErrorPage.tsx @@ -66,7 +66,7 @@ const Container = styled.div` type ErrorPageProps = { title?: string; - debug?: any; + debug?: Record; buttons?: ("retry" | "back" | "home")[]; description?: string | string[]; }; diff --git a/app/src/js/components/pages/Register.tsx b/app/src/js/components/pages/Register.tsx index 27ee4dbe..20992061 100644 --- a/app/src/js/components/pages/Register.tsx +++ b/app/src/js/components/pages/Register.tsx @@ -57,8 +57,9 @@ export const Register = () => { console.log(err); setMsg(err.message); } else { - if ((err as any)?.errorCode === "USER_ALREADY_EXISTS") { - setMsg((err as any).message); + const errObj = err as { errorCode?: string; message?: string }; + if (errObj.errorCode === "USER_ALREADY_EXISTS") { + setMsg(errObj.message ?? "User already exists"); } else { console.log(err); setMsg("Unknown error"); diff --git a/app/src/js/core/client.ts b/app/src/js/core/client.ts index 7627b1d3..aaecaf2a 100644 --- a/app/src/js/core/client.ts +++ b/app/src/js/core/client.ts @@ -20,6 +20,7 @@ export class Client { return this.api.req(...args); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any on(name: string, cb: (e: any) => void) { this.api.on(name, (ev: Event) => { if (ev instanceof CustomEvent) { @@ -32,6 +33,7 @@ export class Client { return this; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any on2(name: string, cb: (e: any) => void) { const handler = (ev: Event) => { if (ev instanceof CustomEvent) { @@ -45,7 +47,7 @@ export class Client { return () => this.api.off(name, handler); } - emit(type: string, data: any) { + emit(type: string, data: unknown) { return this.api.emit(new CustomEvent(type, { detail: data })); } } diff --git a/app/src/js/core/models/files.ts b/app/src/js/core/models/files.ts index 26372d3a..ac1fa312 100644 --- a/app/src/js/core/models/files.ts +++ b/app/src/js/core/models/files.ts @@ -39,7 +39,7 @@ export class FileModel { async dispose() { this.id = undefined; this.clientId = ""; - this.stream = null as any; + this.stream = null as unknown as ReadableStream; this.status = "pending"; this.fileSize = 0; this.fileName = ""; @@ -144,7 +144,7 @@ export class FilesModel { local.patch({ status: "error", progress: 0, error: "unknown error" }); } }); - toJSON(): any { + toJSON(): Array<{ id?: string; clientId: string; fileName: string; fileSize: number; contentType: string }> { return this.list.map((f) => ({ id: f.id, clientId: f.clientId, diff --git a/app/src/js/core/models/input.ts b/app/src/js/core/models/input.ts index e284e206..7904548d 100644 --- a/app/src/js/core/models/input.ts +++ b/app/src/js/core/models/input.ts @@ -43,6 +43,7 @@ export class InputModel { } send = flow(function* (this: InputModel, html: HTMLElement) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const payload: any = fromDom(html); html.innerHTML = ""; payload.attachments = this.files.toJSON(); diff --git a/app/src/js/core/models/message.ts b/app/src/js/core/models/message.ts index e8833cf1..b069017d 100644 --- a/app/src/js/core/models/message.ts +++ b/app/src/js/core/models/message.ts @@ -49,7 +49,7 @@ export class MessageModel implements ViewMessage { favicons: string[]; charset: string; }[]; - parsingErrors?: any[]; + parsingErrors?: Array<{ message: string; nodeAttributes?: Record; nodeName?: string }>; attachments?: Array<{ id: string; fileName: string; diff --git a/app/src/js/core/plugins.ts b/app/src/js/core/plugins.ts index 61ee317e..1bf4745b 100644 --- a/app/src/js/core/plugins.ts +++ b/app/src/js/core/plugins.ts @@ -2,13 +2,13 @@ import { client } from "./client.ts"; declare global { interface Window { - Chat: any; + Chat: typeof plugins; } } -const registry: Record> = {}; +const registry: Record> = {}; const plugins = { - register: (slot: string, data: any) => { + register: (slot: string, data: unknown) => { if (typeof data === "function") { data = data(client); } @@ -16,7 +16,7 @@ const plugins = { [data].flat().forEach((d) => registry[slot].push(d)); }, - get: (slot: string): any => registry[slot] || [], + get: (slot: string): unknown[] => registry[slot] || [], }; window.Chat = plugins; diff --git a/app/src/js/utils.ts b/app/src/js/utils.ts index 6bf65513..7ebc6755 100644 --- a/app/src/js/utils.ts +++ b/app/src/js/utils.ts @@ -32,7 +32,7 @@ export const cn = (...classes: ClassNames[]) => return item; }).flat().filter(Boolean).join(" "); -export const same = (o1: any, o2: any, fields: string[]): boolean => { +export const same = (o1: Record, o2: Record, fields: string[]): boolean => { return fields.every((field) => { return o1 && o2 && o1[field] === o2[field]; }); From b5ae7eae1cb4831a312fc46fdc8ceaeceec0c358 Mon Sep 17 00:00:00 2001 From: Mateusz Russak Date: Wed, 11 Feb 2026 00:46:02 +0100 Subject: [PATCH 6/8] chore(types): remove any types from test files (#276) Replace ~98 any instances across 19 test files with proper types. Final PR (5 of 5) in the remove-any-types series (PRs #272-276). Only 2 justified any remain in test helpers with deno-lint-ignore. --- BACKLOG.md | 6 +- .../inter/http/routes/__tests__/chat.ts | 98 +++++++++++++------ .../inter/http/routes/__tests__/users.ts | 1 + .../routes/auth/__tests__/session.test.ts | 14 +-- .../routes/channel/__tests__/channels.test.ts | 6 +- .../routes/channel/__tests__/direct.test.ts | 2 +- .../commands/__tests__/commands.test.ts | 35 ++++--- .../routes/emojis/__tests__/emojis.test.ts | 2 +- .../__tests__/interaction.test.ts | 6 +- .../routes/messages/__tests__/format.test.ts | 6 +- .../messages/__tests__/messages.test.ts | 29 ++++-- .../messages/__tests__/notifications.test.ts | 8 +- .../routes/messages/__tests__/pinning.test.ts | 12 +-- .../routes/messages/__tests__/react.test.ts | 50 ++++++---- .../routes/messages/__tests__/threads.test.ts | 10 +- .../routes/profile/__tests__/profile.test.ts | 2 +- .../readReceipt/__tests__/readReceipt.test.ts | 22 ++--- .../users/__tests__/registration.test.ts | 10 +- .../http/routes/users/__tests__/users.test.ts | 12 +-- 19 files changed, 207 insertions(+), 124 deletions(-) diff --git a/BACKLOG.md b/BACKLOG.md index a3a01c11..8c009dcf 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -120,7 +120,7 @@ Create standard story template with: - [x] Remove `any` types from storage, encryption, config, migrate, tools modules — PR #273 - [x] Remove `any` types from API module (`deno/api/`) — PR #274 - [x] Remove `any` types from frontend — client, plugins, utils, models, components — PR #275 -- [ ] Remove `any` types from test files — ~98 instances across 19 files +- [x] Remove `any` types from test files (chat.ts helper, 14 test files, users.ts) — PR #276 - [x] Fix `style: any` prop in `ActionButton.tsx` — typed as `string` (PR #275) - [ ] Remove `@ts-ignore` comments in `deno/api/files.ts` (5+ instances) - [ ] Standardize Props interface naming @@ -302,11 +302,15 @@ Create standard story template with: - PR #254: Message story image fix - 🔄 **Section 2: Code Cleanup - IN PROGRESS** - PR #255: Dependency updates (npm packages, @std alignment, ESLint React version) +<<<<<<< HEAD + - PRs #272-276: Remove `any` types across codebase (~120 instances) +======= - PR #272: Remove `any` types from backend core - PR #273: Remove `any` types from storage, encryption, config, migrate, tools <<<<<<< HEAD - PR #274: Remove `any` types from API module ======= +>>>>>>> origin/dev >>>>>>> origin/dev - 🔄 **Section 3: Architecture Refactoring - IN PROGRESS** - ✅ 3.7 Documentation: ARCHITECTURE.md, CONVENTIONS.md, 14 ADRs, CLAUDE.md diff --git a/deno/server/inter/http/routes/__tests__/chat.ts b/deno/server/inter/http/routes/__tests__/chat.ts index 118d3769..a9c42920 100644 --- a/deno/server/inter/http/routes/__tests__/chat.ts +++ b/deno/server/inter/http/routes/__tests__/chat.ts @@ -5,9 +5,11 @@ import { Repository } from "../../../../infra/mod.ts"; import { ensureUser } from "./users.ts"; import { Channel, + Emoji, EntityId, Message, ReplaceEntityId, + User, } from "../../../../types.ts"; import { AsyncLocalStorage } from "node:async_hooks"; import API, { LoginError, Result, UserSession } from "@quack/api"; @@ -19,6 +21,19 @@ export type RegistrationRequest = { email: string; }; +type CommandResult = { + status: string; + json: Record; + channelId: string; + events: SSESource | null; +}; + +type Attachment = { + id: string; + fileName: string; + contentType: string; +}; + // deno-lint-ignore ban-types type Arg = T | ((chat: Chat) => T); const asyncLocalStorage = new AsyncLocalStorage<{ instances: Chat[] }>(); @@ -46,11 +61,12 @@ export class Chat { currentStep = 0; - state: any = {}; + // deno-lint-ignore no-explicit-any + state: Record = {}; - steps: any[] = []; + steps: Array<() => Promise> = []; - cleanup: any[] = []; + cleanup: Array<() => Promise> = []; appVersion = "client-version"; @@ -134,7 +150,7 @@ export class Chat { data: Arg< { token: string; email: string; password: string; oldPassword: string } >, - test?: (session: Result) => Promise | any, + test?: (session: Result) => Promise | void, ) { this.steps.push(async () => { const resetData = this.arg(data); @@ -158,7 +174,9 @@ export class Chat { return this; } - nextEvent(fn: (event: any, chat: Chat) => any) { + nextEvent( + fn: (event: Record, chat: Chat) => Promise | void, + ) { this.steps.push(async () => { const { event } = await this.eventSource?.next() || {}; await fn(JSON.parse(event?.data || "{}"), this); @@ -169,7 +187,7 @@ export class Chat { login( email = "admin", password = "123", - test?: (session: Result) => Promise | any, + test?: (session: Result) => Promise | void, ) { this.steps.push(async () => { await ensureUser(this.repo, email); @@ -183,7 +201,10 @@ export class Chat { return this; } - checkToken(tokenData: Arg, test?: (body: any) => Promise | any) { + checkToken( + tokenData: Arg, + test?: (body: Record) => Promise | void, + ) { this.steps.push(async () => { const token = this.arg(tokenData); const ret = await this.api.auth.checkRegistrationToken({ token }); @@ -194,7 +215,7 @@ export class Chat { register( data: RegistrationRequest, - test?: (body: any) => Promise | any, + test?: (body: Record) => Promise | void, ) { this.steps.push(async () => { const body = await this.api.auth.register(data); @@ -212,7 +233,7 @@ export class Chat { createChannel( channelData: Arg>>, - test?: (channel: Channel, chat: Chat) => Promise | any, + test?: (channel: Channel, chat: Chat) => Promise | void, ) { let channelId: string; this.steps.push(async () => { @@ -248,7 +269,7 @@ export class Chat { test?: ( channel: ReplaceEntityId, chat: Chat, - ) => Promise | any, + ) => Promise | void, ) { this.steps.push(async () => { const { userId } = this.arg(data); @@ -278,7 +299,7 @@ export class Chat { test?: ( channel: ReplaceEntityId, chat: Chat, - ) => Promise | any, + ) => Promise | void, ) { this.steps.push(async () => { const { userId } = this.arg(data); @@ -326,7 +347,7 @@ export class Chat { } getChannel( - fn: (channel: ReplaceEntityId, chat: Chat) => Promise | any, + fn: (channel: ReplaceEntityId, chat: Chat) => Promise | void, ) { this.steps.push(async () => { const res = await this.agent.request() @@ -339,7 +360,7 @@ export class Chat { return this; } - getEmojis(fn: (emojis: any[]) => Promise) { + getEmojis(fn: (emojis: Emoji[]) => Promise | void) { this.steps.push(async () => { const res = await this.agent.request() .get("/api/emojis") @@ -351,7 +372,7 @@ export class Chat { return this; } - getConfig(fn: (config: any) => Promise) { + getConfig(fn: (config: Record) => Promise | void) { this.steps.push(async () => { const res = await this.agent.request() .get("/api/profile/config") @@ -363,7 +384,7 @@ export class Chat { return this; } - getChannels(fn: (channels: Channel[]) => Promise | any) { + getChannels(fn: (channels: Channel[]) => Promise | void) { this.steps.push(async () => { const res = await this.agent.request() .get("/api/channels") @@ -375,7 +396,9 @@ export class Chat { return this; } - getUsers(fn: (users: any[], chat: Chat) => Promise) { + getUsers( + fn: (users: User[], chat: Chat) => Promise | void, + ) { this.steps.push(async () => { const res = await this.agent.request() .get("/api/users") @@ -389,7 +412,7 @@ export class Chat { getUser( userId: string | ((chat: Chat) => string), - fn: (user: any) => Promise, + fn: (user: User) => Promise | void, ) { this.steps.push(async () => { const id = typeof userId === "function" ? userId(this) : userId; @@ -405,7 +428,10 @@ export class Chat { getMessages( queryData: Arg<{ parentId?: string | null }> = {}, - test?: (messages: any[], chat: Chat) => Promise | any, + test?: ( + messages: Record[], + chat: Chat, + ) => Promise | void, ) { this.steps.push(async () => { const { parentId } = this.arg(queryData); @@ -429,7 +455,7 @@ export class Chat { test?: ( message: ReplaceEntityId, chat: Chat, - ) => Promise | any, + ) => Promise | void, ) { this.steps.push(async () => { const message = this.arg(messageData); @@ -453,7 +479,7 @@ export class Chat { channelId?: string; parentId?: string; clientId: string; - payload?: any; + payload?: Record; action: string; }>, ) { @@ -487,7 +513,10 @@ export class Chat { } getChannelReadReceipts( - fn: (receipts: any[], chat: Chat) => Promise | any, + fn: ( + receipts: Record[], + chat: Chat, + ) => Promise | void, ) { this.steps.push(async () => { const res = await this.agent.request() @@ -500,7 +529,12 @@ export class Chat { return this; } - getReadReceipts(fn: (receipts: any[], chat: Chat) => Promise | any) { + getReadReceipts( + fn: ( + receipts: Record[], + chat: Chat, + ) => Promise | void, + ) { this.steps.push(async () => { const res = await this.agent.request() .get("/api/read-receipts") @@ -514,7 +548,10 @@ export class Chat { updateReadReceipts( messageId: string | ((chat: Chat) => string), - test?: (receipt: any, chat: Chat) => Promise | any, + test?: ( + receipt: Record, + chat: Chat, + ) => Promise | void, ) { this.steps.push(async () => { const res = await this.agent.request() @@ -537,8 +574,8 @@ export class Chat { executeCommand( command: string, - attachments: any[], - test?: (...args: any) => any, + attachments: Attachment[], + test?: (result: CommandResult) => Promise | void, ) { this.steps.push(async () => { if (!this.channelId) { @@ -577,7 +614,12 @@ export class Chat { return this; } - getPinnedMessages(fn: (messages: any[], chat: Chat) => Promise | any) { + getPinnedMessages( + fn: ( + messages: Record[], + chat: Chat, + ) => Promise | void, + ) { this.steps.push(async () => { const res = await this.agent.request() .get(`/api/channels/${this.channelId}/messages?pinned=true`) @@ -600,7 +642,7 @@ export class Chat { return this; } - step(test: (chat: Chat) => any) { + step(test: (chat: Chat) => Promise | void) { this.steps.push(async () => { await test(this); }); @@ -612,7 +654,7 @@ export class Chat { return this; } - async then(resolve: (self?: any) => any, reject: (e: unknown) => any) { + async then(resolve: (self?: void) => void, reject: (e: unknown) => void) { let cleanupStart = false; try { while (this.steps[this.currentStep]) { diff --git a/deno/server/inter/http/routes/__tests__/users.ts b/deno/server/inter/http/routes/__tests__/users.ts index 163e4a09..26a4d89f 100644 --- a/deno/server/inter/http/routes/__tests__/users.ts +++ b/deno/server/inter/http/routes/__tests__/users.ts @@ -5,6 +5,7 @@ import * as enc from "@quack/encryption"; export const ensureUser = async ( repo: Repository, email: string, + // deno-lint-ignore no-explicit-any rest: any = {}, ) => { const user = await repo.user.get({ email }); diff --git a/deno/server/inter/http/routes/auth/__tests__/session.test.ts b/deno/server/inter/http/routes/auth/__tests__/session.test.ts index f2b129be..1cc6c016 100644 --- a/deno/server/inter/http/routes/auth/__tests__/session.test.ts +++ b/deno/server/inter/http/routes/auth/__tests__/session.test.ts @@ -24,15 +24,17 @@ Deno.test("POST /auth/session - wrong params", async () => { const body = await res.json(); assertEquals( - body.errors?.map((e: any) => e?.params?.missingProperty).sort(), + body.errors?.map((e: Record) => + (e?.params as Record)?.missingProperty + ).sort(), ["email", "password"], ); }); Deno.test("Login/logout", async (t) => { - let token: any = null; - let userId: any = null; - let key: any = null; + let token: string | null = null; + let userId: string | null = null; + let key: string | null = null; await ensureUser(repo, "admin"); await t.step("POST /auth/session - Create session", async () => { @@ -92,8 +94,8 @@ Deno.test("Login/logout", async (t) => { }); Deno.test("Login/logout - cookies", async (t) => { - let token: any = null; - let userId: any = null; + let token: string | null = null; + let userId: string | null = null; await t.step("POST /auth/session - Create session", async () => { const credentials = await enc.prepareCredentials("admin", "123"); diff --git a/deno/server/inter/http/routes/channel/__tests__/channels.test.ts b/deno/server/inter/http/routes/channel/__tests__/channels.test.ts index 80efe028..4de96700 100644 --- a/deno/server/inter/http/routes/channel/__tests__/channels.test.ts +++ b/deno/server/inter/http/routes/channel/__tests__/channels.test.ts @@ -27,7 +27,7 @@ Deno.test("/api/channels", async () => { .login("admin") .connectSSE() .createChannel({ name: "test-channel-creation" }) - .nextEvent((event: any) => { + .nextEvent((event) => { assertEquals(event.type, "channel"); assertEquals(event.name, "test-channel-creation"); }) @@ -59,7 +59,7 @@ Deno.test("/api/channels - channel with description", async () => { name: "test-channel-with-description", description: "This is a test channel description", }) - .nextEvent((event: any) => { + .nextEvent((event) => { assertEquals(event.type, "channel"); assertEquals(event.name, "test-channel-with-description"); assertEquals(event.description, "This is a test channel description"); @@ -84,7 +84,7 @@ Deno.test("/api/channels - other user receives notification about channel", asyn users: [member.userIdR], }); await member - .nextEvent((event: any) => { + .nextEvent((event) => { assertEquals(event.type, "channel"); assertEquals(event.name, "test-channel-creation-2"); }) diff --git a/deno/server/inter/http/routes/channel/__tests__/direct.test.ts b/deno/server/inter/http/routes/channel/__tests__/direct.test.ts index fe4a5408..1ff63ee0 100644 --- a/deno/server/inter/http/routes/channel/__tests__/direct.test.ts +++ b/deno/server/inter/http/routes/channel/__tests__/direct.test.ts @@ -25,7 +25,7 @@ Deno.test("/api/channels/direct/:userId", async () => { .nextEvent((event, chat) => { assertEquals(event.type, "message"); assertEquals(event.flat, "hello"); - chat.channelId = event.channelId; + chat.channelId = event.channelId as string; chat.state.directChannelId = event.channelId; }) .getMessages({}, (messages) => { diff --git a/deno/server/inter/http/routes/commands/__tests__/commands.test.ts b/deno/server/inter/http/routes/commands/__tests__/commands.test.ts index 175c12f5..04574e96 100644 --- a/deno/server/inter/http/routes/commands/__tests__/commands.test.ts +++ b/deno/server/inter/http/routes/commands/__tests__/commands.test.ts @@ -29,7 +29,10 @@ Deno.test("command /echo ", async () => assertEquals(event.type, "message"); assert(event.clientId, "Event should have clientId"); assertEquals(event.flat, "Hello World!!"); - assertEquals(event.message.text, "Hello World!!"); + assertEquals( + (event.message as Record).text, + "Hello World!!", + ); assertEquals(event.channelId, chat.channelId); }) .end(), @@ -37,7 +40,7 @@ Deno.test("command /echo ", async () => Deno.test("command /emoji ", async () => await Chat.test(app, { type: "handler" }, async (agent) => { - const state: any = {}; + const state: Record = {}; try { await Chat.init(repo, agent) .login("admin") @@ -52,11 +55,11 @@ Deno.test("command /emoji ", async () => ], async ({ channelId }) => { state.channelId = channelId; }) - .nextEvent((event: any) => { + .nextEvent((event) => { assertEquals(event.type, "emoji"); assertEquals(event.shortname, ":party-parrot:"); }) - .nextEvent((event: any) => { + .nextEvent((event) => { assertEquals(event.type, "message"); assert(event.clientId, "Event should have clientId"); assertEquals(event.flat, "Emoji :party-parrot: created"); @@ -82,12 +85,14 @@ Deno.test("command /invite", async () => { .createChannel({ name: "test-commands-invite" }) .connectSSE() .executeCommand("/invite", [], ({ json }) => { - url = json.data; + url = json.data as string; }) - .nextEvent((event: any) => { + .nextEvent((event) => { assertEquals(event.type, "message"); assert(event.clientId, "Event should have clientId"); - const m = event.flat.match("(https?://.*/invite/[0-9a-f]{32})"); + const m = (event.flat as string).match( + "(https?://.*/invite/[0-9a-f]{32})", + ); assert(m, "Result should contain invitation link"); assertEquals(m[1], url); }) @@ -108,7 +113,7 @@ Deno.test("command /avatar", async () => { contentType: "image/gif", }, ]) - .nextEvent((event: any) => { + .nextEvent((event) => { assertEquals(event.type, "user"); assertEquals(event.avatarFileId, "party-parrot"); }) @@ -129,8 +134,8 @@ Deno.test("command /version", async () => { .nextEvent((event) => { assertEquals(event.type, "message"); assert(event.clientId, "Event should have clientId"); - assertEquals(event.flat.includes("server-version"), true); - assertEquals(event.flat.includes("client-version"), true); + assertEquals((event.flat as string).includes("server-version"), true); + assertEquals((event.flat as string).includes("client-version"), true); }) .end(); }); @@ -146,10 +151,10 @@ Deno.test("command /help", async () => { .nextEvent((event) => { assertEquals(event.type, "message"); assert(event.clientId, "Event should have clientId"); - assertEquals(event.flat.includes("/avatar"), true); - assertEquals(event.flat.includes("/emoji"), true); - assertEquals(event.flat.includes("/invite"), true); - assertEquals(event.flat.includes("/version"), true); + assertEquals((event.flat as string).includes("/avatar"), true); + assertEquals((event.flat as string).includes("/emoji"), true); + assertEquals((event.flat as string).includes("/invite"), true); + assertEquals((event.flat as string).includes("/version"), true); }) .end(); }); @@ -169,7 +174,7 @@ Deno.test("command /leave", async () => { .nextEvent((event, chat) => { assertEquals(event.type, "channel"); assert( - !event.users.find((u: any) => u === chat.userId), + !(event.users as string[]).find((u) => u === chat.userId), "Updated channel should not contain user", ); }) diff --git a/deno/server/inter/http/routes/emojis/__tests__/emojis.test.ts b/deno/server/inter/http/routes/emojis/__tests__/emojis.test.ts index d619d9cb..0071d32a 100644 --- a/deno/server/inter/http/routes/emojis/__tests__/emojis.test.ts +++ b/deno/server/inter/http/routes/emojis/__tests__/emojis.test.ts @@ -43,7 +43,7 @@ Deno.test("Adding emojis and listing them", async () => { fileName: "smile.png", contentType: "image/png", }]) - .nextEvent((event: any) => { + .nextEvent((event) => { assertEquals(event.type, "emoji"); assertEquals(event.shortname, ":smile:"); }) diff --git a/deno/server/inter/http/routes/interactions/__tests__/interaction.test.ts b/deno/server/inter/http/routes/interactions/__tests__/interaction.test.ts index 47034bb2..59cc96a7 100644 --- a/deno/server/inter/http/routes/interactions/__tests__/interaction.test.ts +++ b/deno/server/inter/http/routes/interactions/__tests__/interaction.test.ts @@ -2,7 +2,7 @@ import { Agent } from "@planigale/testing"; import { assertEquals } from "@std/assert"; import { createApp } from "../../__tests__/app.ts"; import { Chat } from "../../__tests__/chat.ts"; -import { MessageInteractionEvent } from "../../../../../events.ts"; +import type { Event, MessageInteractionEvent } from "../../../../../events.ts"; import { EntityId } from "../../../../../types.ts"; const { app, repo, core } = createApp(); @@ -16,7 +16,7 @@ Deno.test("POST /api/interactions - dispatching interactions", async (t) => { .createChannel({ name: "test-messages-interactions" }); const { promise, resolve, reject } = Promise.withResolvers(); (async () => { - const event: any = await new Promise((resolve) => + const event: Event = await new Promise((resolve) => core.events.once(resolve) ); if (event.type !== "message:interaction") { @@ -50,7 +50,7 @@ Deno.test("POST /api/interactions - graceful shutdown", async (t) => { .login("admin") .createChannel({ name: "test-messages-interactions" }); for (let i = 0; i < 5; i++) { - const { promise, resolve } = Promise.withResolvers(); + const { promise, resolve } = Promise.withResolvers(); core.events.once(resolve); await admin.interaction({ action: "test", clientId: "clientId" }); await promise; diff --git a/deno/server/inter/http/routes/messages/__tests__/format.test.ts b/deno/server/inter/http/routes/messages/__tests__/format.test.ts index f385c041..a15c4c83 100644 --- a/deno/server/inter/http/routes/messages/__tests__/format.test.ts +++ b/deno/server/inter/http/routes/messages/__tests__/format.test.ts @@ -18,7 +18,11 @@ Deno.test("Check all validations for message field", async (t) => { name: "messages-formating-check", users: [EntityId.from(userId)], }, async (channelId) => { - const testPart = async (status: number, name: string, message: any) => + const testPart = async ( + status: number, + name: string, + message: Record | Record[], + ) => await t.step(name, async () => await agent.request() .post(`/api/channels/${channelId}/messages/`) diff --git a/deno/server/inter/http/routes/messages/__tests__/messages.test.ts b/deno/server/inter/http/routes/messages/__tests__/messages.test.ts index 93bf9964..556f25b8 100644 --- a/deno/server/inter/http/routes/messages/__tests__/messages.test.ts +++ b/deno/server/inter/http/routes/messages/__tests__/messages.test.ts @@ -427,7 +427,11 @@ Deno.test("Messages history", async (t) => { const body = await res.json(); assertEquals(body.length, 3); - assertEquals(body.map((m: any) => m.flat), ["t2", "t1", "t0"]); + assertEquals(body.map((m: Record) => m.flat), [ + "t2", + "t1", + "t0", + ]); }); await t.step("GET /api/channels/:channelId/messages - limit", async () => { @@ -438,7 +442,10 @@ Deno.test("Messages history", async (t) => { const body = await res.json(); assertEquals(body.length, 2); - assertEquals(body.map((m: any) => m.flat), ["t2", "t1"]); + assertEquals(body.map((m: Record) => m.flat), [ + "t2", + "t1", + ]); }); await t.step( @@ -455,7 +462,10 @@ Deno.test("Messages history", async (t) => { const body = await res.json(); assertEquals(body.length, 2); - assertEquals(body.map((m: any) => m.flat), ["t1", "t0"]); + assertEquals(body.map((m: Record) => m.flat), [ + "t1", + "t0", + ]); }, ); await t.step("GET /api/channels/:channelId/messages - before", async () => { @@ -470,7 +480,7 @@ Deno.test("Messages history", async (t) => { const body = await res.json(); assertEquals(body.length, 1); - assertEquals(body.map((m: any) => m.flat), ["t0"]); + assertEquals(body.map((m: Record) => m.flat), ["t0"]); }); await t.step("GET /api/channels/:channelId/messages - after", async () => { const res = await agent.request() @@ -484,7 +494,10 @@ Deno.test("Messages history", async (t) => { const body = await res.json(); assertEquals(body.length, 2); - assertEquals(body.map((m: any) => m.flat), ["t2", "t1"]); + assertEquals(body.map((m: Record) => m.flat), [ + "t2", + "t1", + ]); }); await t.step("GET /api/channels/:channelId/messages - pinend", async () => { const res = await agent.request() @@ -494,7 +507,7 @@ Deno.test("Messages history", async (t) => { const body = await res.json(); assertEquals(body.length, 1); - assertEquals(body.map((m: any) => m.flat), ["t1"]); + assertEquals(body.map((m: Record) => m.flat), ["t1"]); }); await t.step("GET /api/channels/:channelId/messages - offset", async () => { const res = await agent.request() @@ -504,7 +517,7 @@ Deno.test("Messages history", async (t) => { const body = await res.json(); assertEquals(body.length, 1); - assertEquals(body.map((m: any) => m.flat), ["t2"]); + assertEquals(body.map((m: Record) => m.flat), ["t2"]); }); await t.step("GET /api/channels/:channelId/messages - search", async () => { const res = await agent.request() @@ -514,7 +527,7 @@ Deno.test("Messages history", async (t) => { const body = await res.json(); assertEquals(body.length, 1); - assertEquals(body.map((m: any) => m.flat), ["t1"]); + assertEquals(body.map((m: Record) => m.flat), ["t1"]); }); }); await agent.close(); diff --git a/deno/server/inter/http/routes/messages/__tests__/notifications.test.ts b/deno/server/inter/http/routes/messages/__tests__/notifications.test.ts index cdadaf27..e5211079 100644 --- a/deno/server/inter/http/routes/messages/__tests__/notifications.test.ts +++ b/deno/server/inter/http/routes/messages/__tests__/notifications.test.ts @@ -13,7 +13,7 @@ Deno.test("webhook should be sent", async (t) => { }, ], }); - const { promise, resolve } = Promise.withResolvers(); + const { promise, resolve } = Promise.withResolvers>(); const srv = Deno.serve({ port: 8123, handler: async (req) => { @@ -34,7 +34,7 @@ Deno.test("webhook should be sent", async (t) => { const event = await promise; assertEquals(event.type, "message"); - assertEquals(event.event.flat, "test"); + assertEquals((event.event as Record).flat, "test"); await srv.shutdown(); await app.close(); @@ -72,7 +72,7 @@ Deno.test("webhook should be called once", async (t) => { }, ], }); - const { promise, resolve } = Promise.withResolvers(); + const { promise, resolve } = Promise.withResolvers>(); const srv = Deno.serve({ port: 8123, handler: async (req) => { @@ -96,7 +96,7 @@ Deno.test("webhook should be called once", async (t) => { assertEquals(calls, 1); assertEquals(event.type, "message"); - assertEquals(event.event.flat, "test"); + assertEquals((event.event as Record).flat, "test"); await srv.shutdown(); await app.close(); diff --git a/deno/server/inter/http/routes/messages/__tests__/pinning.test.ts b/deno/server/inter/http/routes/messages/__tests__/pinning.test.ts index 49095954..bf7f42b2 100644 --- a/deno/server/inter/http/routes/messages/__tests__/pinning.test.ts +++ b/deno/server/inter/http/routes/messages/__tests__/pinning.test.ts @@ -23,26 +23,26 @@ Deno.test("Pinning other user messsage", async (t) => { flat: "Hello", message: { text: "Hello" }, clientId: "hello", - }, (msg: any) => { - pinMessageId = msg.id; + }, (msg) => { + pinMessageId = msg.id as string; }); await t.step("pinning message", async () => { await member.pinMessage({ messageId: pinMessageId }) - .getPinnedMessages((messages: any) => { + .getPinnedMessages((messages) => { assertEquals(messages.length, 1); assertEquals(messages[0].id, pinMessageId); }); - await admin.getPinnedMessages((messages: any) => { + await admin.getPinnedMessages((messages) => { assertEquals(messages.length, 1); assertEquals(messages[0].id, pinMessageId); }); }); await t.step("unpinning message by other user", async () => { await admin.pinMessage({ messageId: pinMessageId, pinned: false }) - .getPinnedMessages((messages: any) => { + .getPinnedMessages((messages) => { assertEquals(messages.length, 0); }); - await member.getPinnedMessages((messages: any) => { + await member.getPinnedMessages((messages) => { assertEquals(messages.length, 0); }); }); diff --git a/deno/server/inter/http/routes/messages/__tests__/react.test.ts b/deno/server/inter/http/routes/messages/__tests__/react.test.ts index 9f704db9..21d44abb 100644 --- a/deno/server/inter/http/routes/messages/__tests__/react.test.ts +++ b/deno/server/inter/http/routes/messages/__tests__/react.test.ts @@ -28,9 +28,13 @@ Deno.test("PUT /api/messages/:messageId/react - sending reacts to messages", asy messageId: state.messageId, reaction: ":thumbsup:", })) - .getMessages({}, (messages: any) => { - assertEquals(messages[0].reactions.length, 1); - assertEquals(messages[0].reactions[0], { + .getMessages({}, (messages) => { + const reactions = messages[0].reactions as Record< + string, + unknown + >[]; + assertEquals(reactions.length, 1); + assertEquals(reactions[0], { userId: admin.userId, reaction: ":thumbsup:", }); @@ -38,7 +42,7 @@ Deno.test("PUT /api/messages/:messageId/react - sending reacts to messages", asy }); await t.step("checking reactions by second user", async () => { - await member.getMessages({}, (messages: any, { state }) => { + await member.getMessages({}, (messages, { state }) => { state.messageId = messages[0].id; }); }); @@ -54,17 +58,21 @@ Deno.test("PUT /api/messages/:messageId/react - sending reacts to messages", asy messageId: state.messageId, reaction: ":thumbsup:", })) - .getMessages({}, (messages: any) => { - assertEquals(messages[0].reactions.length, 3); - assertEquals(messages[0].reactions[0], { + .getMessages({}, (messages) => { + const reactions = messages[0].reactions as Record< + string, + unknown + >[]; + assertEquals(reactions.length, 3); + assertEquals(reactions[0], { userId: admin.userId, reaction: ":thumbsup:", }); - assertEquals(messages[0].reactions[1], { + assertEquals(reactions[1], { userId: member.userId, reaction: ":thumbsdown:", }); - assertEquals(messages[0].reactions[2], { + assertEquals(reactions[2], { userId: member.userId, reaction: ":thumbsup:", }); @@ -77,13 +85,17 @@ Deno.test("PUT /api/messages/:messageId/react - sending reacts to messages", asy messageId: state.messageId, reaction: ":thumbsup:", })) - .getMessages({}, (messages: any) => { - assertEquals(messages[0].reactions.length, 2); - assertEquals(messages[0].reactions[0], { + .getMessages({}, (messages) => { + const reactions = messages[0].reactions as Record< + string, + unknown + >[]; + assertEquals(reactions.length, 2); + assertEquals(reactions[0], { userId: admin.userId, reaction: ":thumbsup:", }); - assertEquals(messages[0].reactions[1], { + assertEquals(reactions[1], { userId: member.userId, reaction: ":thumbsdown:", }); @@ -120,10 +132,11 @@ Deno.test("PUT /api/messages/:messageId/react - SSR about reactions", async (t) messageId: state.messageId, reaction: ":thumbsup:", })); - await member.nextEvent((event: any) => { + await member.nextEvent((event) => { + const reactions = event.reactions as Record[]; assertEquals(event.type, "message"); - assertEquals(event.reactions.length, 1); - assertEquals(event.reactions[0], { + assertEquals(reactions.length, 1); + assertEquals(reactions[0], { userId: admin.userId, reaction: ":thumbsup:", }); @@ -135,9 +148,10 @@ Deno.test("PUT /api/messages/:messageId/react - SSR about reactions", async (t) messageId: state.messageId, reaction: ":thumbsup:", })); - await member.nextEvent((event: any) => { + await member.nextEvent((event) => { + const reactions = event.reactions as Record[]; assertEquals(event.type, "message"); - assertEquals(event.reactions.length, 0); + assertEquals(reactions.length, 0); }); }); } finally { diff --git a/deno/server/inter/http/routes/messages/__tests__/threads.test.ts b/deno/server/inter/http/routes/messages/__tests__/threads.test.ts index 82545fcc..218f20a1 100644 --- a/deno/server/inter/http/routes/messages/__tests__/threads.test.ts +++ b/deno/server/inter/http/routes/messages/__tests__/threads.test.ts @@ -15,7 +15,7 @@ Deno.test("sending messages to threads", async (t) => { }) .sendMessage({ flat: "Hello", - }, (msg: any, { state }) => { + }, (msg, { state }) => { state.parentId = msg.id; }) .sendMessage({ @@ -23,15 +23,15 @@ Deno.test("sending messages to threads", async (t) => { }) .sendMessage(({ state }) => ({ flat: "msg1", parentId: state.parentId })) .sendMessage(({ state }) => ({ flat: "msg2", parentId: state.parentId })) - .getMessages({ parentId: null }, (messages: any) => { + .getMessages({ parentId: null }, (messages) => { assertEquals(messages.length, 3); - assertEquals(messages[1].thread.length, 2); + assertEquals((messages[1].thread as unknown[]).length, 2); }) .getMessages( ({ state }) => ({ parentId: state.parentId }), - (messages: any, { state }) => { + (messages, { state }) => { assertEquals(messages.length, 3); - assertEquals(messages[0].thread.length, 2); + assertEquals((messages[0].thread as unknown[]).length, 2); assertEquals(messages[0].id, state.parentId); }, ) diff --git a/deno/server/inter/http/routes/profile/__tests__/profile.test.ts b/deno/server/inter/http/routes/profile/__tests__/profile.test.ts index 50cf32b4..198c66c9 100644 --- a/deno/server/inter/http/routes/profile/__tests__/profile.test.ts +++ b/deno/server/inter/http/routes/profile/__tests__/profile.test.ts @@ -23,7 +23,7 @@ Deno.test("GET /api/profile/config - getConfig", async () => { await admin.login("admin") .createChannel({ name: "Test" }); await admin.executeCommand("/main", []) - .getConfig(async (body: any) => { + .getConfig(async (body) => { assertEquals(body.appVersion, "1.2.3"); assertEquals(body.mainChannelId, admin.channelId); }) diff --git a/deno/server/inter/http/routes/readReceipt/__tests__/readReceipt.test.ts b/deno/server/inter/http/routes/readReceipt/__tests__/readReceipt.test.ts index 697a5858..ed56ec08 100644 --- a/deno/server/inter/http/routes/readReceipt/__tests__/readReceipt.test.ts +++ b/deno/server/inter/http/routes/readReceipt/__tests__/readReceipt.test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "@std/assert"; +import { assert, assertEquals } from "@std/assert"; import { Chat } from "../../__tests__/chat.ts"; import { createApp } from "../../__tests__/app.ts"; @@ -19,10 +19,9 @@ Deno.test("/api/channels/:channelId/read-receipts", async () => }, (msg, { state }) => { state.messageId = msg.id; }) - .getChannelReadReceipts((receipts: any, self) => { - const receipt = receipts.find((r: any) => - r.channelId === self.channelId - ); + .getChannelReadReceipts((receipts, self) => { + const receipt = receipts.find((r) => r.channelId === self.channelId); + assert(receipt, "Receipt should exist"); assertEquals(receipt.count, 0); assertEquals(receipt.lastMessageId, self.state.messageId); }) @@ -51,12 +50,12 @@ Deno.test("/api/channels/:channelId/read-receipts - SSE", async () => await admin.connectSSE(); await member - .getMessages({}, async (messages: any, { state, channelId }) => { + .getMessages({}, async (messages, { state, channelId }) => { state.messageId = messages[0].id; }) .updateReadReceipts(({ state }) => state.messageId); - await admin.nextEvent((event: any) => { + await admin.nextEvent((event) => { assertEquals(event.type, "readReceipt"); assertEquals(event.lastMessageId, member.state.messageId); assertEquals(event.userId, member.userId); @@ -88,7 +87,7 @@ Deno.test("/api/read-receipts", async () => { clientId: "test2", }); await member - .getMessages({}, async (messages: any, { state }) => { + .getMessages({}, async (messages, { state }) => { state.messageId = messages[0].id; }) .updateReadReceipts(({ state }) => state.messageId); @@ -102,11 +101,10 @@ Deno.test("/api/read-receipts", async () => { message: { text: "Hello" }, clientId: "test4", }); - await member.getReadReceipts((receipts: any) => { + await member.getReadReceipts((receipts) => { assertEquals(receipts.length, 1); - const receipt = receipts.find((r: any) => - r.channelId === member.channelId - ); + const receipt = receipts.find((r) => r.channelId === member.channelId); + assert(receipt, "Receipt should exist"); assertEquals(receipt.count, 2); }); } finally { diff --git a/deno/server/inter/http/routes/users/__tests__/registration.test.ts b/deno/server/inter/http/routes/users/__tests__/registration.test.ts index 0d8b61c9..4e6a2ac1 100644 --- a/deno/server/inter/http/routes/users/__tests__/registration.test.ts +++ b/deno/server/inter/http/routes/users/__tests__/registration.test.ts @@ -21,8 +21,8 @@ Deno.test("POST /api/users - user creation flow", async (t) => { .sendMessage({ flat: "secret" }); await t.step("creating invite", async () => { - await admin.executeCommand("/invite", [], ({ json }: any) => { - url = json.data; + await admin.executeCommand("/invite", [], ({ json }) => { + url = json.data as string; }); const m = url.match(/https?:\/\/.*\/invite\/(.*)$/); assert(m); @@ -31,8 +31,8 @@ Deno.test("POST /api/users - user creation flow", async (t) => { await t.step("creating second invite", async () => { let url2: string = ""; - await admin.executeCommand("/invite", [], ({ json }: any) => { - url2 = json.data; + await admin.executeCommand("/invite", [], ({ json }) => { + url2 = json.data as string; }); const m = url2.match(/https?:\/\/.*\/invite\/(.*)$/); assert(m); @@ -66,7 +66,7 @@ Deno.test("POST /api/users - user creation flow", async (t) => { assert(session.secrets._iv); }) .openChannel("user-invite-test") - .getMessages({}, (msgs: any[]) => { + .getMessages({}, (msgs) => { assertEquals(msgs[0].flat, "secret"); }); }); diff --git a/deno/server/inter/http/routes/users/__tests__/users.test.ts b/deno/server/inter/http/routes/users/__tests__/users.test.ts index 6bad3001..2a059e06 100644 --- a/deno/server/inter/http/routes/users/__tests__/users.test.ts +++ b/deno/server/inter/http/routes/users/__tests__/users.test.ts @@ -29,7 +29,7 @@ Deno.test("GET /api/users/:userId - getUser with an id and alias", async () => { }) .getUser(({ state }) => state.member.id, async (user: User) => { assert(user.name === "Member"); - assert((user as any).secrets === undefined); + assert((user as Record).secrets === undefined); assert(user.publicKey); }) .end(); @@ -46,8 +46,8 @@ Deno.test("GET /api/users - getAllUsers", async () => { const userNames = users.map((u: User) => u.name); assert(userNames.includes("Admin")); assert(userNames.includes("Member")); - assert((users[0] as any).password === undefined); - assert((users[1] as any).password === undefined); + assert((users[0] as Record).password === undefined); + assert((users[1] as Record).password === undefined); }) .end(); }); @@ -63,8 +63,8 @@ Deno.test("POST /api/users - user creation flow", async () => { await admin.login("admin") .createChannel({ name: "user-invite-test" }) .sendMessage({ flat: "secret" }) - .executeCommand("/invite", [], ({ json }: any) => { - url = json.data; + .executeCommand("/invite", [], ({ json }) => { + url = json.data as string; }); const m = url.match(/https?:\/\/.*\/invite\/(.*)$/); assert(m); @@ -78,7 +78,7 @@ Deno.test("POST /api/users - user creation flow", async () => { }) .login("jack", "test123") .openChannel("user-invite-test") - .getMessages({}, (msgs: any[]) => { + .getMessages({}, (msgs) => { assertEquals(msgs[0].flat, "secret"); }) .end(); From 26c6730b3c32eff8e2b063e4404e73a5b5cdf870 Mon Sep 17 00:00:00 2001 From: Mateusz Russak Date: Mon, 15 Jun 2026 15:35:48 +0200 Subject: [PATCH 7/8] fix(storage): replace sharp with pure-WASM photon for thumbnails (#294) * fix(storage): replace sharp with pure-WASM photon for thumbnails * fix(api): type timeout ids as ReturnType * ci(tests): pin deno to 2.6.8 to match production runtime * chore: remove code comments --- .dockerignore | 21 ++++ .github/workflows/tests.yml | 2 +- Dockerfile | 2 +- deno.lock | 209 ++----------------------------- deno/api/mod.ts | 4 +- deno/storage/deno.json | 2 +- deno/storage/src/core/mod.ts | 79 +++++++----- deno/storage/src/core/streams.ts | 108 ---------------- 8 files changed, 88 insertions(+), 339 deletions(-) delete mode 100644 deno/storage/src/core/streams.ts diff --git a/.dockerignore b/.dockerignore index 0d07b429..f092d3e2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,24 @@ dist app/src-tauri chat.config.ts chat.config.js + +# Platform shells not part of the server image +desktop +mobile + +# Build outputs (frontend dist is rebuilt in the build stage and copied to /app/public) +app/dist +app/storybook-static + +# Dev / worktree / planning artifacts +.worktrees +.planning +.github +.vscode +.idea + +# Logs and local junk +*.log +file-service.log +.DS_Store +coverage diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b52820cb..9ebf6489 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@v3 - uses: denoland/setup-deno@v2 with: - deno-version: '2.x' + deno-version: '2.6.8' - run: deno install --allow-scripts - name: Install frontend dependencies working-directory: app diff --git a/Dockerfile b/Dockerfile index c1d996a9..074fc897 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ FROM denoland/deno:alpine-2.6.8 RUN mv /usr/local/lib /usr/local/lib.bak \ && mkdir /usr/local/lib \ && apk -U upgrade \ - && apk add vips-cpp build-base vips vips-dev \ + && apk add musl-dev \ && cp -a /usr/local/lib.bak/* /usr/local/lib/ \ && rm -rf /usr/local/lib.bak ENV ENVIRONMENT=production diff --git a/deno.lock b/deno.lock index 8a36ab14..ca41e706 100644 --- a/deno.lock +++ b/deno.lock @@ -40,6 +40,7 @@ "jsr:@std/path@1.1.2": "1.1.2", "jsr:@std/streams@~0.224.5": "0.224.5", "jsr:@ts-rex/bcrypt@1.0.3": "1.0.3", + "npm:@cf-wasm/photon@0.3.6": "0.3.6", "npm:@faker-js/faker@9.3.0": "9.3.0", "npm:@google-cloud/storage@7.16.0": "7.16.0", "npm:@jsr/planigale__sse@0.2.8": "0.2.8", @@ -55,7 +56,6 @@ "npm:mongodb@6.12.0": "6.12.0", "npm:mongodb@6.19.0": "6.19.0", "npm:nodemon@3.1.10": "3.1.10", - "npm:sharp@0.33.0": "0.33.0", "npm:valibot@0.31.1": "0.31.1", "npm:valibot@0.36.0": "0.36.0", "npm:valibot@1.1.0": "1.1.0", @@ -219,16 +219,19 @@ } }, "npm": { - "@emnapi/core@1.5.0": { - "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "@cf-wasm/internals@0.1.2": { + "integrity": "sha512-9d/I3JFv1IpQFYOrIw5RQShQPyuZRw9DyeBylF39Uj/MH7my8+EzKDPpUCHiqZ5O7tZS/A6zwP08egpeI5NBhA==" + }, + "@cf-wasm/photon@0.3.6": { + "integrity": "sha512-LfLfJ10+Z+DrohjQTSBkgmqxp6d4gvlGuwFkb5c4RHJGAMyVJxrAQWGrLunWE6TvZssIkT5fhKAjQZYhFSYzqg==", "dependencies": [ - "@emnapi/wasi-threads", - "tslib" + "@cf-wasm/internals" ] }, - "@emnapi/runtime@0.44.0": { - "integrity": "sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw==", + "@emnapi/core@1.5.0": { + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", "dependencies": [ + "@emnapi/wasi-threads", "tslib" ] }, @@ -281,127 +284,6 @@ "uuid@8.3.2" ] }, - "@img/sharp-darwin-arm64@0.33.0": { - "integrity": "sha512-070tEheekI1LJWTGPC9WlQEa5UoKTXzzlORBHMX4TbfUxMiL336YHR8vBEUNsjse0RJCX8dZ4ZXwT595aEF1ug==", - "optionalDependencies": [ - "@img/sharp-libvips-darwin-arm64" - ], - "os": ["darwin"], - "cpu": ["arm64"] - }, - "@img/sharp-darwin-x64@0.33.0": { - "integrity": "sha512-pu/nvn152F3qbPeUkr+4e9zVvEhD3jhwzF473veQfMPkOYo9aoWXSfdZH/E6F+nYC3qvFjbxbvdDbUtEbghLqw==", - "optionalDependencies": [ - "@img/sharp-libvips-darwin-x64" - ], - "os": ["darwin"], - "cpu": ["x64"] - }, - "@img/sharp-libvips-darwin-arm64@1.0.0": { - "integrity": "sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw==", - "os": ["darwin"], - "cpu": ["arm64"] - }, - "@img/sharp-libvips-darwin-x64@1.0.0": { - "integrity": "sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA==", - "os": ["darwin"], - "cpu": ["x64"] - }, - "@img/sharp-libvips-linux-arm64@1.0.0": { - "integrity": "sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA==", - "os": ["linux"], - "cpu": ["arm64"] - }, - "@img/sharp-libvips-linux-arm@1.0.0": { - "integrity": "sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw==", - "os": ["linux"], - "cpu": ["arm"] - }, - "@img/sharp-libvips-linux-s390x@1.0.0": { - "integrity": "sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw==", - "os": ["linux"], - "cpu": ["s390x"] - }, - "@img/sharp-libvips-linux-x64@1.0.0": { - "integrity": "sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==", - "os": ["linux"], - "cpu": ["x64"] - }, - "@img/sharp-libvips-linuxmusl-arm64@1.0.0": { - "integrity": "sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ==", - "os": ["linux"], - "cpu": ["arm64"] - }, - "@img/sharp-libvips-linuxmusl-x64@1.0.0": { - "integrity": "sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg==", - "os": ["linux"], - "cpu": ["x64"] - }, - "@img/sharp-linux-arm64@0.33.0": { - "integrity": "sha512-dcomVSrtgF70SyOr8RCOCQ8XGVThXwe71A1d8MGA+mXEVRJ/J6/TrCbBEJh9ddcEIIsrnrkolaEvYSHqVhswQw==", - "optionalDependencies": [ - "@img/sharp-libvips-linux-arm64" - ], - "os": ["linux"], - "cpu": ["arm64"] - }, - "@img/sharp-linux-arm@0.33.0": { - "integrity": "sha512-4horD3wMFd5a0ddbDY8/dXU9CaOgHjEHALAddXgafoR5oWq5s8X61PDgsSeh4Qupsdo6ycfPPSSNBrfVQnwwrg==", - "optionalDependencies": [ - "@img/sharp-libvips-linux-arm" - ], - "os": ["linux"], - "cpu": ["arm"] - }, - "@img/sharp-linux-s390x@0.33.0": { - "integrity": "sha512-TiVJbx38J2rNVfA309ffSOB+3/7wOsZYQEOlKqOUdWD/nqkjNGrX+YQGz7nzcf5oy2lC+d37+w183iNXRZNngQ==", - "optionalDependencies": [ - "@img/sharp-libvips-linux-s390x" - ], - "os": ["linux"], - "cpu": ["s390x"] - }, - "@img/sharp-linux-x64@0.33.0": { - "integrity": "sha512-PaZM4Zi7/Ek71WgTdvR+KzTZpBqrQOFcPe7/8ZoPRlTYYRe43k6TWsf4GVH6XKRLMYeSp8J89RfAhBrSP4itNA==", - "optionalDependencies": [ - "@img/sharp-libvips-linux-x64" - ], - "os": ["linux"], - "cpu": ["x64"] - }, - "@img/sharp-linuxmusl-arm64@0.33.0": { - "integrity": "sha512-1QLbbN0zt+32eVrg7bb1lwtvEaZwlhEsY1OrijroMkwAqlHqFj6R33Y47s2XUv7P6Ie1PwCxK/uFnNqMnkd5kg==", - "optionalDependencies": [ - "@img/sharp-libvips-linuxmusl-arm64" - ], - "os": ["linux"], - "cpu": ["arm64"] - }, - "@img/sharp-linuxmusl-x64@0.33.0": { - "integrity": "sha512-CecqgB/CnkvCWFhmfN9ZhPGMLXaEBXl4o7WtA6U3Ztrlh/s7FUKX4vNxpMSYLIrWuuzjiaYdfU3+Tdqh1xaHfw==", - "optionalDependencies": [ - "@img/sharp-libvips-linuxmusl-x64" - ], - "os": ["linux"], - "cpu": ["x64"] - }, - "@img/sharp-wasm32@0.33.0": { - "integrity": "sha512-Hn4js32gUX9qkISlemZBUPuMs0k/xNJebUNl/L6djnU07B/HAA2KaxRVb3HvbU5fL242hLOcp0+tR+M8dvJUFw==", - "dependencies": [ - "@emnapi/runtime@0.44.0" - ], - "cpu": ["wasm32"] - }, - "@img/sharp-win32-ia32@0.33.0": { - "integrity": "sha512-5HfcsCZi3l5nPRF2q3bllMVMDXBqEWI3Q8KQONfzl0TferFE5lnsIG0A1YrntMAGqvkzdW6y1Ci1A2uTvxhfzg==", - "os": ["win32"], - "cpu": ["ia32"] - }, - "@img/sharp-win32-x64@0.33.0": { - "integrity": "sha512-i3DtP/2ce1yKFj4OzOnOYltOEL/+dp4dc4dJXJBv6god1AFTcmkaA99H/7SwOmkCOBQkbVvA3lCGm3/5nDtf9Q==", - "os": ["win32"], - "cpu": ["x64"] - }, "@jsr/planigale__sse@0.2.8": { "integrity": "sha512-sI8OCEUGiNA1x6aQgXP3afBor1eZYfADsMnezaXdCTHNqSMc7H38q+iCzyYb1v6cC+so6A/HeIENpI9hP/MWMQ==", "dependencies": [ @@ -438,7 +320,7 @@ "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", "dependencies": [ "@emnapi/core", - "@emnapi/runtime@1.5.0", + "@emnapi/runtime", "@tybys/wasm-util" ] }, @@ -716,29 +598,6 @@ "fsevents" ] }, - "color-convert@2.0.1": { - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": [ - "color-name" - ] - }, - "color-name@1.1.4": { - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "color-string@1.9.1": { - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dependencies": [ - "color-name", - "simple-swizzle" - ] - }, - "color@4.2.3": { - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dependencies": [ - "color-convert", - "color-string" - ] - }, "combined-stream@1.0.8": { "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dependencies": [ @@ -779,9 +638,6 @@ "delayed-stream@1.0.0": { "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, - "detect-libc@2.0.4": { - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==" - }, "dom-serializer@2.0.0": { "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dependencies": [ @@ -1041,9 +897,6 @@ "inherits@2.0.4": { "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "is-arrayish@0.3.4": { - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==" - }, "is-binary-path@2.1.0": { "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dependencies": [ @@ -1263,42 +1116,6 @@ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "bin": true }, - "sharp@0.33.0": { - "integrity": "sha512-99DZKudjm/Rmz+M0/26t4DKpXyywAOJaayGS9boEn7FvgtG0RYBi46uPE2c+obcJRtA3AZa0QwJot63gJQ1F0Q==", - "dependencies": [ - "color", - "detect-libc", - "semver" - ], - "optionalDependencies": [ - "@img/sharp-darwin-arm64", - "@img/sharp-darwin-x64", - "@img/sharp-libvips-darwin-arm64", - "@img/sharp-libvips-darwin-x64", - "@img/sharp-libvips-linux-arm", - "@img/sharp-libvips-linux-arm64", - "@img/sharp-libvips-linux-s390x", - "@img/sharp-libvips-linux-x64", - "@img/sharp-libvips-linuxmusl-arm64", - "@img/sharp-libvips-linuxmusl-x64", - "@img/sharp-linux-arm", - "@img/sharp-linux-arm64", - "@img/sharp-linux-s390x", - "@img/sharp-linux-x64", - "@img/sharp-linuxmusl-arm64", - "@img/sharp-linuxmusl-x64", - "@img/sharp-wasm32", - "@img/sharp-win32-ia32", - "@img/sharp-win32-x64" - ], - "scripts": true - }, - "simple-swizzle@0.2.4": { - "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", - "dependencies": [ - "is-arrayish" - ] - }, "simple-update-notifier@2.0.0": { "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dependencies": [ @@ -1517,12 +1334,12 @@ "jsr:@std/fs@0.221.0", "jsr:@std/media-types@1.0.2", "jsr:@std/path@1.0.1", + "npm:@cf-wasm/photon@0.3.6", "npm:@google-cloud/storage@7.16.0", "npm:@types/node@20.10.4", "npm:concat-stream@2.0.0", "npm:google-auth-library@9.15.0", - "npm:mongodb@6.12.0", - "npm:sharp@0.33.0" + "npm:mongodb@6.12.0" ] } } diff --git a/deno/api/mod.ts b/deno/api/mod.ts index 6bc4dcae..85c040c8 100644 --- a/deno/api/mod.ts +++ b/deno/api/mod.ts @@ -88,7 +88,7 @@ class API extends EventTarget { sseEnabled: boolean; - reconnectTimeout: number | undefined; + reconnectTimeout: ReturnType | undefined; set token(value: string | undefined) { if (typeof value === "string" && value.trim() !== "") { @@ -466,7 +466,7 @@ class API extends EventTarget { async sendMessage(msg: Partial): Promise> { return await new Promise((resolve, reject) => { const data = { ...msg }; - let timeoutId: number | null = setTimeout(() => { + let timeoutId: ReturnType | null = setTimeout(() => { timeoutId = null; reject(new Error("Timeout")); }, 5000); diff --git a/deno/storage/deno.json b/deno/storage/deno.json index d671a526..2f5f173e 100644 --- a/deno/storage/deno.json +++ b/deno/storage/deno.json @@ -16,7 +16,7 @@ "@types/node": "npm:@types/node@20.10.4", "concat-stream": "npm:concat-stream@2.0.0", "mongodb": "npm:mongodb@6.12.0", - "sharp": "npm:sharp@0.33.0", + "@cf-wasm/photon": "npm:@cf-wasm/photon@0.3.6", "google-auth-library": "npm:google-auth-library@9.15.0" } } diff --git a/deno/storage/src/core/mod.ts b/deno/storage/src/core/mod.ts index 8432b626..26a9e06b 100644 --- a/deno/storage/src/core/mod.ts +++ b/deno/storage/src/core/mod.ts @@ -1,6 +1,5 @@ -import sharp from "sharp"; +import { PhotonImage, resize, SamplingFilter } from "@cf-wasm/photon/node"; import type { Config } from "@quack/config"; -import { toNodeStream, toWebStream } from "./streams.ts"; import type { FileData, FileOpts } from "./types.ts"; import { files } from "./store/mod.ts"; import { ApiError } from "@planigale/planigale"; @@ -20,21 +19,9 @@ interface FileService { exists(id: string): Promise; } -class Files { - _sharp: typeof sharp | null | undefined = undefined; - async getSharp() { - if (this._sharp === undefined) { - try { - const { default: sharp } = await import("sharp"); - this._sharp = sharp; - } catch (e) { - console.warn("[WARNING] sharp not available", e); - this._sharp = null; - } - } - return this._sharp; - } +const MAX_RESIZE_BYTES = 25 * 1024 * 1024; +class Files { static getFileId = (id: string, width = 0, height = 0) => `${id}-${width}x${height}`; @@ -42,7 +29,6 @@ class Files { constructor(config: Config) { this.init(config.storage); - this.getSharp(); } init(config: Config["storage"]) { @@ -74,28 +60,61 @@ class Files { throw new ApiError(404, "FILE_NOT_FOUND", "File not found"); } - const sharp = await this.getSharp(); const file = await this.service.get(id); if ( - !sharp || - !opts || !opts.width || !opts.height || + (!width && !height) || + file.size > MAX_RESIZE_BYTES || (file.contentType !== "image/jpeg" && file.contentType !== "image/png") ) { return file; } - await this.service.upload( - toWebStream( - toNodeStream(file.stream).pipe(sharp().resize(width, height)), - ), - { - id: targetId, - filename: file.filename, - contentType: file.contentType, - }, - ); + const resized = await this.scale(file, width, height); + if (!resized) { + return this.service.get(id); + } + + await this.service.upload(resized, { + id: targetId, + filename: file.filename, + contentType: file.contentType, + }); return this.service.get(targetId); } + + private async scale( + file: FileData, + width?: number, + height?: number, + ): Promise | null> { + let img: PhotonImage | undefined; + let out: PhotonImage | undefined; + try { + const bytes = new Uint8Array( + await new Response(file.stream).arrayBuffer(), + ); + img = PhotonImage.new_from_byteslice(bytes); + + const ow = img.get_width(); + const oh = img.get_height(); + let w = width || 0; + let h = height || 0; + if (!w) w = Math.max(1, Math.round((ow / oh) * h)); + if (!h) h = Math.max(1, Math.round((oh / ow) * w)); + + out = resize(img, w, h, SamplingFilter.Lanczos3); + const result = file.contentType === "image/png" + ? out.get_bytes() + : out.get_bytes_jpeg(90); + return new Blob([new Uint8Array(result)]).stream(); + } catch (e) { + console.warn("[storage] thumbnail resize failed, serving original", e); + return null; + } finally { + img?.free(); + out?.free(); + } + } } export type Storage = Files; diff --git a/deno/storage/src/core/streams.ts b/deno/storage/src/core/streams.ts deleted file mode 100644 index 72327526..00000000 --- a/deno/storage/src/core/streams.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Readable } from "node:stream"; - -export function toWebStream(nodeStream: Readable) { - let destroyed = false; - // deno-lint-ignore no-explicit-any - const listeners: Record void> = {}; - - function start(controller: ReadableStreamDefaultController) { - listeners.data = onData; - listeners.end = onData; - listeners.end = onDestroy; - listeners.close = onDestroy; - listeners.error = onDestroy; - for (const name in listeners) { - nodeStream.on(name, listeners[name]); - } - - nodeStream.pause(); - - function onData(chunk: Uint8Array) { - if (destroyed) return; - controller.enqueue(new Uint8Array(chunk)); - nodeStream.pause(); - } - - function onDestroy(err?: Error) { - if (destroyed) return; - destroyed = true; - - for (const name in listeners) { - nodeStream.removeListener(name, listeners[name]); - } - - if (err) controller.error(err); - else controller.close(); - } - } - - function pull() { - if (destroyed) return; - nodeStream.resume(); - } - - function cancel() { - destroyed = true; - - for (const name in listeners) { - nodeStream.removeListener(name, listeners[name]); - } - - nodeStream.push(null); - nodeStream.pause(); - if (nodeStream.destroy) nodeStream.destroy(); - } - - return new ReadableStream({ start, pull, cancel }); -} - -class NodeReadable extends Readable { - public bytesRead = 0; - - public released = false; - - private reader: ReadableStreamDefaultReader; - - private pendingRead?: Promise>; - - constructor(stream: ReadableStream) { - super(); - this.reader = stream.getReader(); - } - - public override async _read() { - if (this.released) { - this.push(null); - return; - } - this.pendingRead = this.reader.read(); - const data = await this.pendingRead; - delete this.pendingRead; - if (data.done || this.released) { - this.push(null); - } else { - this.bytesRead += data.value.length; - this.push(data.value); - } - } - - public async waitForReadToComplete() { - if (this.pendingRead) { - await this.pendingRead; - } - } - - public async close(): Promise { - await this.syncAndRelease(); - } - - private async syncAndRelease() { - this.released = true; - await this.waitForReadToComplete(); - this.reader.releaseLock(); - } -} - -export function toNodeStream(webStream: ReadableStream) { - return new NodeReadable(webStream); -} From e338ba0e50d8fe05e201525bf222c8961f215a03 Mon Sep 17 00:00:00 2001 From: Mateusz Russak Date: Mon, 15 Jun 2026 15:49:02 +0200 Subject: [PATCH 8/8] ci: pin deno to 2.6.8 across remaining workflows (#296) --- .github/workflows/apps.yml | 2 +- .github/workflows/dev.yml | 2 +- .github/workflows/docker.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/apps.yml b/.github/workflows/apps.yml index 7c96d69a..9d9ccceb 100644 --- a/.github/workflows/apps.yml +++ b/.github/workflows/apps.yml @@ -18,7 +18,7 @@ jobs: - uses: denoland/setup-deno@v2 with: - deno-version: '2.x' + deno-version: '2.6.8' - run: deno install --allow-scripts diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index f9b0ed16..c3d368f9 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -33,7 +33,7 @@ jobs: - uses: actions/checkout@v3 - uses: denoland/setup-deno@v2 with: - deno-version: '2.x' + deno-version: '2.6.8' - run: deno install --allow-scripts - name: Install frontend dependencies working-directory: app diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 78c2a3ad..7fd1925b 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -43,7 +43,7 @@ jobs: - uses: actions/checkout@v3 - uses: denoland/setup-deno@v2 with: - deno-version: '2.x' + deno-version: '2.6.8' - run: deno install --allow-scripts - run: deno task migrate:tests - run: deno task check