From f127603bc0dd9dc1fc7870a7d6ac1b173aa8bd15 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Tue, 24 Jun 2025 00:22:33 -0300 Subject: [PATCH 01/28] initial implementation of WebSocket client with error handling and buffer management --- packages/ws/.npmignore | 3 + packages/ws/esbuild.mjs | 35 +++++ packages/ws/package.json | 32 +++++ packages/ws/src/client.ts | 171 +++++++++++++++++++++++ packages/ws/src/constants.ts | 18 +++ packages/ws/src/errors/BufferOverflow.ts | 7 + packages/ws/src/errors/index.ts | 1 + packages/ws/src/index.ts | 6 + packages/ws/src/types.ts | 47 +++++++ packages/ws/src/utils/buffer.ts | 22 +++ packages/ws/src/utils/index.ts | 1 + packages/ws/tsconfig.json | 7 + packages/ws/typedoc.json | 3 + 13 files changed, 353 insertions(+) create mode 100644 packages/ws/.npmignore create mode 100644 packages/ws/esbuild.mjs create mode 100644 packages/ws/package.json create mode 100644 packages/ws/src/client.ts create mode 100644 packages/ws/src/constants.ts create mode 100644 packages/ws/src/errors/BufferOverflow.ts create mode 100644 packages/ws/src/errors/index.ts create mode 100644 packages/ws/src/index.ts create mode 100644 packages/ws/src/types.ts create mode 100644 packages/ws/src/utils/buffer.ts create mode 100644 packages/ws/src/utils/index.ts create mode 100644 packages/ws/tsconfig.json create mode 100644 packages/ws/typedoc.json diff --git a/packages/ws/.npmignore b/packages/ws/.npmignore new file mode 100644 index 000000000..f72befe76 --- /dev/null +++ b/packages/ws/.npmignore @@ -0,0 +1,3 @@ +__test__/ +out/** +src/** diff --git a/packages/ws/esbuild.mjs b/packages/ws/esbuild.mjs new file mode 100644 index 000000000..e8b988f41 --- /dev/null +++ b/packages/ws/esbuild.mjs @@ -0,0 +1,35 @@ +import { context } from "esbuild"; +import { esbuildDefaultPlugins } from "../../esbuild.mjs"; + +async function main() { + const production = process.argv.includes("--production"); + const watch = process.argv.includes("--watch"); + + const ctx = await context({ + entryPoints: ["src/index.ts"], + bundle: true, + format: "cjs", + minify: production, + sourcemap: "inline", + sourcesContent: false, + platform: "node", + outdir: "dist", + logLevel: "warning", + packages: "external", + plugins: esbuildDefaultPlugins, + }); + + if (watch) { + await ctx.watch(); + } else { + await ctx.rebuild(); + await ctx.dispose(); + } +} + +try { + await main(); +} catch (error) { + console.error(error); + process.exit(1); +} diff --git a/packages/ws/package.json b/packages/ws/package.json new file mode 100644 index 000000000..cde678b65 --- /dev/null +++ b/packages/ws/package.json @@ -0,0 +1,32 @@ +{ + "name": "@discloudapp/ws", + "version": "0.1.0", + "description": "A WebSocket for discloud.app", + "main": "dist", + "types": "dist/index.d.ts", + "scripts": { + "watch": "npm-run-all -p watch:*", + "watch:esbuild": "node esbuild.mjs --watch", + "watch:tsc": "tsc --noEmit --watch", + "prepublish": "node esbuild.mjs --production && tsc --emitDeclarationOnly --outDir dist", + "release:pre": "npm version pre --legacy-peer-deps --no-git-tag-version && npm run prepublish && npm publish --tag=beta", + "release": "npm run prepublish && npm publish", + "test": "tsc --noEmit && tsc && npm run test:node", + "test:node": "node --test" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/discloud/discloud.app.git" + }, + "keywords": [ + "discloud", + "discloud.app" + ], + "license": "Apache-2.0", + "dependencies": { + "ws": "^8.18.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts new file mode 100644 index 000000000..dba3eb5c4 --- /dev/null +++ b/packages/ws/src/client.ts @@ -0,0 +1,171 @@ +import EventEmitter from "events"; +import WebSocket from "ws"; +import { DEFAULT_CHUNK_SIZE, MAX_FILE_SIZE, NETWORK_UNREACHABLE_CODE } from "./constants"; +import { BufferOverflowError } from "./errors"; +import { type OnProgressCallback, type SocketEventsMap, type SocketOptions } from "./types"; + +export class SocketClient | any[] = Record | any[]> + extends EventEmitter> + implements Disposable { + constructor(protected wsURL: URL, options?: SocketOptions) { + super({ captureRejections: true }); + + if (options) { + if (options.connectingTimeout !== undefined) + this._connectingTimeout = options.connectingTimeout; + + if (typeof options.disposeOnClose === "boolean") + this._disposeOnClose = options.disposeOnClose; + + if (options.headers) Object.assign(this._headers, options.headers); + } + } + + protected readonly _connectingTimeout: number | null = 10_000; + protected readonly _disposeOnClose: boolean = true; + protected readonly _headers: Record = {}; + declare protected _socket?: WebSocket; + declare protected _ping: number; + declare protected _pong: number; + declare ping: number; + + get closed() { return !this._socket || this._socket.readyState === this._socket.CLOSED; } + get closing() { return this._socket ? this._socket.readyState === this._socket.CLOSING : false; } + get connected() { return this._socket ? this._socket.readyState === this._socket.OPEN : false; } + get connecting() { return this._socket ? this._socket.readyState === this._socket.CONNECTING : false; } + + close() { + if (this._socket) { + this._socket.removeAllListeners().close(); + delete this._socket; + } + } + + dispose() { + this[Symbol.dispose](); + } + + async connect() { + await new Promise((resolve, reject) => { + if (this.connected) return resolve(); + this.#createWebSocket().then(resolve).catch(reject); + }); + } + + async #waitConnect() { + await new Promise((resolve, reject) => { + if (this.connecting) { + const onConnect = () => { + this.off("close", onClose); + resolve(); + }; + const onClose = () => { + this.off("connect", onConnect); + reject(); + }; + return this.once("connect", onConnect).once("close", onClose); + } + if (this.connected) return resolve(); + reject(); + }); + } + + async sendJSON(value: Record): Promise { + if (!this.connected) await this.connect(); + + await new Promise((resolve, reject) => { + this._socket!.send(JSON.stringify(value), (err) => { + if (err) return reject(err); + resolve(); + }); + }); + } + + async sendBuffer(buffer: Buffer) { + if (!this.connected) await this.connect(); + + await new Promise((resolve, reject) => { + this._socket!.send(buffer, (err) => { + if (err) return reject(err); + resolve(); + }); + }); + } + + async sendFile(buffer: Buffer, onProgress?: OnProgressCallback) { + if (buffer.length > MAX_FILE_SIZE) throw new BufferOverflowError(); + + const total = Math.ceil(buffer.length / DEFAULT_CHUNK_SIZE); + const chunkSize = Math.ceil(buffer.length / total); + + for (let i = 0; i < total;) { + const start = chunkSize * i; + const end = start + chunkSize; + const chunk = buffer.subarray(start, end); + const current = ++i; + const pending = current < total; + + await this.sendJSON({ current, chunk, pending, total }); + + await onProgress?.({ current, total }); + } + } + + #createWebSocket() { + return new Promise((resolve, reject) => { + if (this.connecting) return this.#waitConnect().then(resolve).catch(reject); + + if (this.connected) return resolve(); + + this.emit("connecting"); + + const options: ConstructorParameters[2] = { + headers: this._headers, + ...typeof this._connectingTimeout === "number" + ? { signal: AbortSignal.timeout(this._connectingTimeout) } + : {}, + }; + + let _error!: any; + + this._socket = new WebSocket(this.wsURL, options) + .once("close", (code, reason) => { + if (this._disposeOnClose) queueMicrotask(() => this.dispose()); + + if (_error) { + switch (_error.code) { + case NETWORK_UNREACHABLE_CODE: return; + } + } + + this.emit("close", code, reason); + }) + .on("error", (error) => { + this.emit("error", _error = error); + }) + .on("message", (data) => { + try { this.emit("data", JSON.parse(data.toString())); } + catch { this.emit("message", data); } + }) + .once("open", () => { + this._ping = Date.now(); + this._socket!.ping(); + this.emit("connect"); + resolve(); + }) + .on("ping", () => { + this._ping = Date.now(); + this._socket!.ping(); + }) + .on("pong", () => { + this._pong = Date.now(); + this.ping = this._pong - this._ping; + }); + }); + } + + [Symbol.dispose]() { + this.close(); + this.removeAllListeners(); + } +} diff --git a/packages/ws/src/constants.ts b/packages/ws/src/constants.ts new file mode 100644 index 000000000..bd52b6555 --- /dev/null +++ b/packages/ws/src/constants.ts @@ -0,0 +1,18 @@ +/** `64KB` */ +export const DEFAULT_CHUNK_SIZE = 65_536; + +/** `100MB` */ +export const MAX_BUFFER_SIZE = 104_857_600; + +/** `1MB` */ +export const MAX_CHUNK_SIZE = 1_048_576; + +/** `512MB` */ +export const MAX_FILE_SIZE = 536_870_912; + +/** `8KB` */ +export const MIN_CHUNK_SIZE = 8_192; + +export const NETWORK_UNREACHABLE_ERRNO = -3008 as const; + +export const NETWORK_UNREACHABLE_CODE = "ENOTFOUND" as const; diff --git a/packages/ws/src/errors/BufferOverflow.ts b/packages/ws/src/errors/BufferOverflow.ts new file mode 100644 index 000000000..3e0cab302 --- /dev/null +++ b/packages/ws/src/errors/BufferOverflow.ts @@ -0,0 +1,7 @@ +export class BufferOverflowError extends Error { + readonly name = "BufferOverflow"; + + constructor() { + super(); + } +} diff --git a/packages/ws/src/errors/index.ts b/packages/ws/src/errors/index.ts new file mode 100644 index 000000000..b757932e7 --- /dev/null +++ b/packages/ws/src/errors/index.ts @@ -0,0 +1 @@ +export * from "./BufferOverflow"; diff --git a/packages/ws/src/index.ts b/packages/ws/src/index.ts new file mode 100644 index 000000000..d9c9349ab --- /dev/null +++ b/packages/ws/src/index.ts @@ -0,0 +1,6 @@ +export * from "./client"; +export * from "./constants"; +export * from "./errors"; +export * from "./types"; +export * from "./utils"; + diff --git a/packages/ws/src/types.ts b/packages/ws/src/types.ts new file mode 100644 index 000000000..d3cebcb72 --- /dev/null +++ b/packages/ws/src/types.ts @@ -0,0 +1,47 @@ +import { type ApiUploadApp } from "discloud.app"; +import { type RawData } from "ws"; + +export interface SocketEventsMap | any[] = Record | any[]> { + connecting: [] + connect: [] + close: [code: number, reason: Buffer] + data: [data: Data] + error: [error: Error] + message: [data: RawData] +} + +export interface SocketOptions { + /** + * Connecting timeout in milliseconds + * + * @default 10_000 + */ + connectingTimeout?: number | null + /** + * @default true + */ + disposeOnClose?: boolean + headers?: Record +} + +export interface SocketEventUploadData { + app?: ApiUploadApp + logs?: string + message: string | null + progress: SocketProgressData + status: "ok" | "error" + statusCode: number +} + +export interface SocketProgressData { + /** `0 - 100` */ + bar: number + log: string +} + +export interface ProgressData { + current: number + total: number +} + +export type OnProgressCallback = (data: ProgressData) => unknown | Promise diff --git a/packages/ws/src/utils/buffer.ts b/packages/ws/src/utils/buffer.ts new file mode 100644 index 000000000..9dbc9d01c --- /dev/null +++ b/packages/ws/src/utils/buffer.ts @@ -0,0 +1,22 @@ +import { DEFAULT_CHUNK_SIZE, MAX_CHUNK_SIZE, MIN_CHUNK_SIZE } from "../constants"; + +/** + * This is a Buffer chunk generator + * + * ```js + * for (const chunk of splitBuffer(buffer, chunkSize)) { + * // ... + * } + * ``` + * + * @param chunkSize + * Limited between `8_192` (`8KB`) and `1_048_576` (`1MB`) + * Default `65_536` (`64KB`) + */ +export function* splitBuffer(buffer: Buffer, chunkSize: number = DEFAULT_CHUNK_SIZE) { + chunkSize = Math.max(MIN_CHUNK_SIZE, Math.min(MAX_CHUNK_SIZE, chunkSize)); + + for (let i = 0; i < buffer.length;) { + yield buffer.subarray(i, i += chunkSize); + } +} diff --git a/packages/ws/src/utils/index.ts b/packages/ws/src/utils/index.ts new file mode 100644 index 000000000..bfea5c780 --- /dev/null +++ b/packages/ws/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./buffer"; diff --git a/packages/ws/tsconfig.json b/packages/ws/tsconfig.json new file mode 100644 index 000000000..659491e83 --- /dev/null +++ b/packages/ws/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "outDir": "out" + } +} diff --git a/packages/ws/typedoc.json b/packages/ws/typedoc.json new file mode 100644 index 000000000..be8ce11f3 --- /dev/null +++ b/packages/ws/typedoc.json @@ -0,0 +1,3 @@ +{ + "entryPoints": ["./src/index.ts"] +} From b2ba75557798523fd198d0d930e543c67bc0da52 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Tue, 24 Jun 2025 00:37:59 -0300 Subject: [PATCH 02/28] refactor: rename event handlers for clarity and update event types --- packages/ws/src/client.ts | 34 +++++++++++++++++++++++++--------- packages/ws/src/types.ts | 3 ++- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index dba3eb5c4..657d6cf99 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -55,15 +55,15 @@ export class SocketClient | any[] = Record((resolve, reject) => { if (this.connecting) { - const onConnect = () => { + const onConnected = () => { this.off("close", onClose); resolve(); }; const onClose = () => { - this.off("connect", onConnect); + this.off("connected", onConnected); reject(); }; - return this.once("connect", onConnect).once("close", onClose); + return this.once("connected", onConnected).once("close", onClose); } if (this.connected) return resolve(); reject(); @@ -126,31 +126,47 @@ export class SocketClient | any[] = Record { if (this._disposeOnClose) queueMicrotask(() => this.dispose()); - if (_error) { - switch (_error.code) { - case NETWORK_UNREACHABLE_CODE: return; + if (!status.connected) return this.emit("connectionFailed"); + + status.connected = false; + + if (status.error) { + const error = status.error; + delete status.error; + + switch (error.code) { + case NETWORK_UNREACHABLE_CODE: + return this.emit("connectionFailed"); } } this.emit("close", code, reason); }) .on("error", (error) => { - this.emit("error", _error = error); + this.emit("error", status.error = error); }) .on("message", (data) => { try { this.emit("data", JSON.parse(data.toString())); } catch { this.emit("message", data); } }) .once("open", () => { + status.connected = true; + status.error = null; + this._ping = Date.now(); this._socket!.ping(); - this.emit("connect"); + + this.emit("connected"); + resolve(); }) .on("ping", () => { diff --git a/packages/ws/src/types.ts b/packages/ws/src/types.ts index d3cebcb72..3fd209d53 100644 --- a/packages/ws/src/types.ts +++ b/packages/ws/src/types.ts @@ -2,8 +2,9 @@ import { type ApiUploadApp } from "discloud.app"; import { type RawData } from "ws"; export interface SocketEventsMap | any[] = Record | any[]> { + connected: [] connecting: [] - connect: [] + connectionFailed: [] close: [code: number, reason: Buffer] data: [data: Data] error: [error: Error] From f7399d2e82c6e643fd5bd7fdd9d1b02006c562cc Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Tue, 24 Jun 2025 09:56:35 -0300 Subject: [PATCH 03/28] feat: add unauthorized event handling and corresponding constant --- packages/ws/src/client.ts | 7 ++++++- packages/ws/src/constants.ts | 2 ++ packages/ws/src/types.ts | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index 657d6cf99..c1d77efb1 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -1,6 +1,6 @@ import EventEmitter from "events"; import WebSocket from "ws"; -import { DEFAULT_CHUNK_SIZE, MAX_FILE_SIZE, NETWORK_UNREACHABLE_CODE } from "./constants"; +import { DEFAULT_CHUNK_SIZE, MAX_FILE_SIZE, NETWORK_UNREACHABLE_CODE, SOCKET_UNAUTHORIZED_CODE } from "./constants"; import { BufferOverflowError } from "./errors"; import { type OnProgressCallback, type SocketEventsMap, type SocketOptions } from "./types"; @@ -135,6 +135,11 @@ export class SocketClient | any[] = Record { if (this._disposeOnClose) queueMicrotask(() => this.dispose()); + switch (code) { + case SOCKET_UNAUTHORIZED_CODE: + return this.emit("unauthorized"); + } + if (!status.connected) return this.emit("connectionFailed"); status.connected = false; diff --git a/packages/ws/src/constants.ts b/packages/ws/src/constants.ts index bd52b6555..8f0878386 100644 --- a/packages/ws/src/constants.ts +++ b/packages/ws/src/constants.ts @@ -16,3 +16,5 @@ export const MIN_CHUNK_SIZE = 8_192; export const NETWORK_UNREACHABLE_ERRNO = -3008 as const; export const NETWORK_UNREACHABLE_CODE = "ENOTFOUND" as const; + +export const SOCKET_UNAUTHORIZED_CODE = 3000 as const; diff --git a/packages/ws/src/types.ts b/packages/ws/src/types.ts index 3fd209d53..fef17e1a5 100644 --- a/packages/ws/src/types.ts +++ b/packages/ws/src/types.ts @@ -9,6 +9,7 @@ export interface SocketEventsMap | any[] = Record< data: [data: Data] error: [error: Error] message: [data: RawData] + unauthorized: [] } export interface SocketOptions { From aa36d8b2ac509c20920cb62c673cff777e370895 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Tue, 24 Jun 2025 10:07:36 -0300 Subject: [PATCH 04/28] fix: reorder close event definition in SocketEventsMap for consistency --- packages/ws/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ws/src/types.ts b/packages/ws/src/types.ts index fef17e1a5..481e729f7 100644 --- a/packages/ws/src/types.ts +++ b/packages/ws/src/types.ts @@ -2,10 +2,10 @@ import { type ApiUploadApp } from "discloud.app"; import { type RawData } from "ws"; export interface SocketEventsMap | any[] = Record | any[]> { + close: [code: number, reason: Buffer] connected: [] connecting: [] connectionFailed: [] - close: [code: number, reason: Buffer] data: [data: Data] error: [error: Error] message: [data: RawData] From 27c83c573824ff44fdf73375bda5f2ca4b21460e Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Tue, 24 Jun 2025 10:47:49 -0300 Subject: [PATCH 05/28] feat: add configurable chunk size to SocketClient and BufferOverflowError --- packages/ws/src/client.ts | 23 ++++++++--------------- packages/ws/src/errors/BufferOverflow.ts | 4 +++- packages/ws/src/types.ts | 10 ++++++++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index c1d77efb1..00de42b4d 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -11,6 +11,9 @@ export class SocketClient | any[] = Record | any[] = Record = {}; @@ -81,21 +85,10 @@ export class SocketClient | any[] = Record((resolve, reject) => { - this._socket!.send(buffer, (err) => { - if (err) return reject(err); - resolve(); - }); - }); - } - - async sendFile(buffer: Buffer, onProgress?: OnProgressCallback) { - if (buffer.length > MAX_FILE_SIZE) throw new BufferOverflowError(); + async sendBuffer(buffer: Buffer, onProgress?: OnProgressCallback) { + if (buffer.length > MAX_FILE_SIZE) throw new BufferOverflowError(MAX_FILE_SIZE); - const total = Math.ceil(buffer.length / DEFAULT_CHUNK_SIZE); + const total = Math.ceil(buffer.length / this._chunkSize); const chunkSize = Math.ceil(buffer.length / total); for (let i = 0; i < total;) { @@ -105,7 +98,7 @@ export class SocketClient | any[] = Record | any[] = Record | any[]> { close: [code: number, reason: Buffer] @@ -13,6 +13,12 @@ export interface SocketEventsMap | any[] = Record< } export interface SocketOptions { + /** + * Set the buffer chunk size per message + * + * @default 65_536 (64KB) + */ + chunkSize?: number /** * Connecting timeout in milliseconds * From e9640d8e41bccdd3c47c62c7ecf82426c83177c1 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Tue, 24 Jun 2025 11:10:34 -0300 Subject: [PATCH 06/28] fix: update documentation for chunkSize to clarify potential issues with large chunks --- packages/ws/src/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ws/src/types.ts b/packages/ws/src/types.ts index 86a69e492..22b0cccdc 100644 --- a/packages/ws/src/types.ts +++ b/packages/ws/src/types.ts @@ -16,6 +16,8 @@ export interface SocketOptions { /** * Set the buffer chunk size per message * + * Note that very large chunks may cause unexpected closure + * * @default 65_536 (64KB) */ chunkSize?: number From 840323a4ebc510deda0a127a5ed55316a8c5566c Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Tue, 24 Jun 2025 11:48:55 -0300 Subject: [PATCH 07/28] fix: update default chunk size to 256KB in constants and types --- packages/ws/src/constants.ts | 4 ++-- packages/ws/src/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ws/src/constants.ts b/packages/ws/src/constants.ts index 8f0878386..ad7dd5e59 100644 --- a/packages/ws/src/constants.ts +++ b/packages/ws/src/constants.ts @@ -1,5 +1,5 @@ -/** `64KB` */ -export const DEFAULT_CHUNK_SIZE = 65_536; +/** `256KB` */ +export const DEFAULT_CHUNK_SIZE = 262_144; /** `100MB` */ export const MAX_BUFFER_SIZE = 104_857_600; diff --git a/packages/ws/src/types.ts b/packages/ws/src/types.ts index 22b0cccdc..e0b7caf8c 100644 --- a/packages/ws/src/types.ts +++ b/packages/ws/src/types.ts @@ -18,7 +18,7 @@ export interface SocketOptions { * * Note that very large chunks may cause unexpected closure * - * @default 65_536 (64KB) + * @default 262_144 (256KB) */ chunkSize?: number /** From 59f7321f1114acf36fea3ba9fed9dd640d1327ab Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Tue, 24 Jun 2025 12:14:29 -0300 Subject: [PATCH 08/28] fix: ensure close event is emitted correctly in SocketClient and clarify event documentation --- packages/ws/src/client.ts | 3 +-- packages/ws/src/types.ts | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index 00de42b4d..ee0614bb4 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -126,6 +126,7 @@ export class SocketClient | any[] = Record { + queueMicrotask(() => this.emit("close", code, reason)); if (this._disposeOnClose) queueMicrotask(() => this.dispose()); switch (code) { @@ -146,8 +147,6 @@ export class SocketClient | any[] = Record { this.emit("error", status.error = error); diff --git a/packages/ws/src/types.ts b/packages/ws/src/types.ts index e0b7caf8c..5ecfa5a27 100644 --- a/packages/ws/src/types.ts +++ b/packages/ws/src/types.ts @@ -5,10 +5,12 @@ export interface SocketEventsMap | any[] = Record< close: [code: number, reason: Buffer] connected: [] connecting: [] + /** This event closes the socket */ connectionFailed: [] data: [data: Data] error: [error: Error] message: [data: RawData] + /** This event closes the socket */ unauthorized: [] } From b556326f21d4586f1ad159cf7767e2a28847a576 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Tue, 24 Jun 2025 15:16:30 -0300 Subject: [PATCH 09/28] fix: ensure chunkSize is validated as a number and improve connection state management in SocketClient --- packages/ws/src/client.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index ee0614bb4..9e45ee4a1 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -11,7 +11,7 @@ export class SocketClient | any[] = Record | any[] = Record = {}; + declare protected _lastError?: any; declare protected _socket?: WebSocket; declare protected _ping: number; declare protected _pong: number; @@ -119,11 +121,6 @@ export class SocketClient | any[] = Record { queueMicrotask(() => this.emit("close", code, reason)); @@ -131,16 +128,17 @@ export class SocketClient | any[] = Record | any[] = Record { - this.emit("error", status.error = error); + this.emit("error", this._lastError = error); }) .on("message", (data) => { try { this.emit("data", JSON.parse(data.toString())); } catch { this.emit("message", data); } }) .once("open", () => { - status.connected = true; - status.error = null; + this._connected = true; + delete this._lastError; this._ping = Date.now(); this._socket!.ping(); From c207ba191a41b5cdf7623d2dbb58f171d0a84fb3 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Tue, 24 Jun 2025 15:19:31 -0300 Subject: [PATCH 10/28] fix: update default chunk size in documentation to 256KB for clarity --- packages/ws/src/utils/buffer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ws/src/utils/buffer.ts b/packages/ws/src/utils/buffer.ts index 9dbc9d01c..3c31af0e9 100644 --- a/packages/ws/src/utils/buffer.ts +++ b/packages/ws/src/utils/buffer.ts @@ -11,7 +11,7 @@ import { DEFAULT_CHUNK_SIZE, MAX_CHUNK_SIZE, MIN_CHUNK_SIZE } from "../constants * * @param chunkSize * Limited between `8_192` (`8KB`) and `1_048_576` (`1MB`) - * Default `65_536` (`64KB`) + * Default `262_144` (`256KB`) */ export function* splitBuffer(buffer: Buffer, chunkSize: number = DEFAULT_CHUNK_SIZE) { chunkSize = Math.max(MIN_CHUNK_SIZE, Math.min(MAX_CHUNK_SIZE, chunkSize)); From 7ee8c15962629ea373a7b7c8576c76c9c04a38de Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Tue, 24 Jun 2025 15:24:13 -0300 Subject: [PATCH 11/28] fix: update sendJSON method to use ProgressData type and enhance type definitions in SocketClient --- packages/ws/src/client.ts | 10 ++++++---- packages/ws/src/types.ts | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index 9e45ee4a1..eb4adb8bb 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -2,7 +2,7 @@ import EventEmitter from "events"; import WebSocket from "ws"; import { DEFAULT_CHUNK_SIZE, MAX_FILE_SIZE, NETWORK_UNREACHABLE_CODE, SOCKET_UNAUTHORIZED_CODE } from "./constants"; import { BufferOverflowError } from "./errors"; -import { type OnProgressCallback, type SocketEventsMap, type SocketOptions } from "./types"; +import { type OnProgressCallback, type ProgressData, type SocketEventsMap, type SocketOptions } from "./types"; export class SocketClient | any[] = Record | any[]> extends EventEmitter> @@ -76,7 +76,7 @@ export class SocketClient | any[] = Record): Promise { + async sendJSON(value: Record | any[]): Promise { if (!this.connected) await this.connect(); await new Promise((resolve, reject) => { @@ -100,9 +100,11 @@ export class SocketClient | any[] = Record Date: Tue, 24 Jun 2025 15:40:30 -0300 Subject: [PATCH 12/28] fix: add offset to ProgressData and update chunk processing in sendBuffer method --- packages/ws/src/client.ts | 8 ++++---- packages/ws/src/types.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index eb4adb8bb..40d57ecc3 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -94,13 +94,13 @@ export class SocketClient | any[] = Record Date: Tue, 24 Jun 2025 21:01:11 -0300 Subject: [PATCH 13/28] feat: add custom error classes for network issues and unauthorized access --- packages/ws/src/client.ts | 16 +++++++++++----- packages/ws/src/errors/BufferOverflow.ts | 2 +- packages/ws/src/errors/NetworkUnreachable.ts | 7 +++++++ packages/ws/src/errors/Unauthorized.ts | 7 +++++++ packages/ws/src/errors/index.ts | 3 +++ 5 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 packages/ws/src/errors/NetworkUnreachable.ts create mode 100644 packages/ws/src/errors/Unauthorized.ts diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index 40d57ecc3..f2d26a456 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -1,7 +1,7 @@ import EventEmitter from "events"; import WebSocket from "ws"; import { DEFAULT_CHUNK_SIZE, MAX_FILE_SIZE, NETWORK_UNREACHABLE_CODE, SOCKET_UNAUTHORIZED_CODE } from "./constants"; -import { BufferOverflowError } from "./errors"; +import { BufferOverflowError, NetworkUnreachableError, UnauthorizedError } from "./errors"; import { type OnProgressCallback, type ProgressData, type SocketEventsMap, type SocketOptions } from "./types"; export class SocketClient | any[] = Record | any[]> @@ -125,16 +125,19 @@ export class SocketClient | any[] = Record { - queueMicrotask(() => this.emit("close", code, reason)); if (this._disposeOnClose) queueMicrotask(() => this.dispose()); switch (code) { case SOCKET_UNAUTHORIZED_CODE: this._connected = false; - return this.emit("unauthorized"); + this.emit("unauthorized"); + return reject(new UnauthorizedError()); } - if (!this._connected) return this.emit("connectionFailed"); + if (!this._connected) { + this.emit("connectionFailed"); + return reject(new NetworkUnreachableError()); + } this._connected = false; @@ -144,9 +147,12 @@ export class SocketClient | any[] = Record { this.emit("error", this._lastError = error); diff --git a/packages/ws/src/errors/BufferOverflow.ts b/packages/ws/src/errors/BufferOverflow.ts index d670134a8..76fb19b8d 100644 --- a/packages/ws/src/errors/BufferOverflow.ts +++ b/packages/ws/src/errors/BufferOverflow.ts @@ -4,6 +4,6 @@ export class BufferOverflowError extends Error { constructor( readonly max: number, ) { - super(); + super("Buffer overflow"); } } diff --git a/packages/ws/src/errors/NetworkUnreachable.ts b/packages/ws/src/errors/NetworkUnreachable.ts new file mode 100644 index 000000000..6ffe92db5 --- /dev/null +++ b/packages/ws/src/errors/NetworkUnreachable.ts @@ -0,0 +1,7 @@ +export class NetworkUnreachableError extends Error { + readonly name = "NetworkUnreachable"; + + constructor() { + super("Network unreachable"); + } +} diff --git a/packages/ws/src/errors/Unauthorized.ts b/packages/ws/src/errors/Unauthorized.ts new file mode 100644 index 000000000..7ebc8987b --- /dev/null +++ b/packages/ws/src/errors/Unauthorized.ts @@ -0,0 +1,7 @@ +export class UnauthorizedError extends Error { + readonly name = "Unauthorized"; + + constructor() { + super("Unauthorized"); + } +} diff --git a/packages/ws/src/errors/index.ts b/packages/ws/src/errors/index.ts index b757932e7..fcf92a6e7 100644 --- a/packages/ws/src/errors/index.ts +++ b/packages/ws/src/errors/index.ts @@ -1 +1,4 @@ export * from "./BufferOverflow"; +export * from "./NetworkUnreachable"; +export * from "./Unauthorized"; + From c96a62e82778e977e5644b0d242a6ea850f47baa Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Tue, 24 Jun 2025 21:05:56 -0300 Subject: [PATCH 14/28] fix: rename variable for clarity in sendBuffer method --- packages/ws/src/client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index f2d26a456..91fc65006 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -100,11 +100,11 @@ export class SocketClient | any[] = Record Date: Tue, 24 Jun 2025 21:20:35 -0300 Subject: [PATCH 15/28] feat: add SOCKET_ABNORMAL_CLOSURE constant and handle abnormal closure in SocketClient --- packages/ws/src/client.ts | 12 ++++++------ packages/ws/src/constants.ts | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index 91fc65006..47567eacd 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -1,6 +1,6 @@ import EventEmitter from "events"; import WebSocket from "ws"; -import { DEFAULT_CHUNK_SIZE, MAX_FILE_SIZE, NETWORK_UNREACHABLE_CODE, SOCKET_UNAUTHORIZED_CODE } from "./constants"; +import { DEFAULT_CHUNK_SIZE, MAX_FILE_SIZE, NETWORK_UNREACHABLE_CODE, SOCKET_ABNORMAL_CLOSURE, SOCKET_UNAUTHORIZED_CODE } from "./constants"; import { BufferOverflowError, NetworkUnreachableError, UnauthorizedError } from "./errors"; import { type OnProgressCallback, type ProgressData, type SocketEventsMap, type SocketOptions } from "./types"; @@ -128,17 +128,17 @@ export class SocketClient | any[] = Record this.dispose()); switch (code) { + case SOCKET_ABNORMAL_CLOSURE: + if (this._connected) break; + this.emit("connectionFailed"); + return reject(new NetworkUnreachableError()); + case SOCKET_UNAUTHORIZED_CODE: this._connected = false; this.emit("unauthorized"); return reject(new UnauthorizedError()); } - if (!this._connected) { - this.emit("connectionFailed"); - return reject(new NetworkUnreachableError()); - } - this._connected = false; if (this._lastError) { diff --git a/packages/ws/src/constants.ts b/packages/ws/src/constants.ts index ad7dd5e59..b332c105a 100644 --- a/packages/ws/src/constants.ts +++ b/packages/ws/src/constants.ts @@ -17,4 +17,6 @@ export const NETWORK_UNREACHABLE_ERRNO = -3008 as const; export const NETWORK_UNREACHABLE_CODE = "ENOTFOUND" as const; +export const SOCKET_ABNORMAL_CLOSURE = 1006 as const; + export const SOCKET_UNAUTHORIZED_CODE = 3000 as const; From b845176259fc32dc4a3bf147e45379dd6fea36cf Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Tue, 24 Jun 2025 21:29:42 -0300 Subject: [PATCH 16/28] fix: improve connection state handling on abnormal closure --- packages/ws/src/client.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index 47567eacd..8bd28fb57 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -127,20 +127,20 @@ export class SocketClient | any[] = Record { if (this._disposeOnClose) queueMicrotask(() => this.dispose()); + const isConnected = this._connected; + this._connected = false; + switch (code) { case SOCKET_ABNORMAL_CLOSURE: - if (this._connected) break; + if (isConnected) break; this.emit("connectionFailed"); return reject(new NetworkUnreachableError()); case SOCKET_UNAUTHORIZED_CODE: - this._connected = false; this.emit("unauthorized"); return reject(new UnauthorizedError()); } - this._connected = false; - if (this._lastError) { const error = this._lastError; delete this._lastError; From 4dc94b94fc5856ae545a6ccfb5d3dcf1e6c333da Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Tue, 24 Jun 2025 21:59:33 -0300 Subject: [PATCH 17/28] feat: add README file with documentation link for ws package --- packages/ws/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/ws/README.md diff --git a/packages/ws/README.md b/packages/ws/README.md new file mode 100644 index 000000000..9efb2bce4 --- /dev/null +++ b/packages/ws/README.md @@ -0,0 +1 @@ +# [View the documentation here.](https://discloud.github.io/discloud.app/modules/_discloudapp_ws.html) From 56e0f7adc5dbedf4551ab7cbb0d36c8e8143397e Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Tue, 24 Jun 2025 23:21:31 -0300 Subject: [PATCH 18/28] feat: export version constant in index file --- packages/ws/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ws/src/index.ts b/packages/ws/src/index.ts index d9c9349ab..e3eb0a3d5 100644 --- a/packages/ws/src/index.ts +++ b/packages/ws/src/index.ts @@ -4,3 +4,4 @@ export * from "./errors"; export * from "./types"; export * from "./utils"; +export const version: string = "[VI]{{inject}}[/VI]"; From 59848d92ba9d2e8d405ff2c5dfacaefbe57d13da Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Wed, 25 Jun 2025 10:07:12 -0300 Subject: [PATCH 19/28] fix: add missing dependency for @discloudapp/api-types in package.json --- packages/ws/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ws/package.json b/packages/ws/package.json index cde678b65..a33c2a7e8 100644 --- a/packages/ws/package.json +++ b/packages/ws/package.json @@ -24,6 +24,7 @@ ], "license": "Apache-2.0", "dependencies": { + "@discloudapp/api-types": "^1.0.1", "ws": "^8.18.2" }, "publishConfig": { From 877976efc696194dbd3f813c5334566e7536007c Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Wed, 25 Jun 2025 10:07:30 -0300 Subject: [PATCH 20/28] feat: introduce SocketEvents enum and refactor SocketClient to use it --- packages/ws/src/client.ts | 31 ++++++++-------- packages/ws/src/enum/events.ts | 10 ++++++ packages/ws/src/enum/index.ts | 1 + packages/ws/src/types.ts | 62 -------------------------------- packages/ws/src/types/events.ts | 14 ++++++++ packages/ws/src/types/index.ts | 4 +++ packages/ws/src/types/options.ts | 21 +++++++++++ packages/ws/src/types/payload.ts | 26 ++++++++++++++ 8 files changed, 92 insertions(+), 77 deletions(-) create mode 100644 packages/ws/src/enum/events.ts create mode 100644 packages/ws/src/enum/index.ts delete mode 100644 packages/ws/src/types.ts create mode 100644 packages/ws/src/types/events.ts create mode 100644 packages/ws/src/types/index.ts create mode 100644 packages/ws/src/types/options.ts create mode 100644 packages/ws/src/types/payload.ts diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index 8bd28fb57..ba4e2bc14 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -3,6 +3,7 @@ import WebSocket from "ws"; import { DEFAULT_CHUNK_SIZE, MAX_FILE_SIZE, NETWORK_UNREACHABLE_CODE, SOCKET_ABNORMAL_CLOSURE, SOCKET_UNAUTHORIZED_CODE } from "./constants"; import { BufferOverflowError, NetworkUnreachableError, UnauthorizedError } from "./errors"; import { type OnProgressCallback, type ProgressData, type SocketEventsMap, type SocketOptions } from "./types"; +import { SocketEvents } from "./enum"; export class SocketClient | any[] = Record | any[]> extends EventEmitter> @@ -35,10 +36,10 @@ export class SocketClient | any[] = Record | any[] = Record((resolve, reject) => { if (this.connecting) { const onConnected = () => { - this.off("close", onClose); + this.off(SocketEvents.close, onClose); resolve(); }; const onClose = () => { - this.off("connected", onConnected); + this.off(SocketEvents.connected, onConnected); reject(); }; - return this.once("connected", onConnected).once("close", onClose); + return this.once(SocketEvents.connected, onConnected).once(SocketEvents.close, onClose); } if (this.connected) return resolve(); reject(); @@ -133,11 +134,11 @@ export class SocketClient | any[] = Record | any[] = Record { - this.emit("error", this._lastError = error); + this.emit(SocketEvents.error, this._lastError = error); }) .on("message", (data) => { - try { this.emit("data", JSON.parse(data.toString())); } - catch { this.emit("message", data); } + try { this.emit(SocketEvents.data, JSON.parse(data.toString())); } + catch { this.emit(SocketEvents.message, data); } }) .once("open", () => { this._connected = true; @@ -168,7 +169,7 @@ export class SocketClient | any[] = Record | any[] = Record | any[]> { - close: [code: number, reason: Buffer] - connected: [] - connecting: [] - /** This event closes the socket */ - connectionFailed: [] - data: [data: Data] - error: [error: Error] - message: [data: RawData] - /** This event closes the socket */ - unauthorized: [] -} - -export interface SocketOptions { - /** - * Set the buffer chunk size per message - * - * Note that very large chunks may cause unexpected closure - * - * @default 262_144 (256KB) - */ - chunkSize?: number - /** - * Connecting timeout in milliseconds - * - * @default 10_000 - */ - connectingTimeout?: number | null - /** - * @default true - */ - disposeOnClose?: boolean - headers?: Record -} - -export interface SocketEventUploadData { - app?: ApiUploadApp - logs?: string - message: string | null - progress: SocketProgressData - status: "ok" | "error" - statusCode: number -} - -export interface SocketProgressData { - /** `0 - 100` */ - bar: number - log: string -} - -export interface ProgressData { - chunk: Buffer - current: number - offset: number - pending: boolean - total: number -} - -export type OnProgressCallback = (data: ProgressData) => unknown | Promise diff --git a/packages/ws/src/types/events.ts b/packages/ws/src/types/events.ts new file mode 100644 index 000000000..ad3caf7b6 --- /dev/null +++ b/packages/ws/src/types/events.ts @@ -0,0 +1,14 @@ +import type { RawData } from "ws"; + +export interface SocketEventsMap | any[] = Record | any[]> { + close: [code: number, reason: Buffer] + connected: [] + connecting: [] + /** This event closes the socket */ + connectionFailed: [] + data: [data: Data] + error: [error: Error] + message: [data: RawData] + /** This event closes the socket */ + unauthorized: [] +} diff --git a/packages/ws/src/types/index.ts b/packages/ws/src/types/index.ts new file mode 100644 index 000000000..7bef7f3f7 --- /dev/null +++ b/packages/ws/src/types/index.ts @@ -0,0 +1,4 @@ +export * from "./events"; +export * from "./options"; +export * from "./payload"; + diff --git a/packages/ws/src/types/options.ts b/packages/ws/src/types/options.ts new file mode 100644 index 000000000..dbae7d8ce --- /dev/null +++ b/packages/ws/src/types/options.ts @@ -0,0 +1,21 @@ +export interface SocketOptions { + /** + * Set the buffer chunk size per message + * + * Note that very large chunks may cause unexpected closure + * + * @default 262_144 (256KB) + */ + chunkSize?: number + /** + * Connecting timeout in milliseconds + * + * @default 10_000 + */ + connectingTimeout?: number | null + /** + * @default true + */ + disposeOnClose?: boolean + headers?: Record +} diff --git a/packages/ws/src/types/payload.ts b/packages/ws/src/types/payload.ts new file mode 100644 index 000000000..d086cfae8 --- /dev/null +++ b/packages/ws/src/types/payload.ts @@ -0,0 +1,26 @@ +import type { ApiUploadApp } from "@discloudapp/api-types/v2"; + +export interface SocketEventUploadData { + app?: ApiUploadApp + logs?: string + message: string | null + progress: SocketProgressData + status: "ok" | "error" + statusCode: number +} + +export interface SocketProgressData { + /** `0 - 100` */ + bar: number + log: string +} + +export interface ProgressData { + chunk: Buffer + current: number + offset: number + pending: boolean + total: number +} + +export type OnProgressCallback = (data: ProgressData) => unknown | Promise From a7c1d52cd9e055fae5179c2e3335ef1b10cb78c0 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Wed, 25 Jun 2025 10:07:35 -0300 Subject: [PATCH 21/28] chore: update ws package to version 8.18.2 in yarn.lock --- yarn.lock | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/yarn.lock b/yarn.lock index e4bce0ddb..69935404d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2694,6 +2694,11 @@ wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" +ws@^8.18.2: + version "8.18.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a" + integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ== + yaml@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.1.tgz#44a247d1b88523855679ac7fa7cda6ed7e135cf6" From 16e0c7ffab7f2c9351d3e39813dcf2090c4c3348 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Fri, 27 Jun 2025 21:13:54 -0300 Subject: [PATCH 22/28] Bump deps --- packages/ws/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ws/package.json b/packages/ws/package.json index a33c2a7e8..0a4810912 100644 --- a/packages/ws/package.json +++ b/packages/ws/package.json @@ -24,7 +24,7 @@ ], "license": "Apache-2.0", "dependencies": { - "@discloudapp/api-types": "^1.0.1", + "@discloudapp/api-types": "^1.0.2", "ws": "^8.18.2" }, "publishConfig": { From e7738f490b1163bee3ac779c228d368a6fd2e2b1 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Fri, 27 Jun 2025 21:32:59 -0300 Subject: [PATCH 23/28] fix: correct function name in documentation and implementation for buffer chunking --- packages/ws/src/utils/buffer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ws/src/utils/buffer.ts b/packages/ws/src/utils/buffer.ts index 3c31af0e9..b75b21b3d 100644 --- a/packages/ws/src/utils/buffer.ts +++ b/packages/ws/src/utils/buffer.ts @@ -4,7 +4,7 @@ import { DEFAULT_CHUNK_SIZE, MAX_CHUNK_SIZE, MIN_CHUNK_SIZE } from "../constants * This is a Buffer chunk generator * * ```js - * for (const chunk of splitBuffer(buffer, chunkSize)) { + * for (const chunk of chunkifyBuffer(buffer, chunkSize)) { * // ... * } * ``` @@ -13,7 +13,7 @@ import { DEFAULT_CHUNK_SIZE, MAX_CHUNK_SIZE, MIN_CHUNK_SIZE } from "../constants * Limited between `8_192` (`8KB`) and `1_048_576` (`1MB`) * Default `262_144` (`256KB`) */ -export function* splitBuffer(buffer: Buffer, chunkSize: number = DEFAULT_CHUNK_SIZE) { +export function* chunkifyBuffer(buffer: Buffer, chunkSize: number = DEFAULT_CHUNK_SIZE) { chunkSize = Math.max(MIN_CHUNK_SIZE, Math.min(MAX_CHUNK_SIZE, chunkSize)); for (let i = 0; i < buffer.length;) { From 3e34d83dce4cd759f99bcaf193d4b13ad3d641d8 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Fri, 27 Jun 2025 22:26:02 -0300 Subject: [PATCH 24/28] fix: resolve headers in WebSocket connection options --- packages/ws/src/client.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index ba4e2bc14..80d2d6a02 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -1,9 +1,9 @@ import EventEmitter from "events"; import WebSocket from "ws"; import { DEFAULT_CHUNK_SIZE, MAX_FILE_SIZE, NETWORK_UNREACHABLE_CODE, SOCKET_ABNORMAL_CLOSURE, SOCKET_UNAUTHORIZED_CODE } from "./constants"; +import { SocketEvents } from "./enum"; import { BufferOverflowError, NetworkUnreachableError, UnauthorizedError } from "./errors"; import { type OnProgressCallback, type ProgressData, type SocketEventsMap, type SocketOptions } from "./types"; -import { SocketEvents } from "./enum"; export class SocketClient | any[] = Record | any[]> extends EventEmitter> @@ -118,7 +118,7 @@ export class SocketClient | any[] = Record[2] = { - headers: this._headers, + headers: this.#resolveHeaders(this._headers), ...typeof this._connectingTimeout === "number" ? { signal: AbortSignal.timeout(this._connectingTimeout) } : {}, @@ -184,6 +184,12 @@ export class SocketClient | any[] = Record) { + headers["api-token"] ??= process.env.DISCLOUD_TOKEN!; + + return headers; + } + [Symbol.dispose]() { this.close(); this.removeAllListeners(); From 1efff8ff03dd7841dc68d30408502c5cedb97e77 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Sat, 28 Jun 2025 00:57:12 -0300 Subject: [PATCH 25/28] refactor: remove disposeOnClose option from SocketOptions and update disconnect method --- packages/ws/src/client.ts | 10 +++------- packages/ws/src/types/options.ts | 4 ---- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index 80d2d6a02..7938f077e 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -18,9 +18,6 @@ export class SocketClient | any[] = Record | any[] = Record = {}; declare protected _lastError?: any; declare protected _socket?: WebSocket; @@ -41,7 +37,7 @@ export class SocketClient | any[] = Record | any[] = Record { - if (this._disposeOnClose) queueMicrotask(() => this.dispose()); + queueMicrotask(() => this.dispose()); const isConnected = this._connected; this._connected = false; @@ -191,7 +187,7 @@ export class SocketClient | any[] = Record } From 19075047122439c66900482ce305b548ec6eab18 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Mon, 30 Jun 2025 19:26:46 -0300 Subject: [PATCH 26/28] feat: implement uploadAction and integrate with SocketClient for file uploads --- packages/ws/src/actions/upload.ts | 33 +++++++++++++++++++++++++++++++ packages/ws/src/client.ts | 15 ++++++++++---- packages/ws/src/types/actions.ts | 9 +++++++++ packages/ws/src/types/events.ts | 4 ---- packages/ws/src/types/index.ts | 1 + 5 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 packages/ws/src/actions/upload.ts create mode 100644 packages/ws/src/types/actions.ts diff --git a/packages/ws/src/actions/upload.ts b/packages/ws/src/actions/upload.ts new file mode 100644 index 000000000..df0838ea6 --- /dev/null +++ b/packages/ws/src/actions/upload.ts @@ -0,0 +1,33 @@ +import { type ApiUploadApp } from "@discloudapp/api-types/v2"; +import { type SocketClient } from "../client"; +import { SocketEvents } from "../enum"; +import type { SocketEventUploadData, SocketUploadActionOptions } from "../types"; + +export function uploadAction(socket: SocketClient, buffer: Buffer, options: SocketUploadActionOptions): Promise +export function uploadAction(socket: SocketClient, buffer: Buffer, options: SocketUploadActionOptions) { + return new Promise((resolve, reject) => { + if (typeof options.onConnecting === "function") socket.on(SocketEvents.connecting, options.onConnecting); + if (typeof options.onError === "function") socket.on(SocketEvents.error, options.onError); + + let app: ApiUploadApp; + + socket + .on(SocketEvents.close, (_code, _reason) => resolve(app)) + .on(SocketEvents.connected, function () { + if (typeof options.onConnected === "function") options.onConnected(); + + socket.sendBuffer(buffer, options.onUploading).catch(reject); + }) + .on(SocketEvents.data, (data: SocketEventUploadData) => { + switch (data.statusCode) { + case 102: + if (typeof options.onData === "function") options.onData(data); + break; + } + + if (data.app) app = data.app; + }) + .connect() + .catch(reject); + }); +} diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index 7938f077e..050cbe1b9 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -1,13 +1,23 @@ +import { RouteBases, Routes } from "@discloudapp/api-types/v2"; import EventEmitter from "events"; import WebSocket from "ws"; +import { uploadAction } from "./actions/upload"; import { DEFAULT_CHUNK_SIZE, MAX_FILE_SIZE, NETWORK_UNREACHABLE_CODE, SOCKET_ABNORMAL_CLOSURE, SOCKET_UNAUTHORIZED_CODE } from "./constants"; import { SocketEvents } from "./enum"; import { BufferOverflowError, NetworkUnreachableError, UnauthorizedError } from "./errors"; -import { type OnProgressCallback, type ProgressData, type SocketEventsMap, type SocketOptions } from "./types"; +import { type OnProgressCallback, type ProgressData, type SocketEventsMap, type SocketEventUploadData, type SocketOptions, type SocketUploadActionOptions } from "./types"; export class SocketClient | any[] = Record | any[]> extends EventEmitter> implements Disposable { + static async upload(buffer: Buffer, options: SocketUploadActionOptions) { + const url = new URL(`${RouteBases.api}/ws${Routes.upload()}`); + + const socket = new SocketClient(url); + + return uploadAction(socket, buffer, options); + } + constructor(protected wsURL: URL, options?: SocketOptions) { super({ captureRejections: true }); @@ -130,11 +140,9 @@ export class SocketClient | any[] = Record | any[] = Record unknown + onConnecting?: () => unknown + onData?: (data: SocketEventUploadData) => unknown + onError?: (error: Error) => unknown + onUploading?: OnProgressCallback +} diff --git a/packages/ws/src/types/events.ts b/packages/ws/src/types/events.ts index ad3caf7b6..0f770f9e3 100644 --- a/packages/ws/src/types/events.ts +++ b/packages/ws/src/types/events.ts @@ -4,11 +4,7 @@ export interface SocketEventsMap | any[] = Record< close: [code: number, reason: Buffer] connected: [] connecting: [] - /** This event closes the socket */ - connectionFailed: [] data: [data: Data] error: [error: Error] message: [data: RawData] - /** This event closes the socket */ - unauthorized: [] } diff --git a/packages/ws/src/types/index.ts b/packages/ws/src/types/index.ts index 7bef7f3f7..eb3aacf06 100644 --- a/packages/ws/src/types/index.ts +++ b/packages/ws/src/types/index.ts @@ -1,3 +1,4 @@ +export * from "./actions"; export * from "./events"; export * from "./options"; export * from "./payload"; From 23edb365f2302113f6623d3bc580d7f771f8af35 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Sun, 28 Sep 2025 18:53:30 -0300 Subject: [PATCH 27/28] finalize upload action --- packages/ws/src/actions/upload.ts | 37 ++++++--- packages/ws/src/client.ts | 85 +++++++++++--------- packages/ws/src/errors/BufferOverflow.ts | 1 + packages/ws/src/errors/Closed.ts | 10 +++ packages/ws/src/errors/NetworkUnreachable.ts | 4 +- packages/ws/src/errors/Unauthorized.ts | 7 +- packages/ws/src/errors/index.ts | 4 - packages/ws/src/index.ts | 5 +- packages/ws/src/types/actions.ts | 13 ++- packages/ws/src/types/options.ts | 2 +- packages/ws/src/types/payload.ts | 6 +- 11 files changed, 113 insertions(+), 61 deletions(-) create mode 100644 packages/ws/src/errors/Closed.ts delete mode 100644 packages/ws/src/errors/index.ts diff --git a/packages/ws/src/actions/upload.ts b/packages/ws/src/actions/upload.ts index df0838ea6..8c4d86e77 100644 --- a/packages/ws/src/actions/upload.ts +++ b/packages/ws/src/actions/upload.ts @@ -1,24 +1,39 @@ -import { type ApiUploadApp } from "@discloudapp/api-types/v2"; +import { type ApiApp } from "@discloudapp/api-types/v2"; import { type SocketClient } from "../client"; import { SocketEvents } from "../enum"; import type { SocketEventUploadData, SocketUploadActionOptions } from "../types"; -export function uploadAction(socket: SocketClient, buffer: Buffer, options: SocketUploadActionOptions): Promise -export function uploadAction(socket: SocketClient, buffer: Buffer, options: SocketUploadActionOptions) { +export function uploadAction(socket: SocketClient, buffer: Buffer, options: SocketUploadActionOptions): Promise +export function uploadAction(socket: SocketClient, buffer: Buffer, options: SocketUploadActionOptions): Promise +export function uploadAction(socket: SocketClient, buffer: Buffer, options: SocketUploadActionOptions) { return new Promise((resolve, reject) => { - if (typeof options.onConnecting === "function") socket.on(SocketEvents.connecting, options.onConnecting); - if (typeof options.onError === "function") socket.on(SocketEvents.error, options.onError); + let app: ApiApp; - let app: ApiUploadApp; + function onError(error: any) { + if (typeof options.onError === "function") return options.onError(error); + socket.dispose(); + reject(error); + } socket - .on(SocketEvents.close, (_code, _reason) => resolve(app)) - .on(SocketEvents.connected, function () { + .on(SocketEvents.close, (code, reason) => { + if (typeof options.onClose === "function") options.onClose(code, reason); + resolve(app); + }) + .on(SocketEvents.error, onError) + .on(SocketEvents.connecting, () => { + if (typeof options.onConnecting === "function") options.onConnecting(); + }) + .on(SocketEvents.connected, async () => { if (typeof options.onConnected === "function") options.onConnected(); - socket.sendBuffer(buffer, options.onUploading).catch(reject); + try { + await socket.sendBuffer(buffer, options.onProgress); + } catch (error: any) { + onError(error); + } }) - .on(SocketEvents.data, (data: SocketEventUploadData) => { + .on(SocketEvents.data, (data) => { switch (data.statusCode) { case 102: if (typeof options.onData === "function") options.onData(data); @@ -28,6 +43,6 @@ export function uploadAction(socket: SocketClient, buffer: Buffer, options: if (data.app) app = data.app; }) .connect() - .catch(reject); + .catch(onError); }); } diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index 050cbe1b9..167f22498 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -4,8 +4,11 @@ import WebSocket from "ws"; import { uploadAction } from "./actions/upload"; import { DEFAULT_CHUNK_SIZE, MAX_FILE_SIZE, NETWORK_UNREACHABLE_CODE, SOCKET_ABNORMAL_CLOSURE, SOCKET_UNAUTHORIZED_CODE } from "./constants"; import { SocketEvents } from "./enum"; -import { BufferOverflowError, NetworkUnreachableError, UnauthorizedError } from "./errors"; -import { type OnProgressCallback, type ProgressData, type SocketEventsMap, type SocketEventUploadData, type SocketOptions, type SocketUploadActionOptions } from "./types"; +import { BufferOverflowError } from "./errors/BufferOverflow"; +import ClosedError from "./errors/Closed"; +import { NetworkUnreachableError } from "./errors/NetworkUnreachable"; +import { UnauthorizedError } from "./errors/Unauthorized"; +import { type BufferLike, type OnProgressCallback, type ProgressData, type SocketEventsMap, type SocketEventUploadData, type SocketOptions, type SocketUploadActionOptions } from "./types"; export class SocketClient | any[] = Record | any[]> extends EventEmitter> @@ -33,14 +36,16 @@ export class SocketClient | any[] = Record = {}; - declare protected _lastError?: any; + declare protected _error?: any; declare protected _socket?: WebSocket; declare protected _ping: number; declare protected _pong: number; - declare ping: number; + + get ping() { return this._pong; } get closed() { return !this._socket || this._socket.readyState === WebSocket.CLOSED; } get closing() { return this._socket ? this._socket.readyState === WebSocket.CLOSING : false; } @@ -65,39 +70,27 @@ export class SocketClient | any[] = Record((resolve, reject) => { - if (this.connecting) { - const onConnected = () => { - this.off(SocketEvents.close, onClose); - resolve(); - }; - const onClose = () => { - this.off(SocketEvents.connected, onConnected); - reject(); - }; - return this.once(SocketEvents.connected, onConnected).once(SocketEvents.close, onClose); - } - if (this.connected) return resolve(); - reject(); - }); - } - - async sendJSON(value: Record | any[]): Promise { + async sendAsync(data: BufferLike) { if (!this.connected) await this.connect(); await new Promise((resolve, reject) => { - this._socket!.send(JSON.stringify(value), (err) => { + this._socket!.send(data, (err) => { if (err) return reject(err); resolve(); }); }); } + async sendJSON(value: Record | any[]): Promise { + await this.sendAsync(JSON.stringify(value)); + } + async sendBuffer(buffer: Buffer, onProgress?: OnProgressCallback) { - if (buffer.length > MAX_FILE_SIZE) throw new BufferOverflowError(MAX_FILE_SIZE); + if (buffer.length > MAX_FILE_SIZE) throw new BufferOverflowError(buffer.length, MAX_FILE_SIZE); + /** Number of parts to be sent */ const total = Math.ceil(buffer.length / this._chunkSize); + /** Size of each part to be sent */ const chunkSize = Math.ceil(buffer.length / total); for (let i = 0; i < total;) { @@ -121,7 +114,7 @@ export class SocketClient | any[] = Record[2] = { headers: this.#resolveHeaders(this._headers), @@ -140,26 +133,26 @@ export class SocketClient | any[] = Record { - this.emit(SocketEvents.error, this._lastError = error); + this.emit(SocketEvents.error, this._error = error); }) .on("message", (data) => { try { this.emit(SocketEvents.data, JSON.parse(data.toString())); } @@ -167,7 +160,7 @@ export class SocketClient | any[] = Record { this._connected = true; - delete this._lastError; + delete this._error; this._ping = Date.now(); this._socket!.ping(); @@ -181,14 +174,32 @@ export class SocketClient | any[] = Record { - this._pong = Date.now(); - this.ping = this._pong - this._ping; + this._pong = Date.now() - this._ping; }); }); } + async #waitConnect() { + await new Promise((resolve, reject) => { + if (this.connecting) { + const onConnected = () => { + this.off(SocketEvents.close, onClose); + resolve(); + }; + const onClose = (code: number, reason: Buffer) => { + this.off(SocketEvents.connected, onConnected); + reject(new ClosedError(code, reason)); + }; + return this.once(SocketEvents.connected, onConnected).once(SocketEvents.close, onClose); + } + if (this.connected) return resolve(); + reject(this._error); + }); + } + #resolveHeaders(headers: Record) { - headers["api-token"] ??= process.env.DISCLOUD_TOKEN!; + if (!headers["api-token"] && process.env.DISCLOUD_TOKEN) + headers["api-token"] = process.env.DISCLOUD_TOKEN; return headers; } diff --git a/packages/ws/src/errors/BufferOverflow.ts b/packages/ws/src/errors/BufferOverflow.ts index 76fb19b8d..2caad1f56 100644 --- a/packages/ws/src/errors/BufferOverflow.ts +++ b/packages/ws/src/errors/BufferOverflow.ts @@ -2,6 +2,7 @@ export class BufferOverflowError extends Error { readonly name = "BufferOverflow"; constructor( + readonly size: number, readonly max: number, ) { super("Buffer overflow"); diff --git a/packages/ws/src/errors/Closed.ts b/packages/ws/src/errors/Closed.ts new file mode 100644 index 000000000..c5f95f7fc --- /dev/null +++ b/packages/ws/src/errors/Closed.ts @@ -0,0 +1,10 @@ +export default class ClosedError extends Error { + readonly name = "Closed"; + + constructor( + readonly code: number, + readonly reason: Buffer, + ) { + super(); + } +} diff --git a/packages/ws/src/errors/NetworkUnreachable.ts b/packages/ws/src/errors/NetworkUnreachable.ts index 6ffe92db5..7b7071306 100644 --- a/packages/ws/src/errors/NetworkUnreachable.ts +++ b/packages/ws/src/errors/NetworkUnreachable.ts @@ -1,7 +1,9 @@ export class NetworkUnreachableError extends Error { readonly name = "NetworkUnreachable"; - constructor() { + constructor( + readonly reason: Buffer, + ) { super("Network unreachable"); } } diff --git a/packages/ws/src/errors/Unauthorized.ts b/packages/ws/src/errors/Unauthorized.ts index 7ebc8987b..336158dfb 100644 --- a/packages/ws/src/errors/Unauthorized.ts +++ b/packages/ws/src/errors/Unauthorized.ts @@ -1,7 +1,12 @@ +import { SOCKET_UNAUTHORIZED_CODE } from "../constants"; + export class UnauthorizedError extends Error { + readonly code = SOCKET_UNAUTHORIZED_CODE; readonly name = "Unauthorized"; - constructor() { + constructor( + readonly reason: Buffer, + ) { super("Unauthorized"); } } diff --git a/packages/ws/src/errors/index.ts b/packages/ws/src/errors/index.ts deleted file mode 100644 index fcf92a6e7..000000000 --- a/packages/ws/src/errors/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./BufferOverflow"; -export * from "./NetworkUnreachable"; -export * from "./Unauthorized"; - diff --git a/packages/ws/src/index.ts b/packages/ws/src/index.ts index e3eb0a3d5..8829a8ae0 100644 --- a/packages/ws/src/index.ts +++ b/packages/ws/src/index.ts @@ -1,6 +1,9 @@ export * from "./client"; export * from "./constants"; -export * from "./errors"; +export * from "./errors/BufferOverflow"; +export * from "./errors/Closed"; +export * from "./errors/NetworkUnreachable"; +export * from "./errors/Unauthorized"; export * from "./types"; export * from "./utils"; diff --git a/packages/ws/src/types/actions.ts b/packages/ws/src/types/actions.ts index 6431c6824..6c4b3caca 100644 --- a/packages/ws/src/types/actions.ts +++ b/packages/ws/src/types/actions.ts @@ -1,9 +1,16 @@ import type { OnProgressCallback, SocketEventUploadData } from "."; -export interface SocketUploadActionOptions { +/** *Note* Setting the `options.onError` property prevents promise rejection */ +export interface SocketActionOptions { + onClose?: (code: number, reason: Buffer) => unknown onConnected?: () => unknown onConnecting?: () => unknown - onData?: (data: SocketEventUploadData) => unknown + onData?: (data: Data) => unknown + /** *Note* Setting this property prevents promise rejection */ onError?: (error: Error) => unknown - onUploading?: OnProgressCallback +} + +/** *Note* Setting the `options.onError` property prevents promise rejection */ +export interface SocketUploadActionOptions extends SocketActionOptions { + onProgress?: OnProgressCallback } diff --git a/packages/ws/src/types/options.ts b/packages/ws/src/types/options.ts index a3a7fbb80..42ea01a9a 100644 --- a/packages/ws/src/types/options.ts +++ b/packages/ws/src/types/options.ts @@ -10,7 +10,7 @@ export interface SocketOptions { /** * Connecting timeout in milliseconds * - * @default 10_000 + * @default 10_000 (10 seconds) */ connectingTimeout?: number | null headers?: Record diff --git a/packages/ws/src/types/payload.ts b/packages/ws/src/types/payload.ts index d086cfae8..eb3a0816b 100644 --- a/packages/ws/src/types/payload.ts +++ b/packages/ws/src/types/payload.ts @@ -1,7 +1,9 @@ -import type { ApiUploadApp } from "@discloudapp/api-types/v2"; +import type { ApiApp } from "@discloudapp/api-types/v2"; + +export type BufferLike = Parameters[0] export interface SocketEventUploadData { - app?: ApiUploadApp + app?: ApiApp logs?: string message: string | null progress: SocketProgressData From e397eb319bdf4d0ad44c81c53ebab6048d263bef Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Sun, 28 Sep 2025 19:45:56 -0300 Subject: [PATCH 28/28] feat: add user and team commit by socket --- packages/ws/src/actions/commit.ts | 50 +++++++++++++++++++++++++++++++ packages/ws/src/actions/upload.ts | 9 +++--- packages/ws/src/client.ts | 19 +++++++++++- packages/ws/src/types/actions.ts | 11 +++++-- packages/ws/src/types/payload.ts | 8 +++++ 5 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 packages/ws/src/actions/commit.ts diff --git a/packages/ws/src/actions/commit.ts b/packages/ws/src/actions/commit.ts new file mode 100644 index 000000000..6b8c9d6b8 --- /dev/null +++ b/packages/ws/src/actions/commit.ts @@ -0,0 +1,50 @@ +import { type SocketClient } from "../client"; +import { SocketEvents } from "../enum"; +import type { SocketCommitActionOptions, SocketEventCommitData } from "../types"; + +/** *Note* Setting the `options.onError` property prevents promise rejection */ +export function commitAction( + socket: SocketClient, + buffer: Buffer, + options: SocketCommitActionOptions = {}, +) { + return new Promise((resolve, reject) => { + let success = false; + + function onError(error: any) { + if (typeof options.onError === "function") return options.onError(error); + socket.dispose(); + reject(error); + } + + socket + .on(SocketEvents.close, (code, reason) => { + if (typeof options.onClose === "function") options.onClose(code, reason); + resolve(success); + }) + .on(SocketEvents.error, onError) + .on(SocketEvents.connecting, () => { + if (typeof options.onConnecting === "function") options.onConnecting(); + }) + .on(SocketEvents.connected, async () => { + if (typeof options.onConnected === "function") options.onConnected(); + + try { + await socket.sendBuffer(buffer, options.onProgress); + } catch (error: any) { + onError(error); + } + }) + .on(SocketEvents.data, (data) => { + if (typeof options.onData === "function") options.onData(data); + + switch (data.statusCode) { + case 200: + success = true; + break; + } + }) + .connect() + .catch(onError); + }); +} diff --git a/packages/ws/src/actions/upload.ts b/packages/ws/src/actions/upload.ts index 8c4d86e77..50250103c 100644 --- a/packages/ws/src/actions/upload.ts +++ b/packages/ws/src/actions/upload.ts @@ -3,10 +3,11 @@ import { type SocketClient } from "../client"; import { SocketEvents } from "../enum"; import type { SocketEventUploadData, SocketUploadActionOptions } from "../types"; -export function uploadAction(socket: SocketClient, buffer: Buffer, options: SocketUploadActionOptions): Promise -export function uploadAction(socket: SocketClient, buffer: Buffer, options: SocketUploadActionOptions): Promise -export function uploadAction(socket: SocketClient, buffer: Buffer, options: SocketUploadActionOptions) { - return new Promise((resolve, reject) => { +/** *Note* Setting the `options.onError` property prevents promise rejection */ +export function uploadAction(socket: SocketClient, buffer: Buffer, options?: SocketUploadActionOptions): Promise +export function uploadAction(socket: SocketClient, buffer: Buffer, options?: SocketUploadActionOptions): Promise +export function uploadAction(socket: SocketClient, buffer: Buffer, options: SocketUploadActionOptions = {}) { + return new Promise((resolve, reject) => { let app: ApiApp; function onError(error: any) { diff --git a/packages/ws/src/client.ts b/packages/ws/src/client.ts index 167f22498..39b3e6d5d 100644 --- a/packages/ws/src/client.ts +++ b/packages/ws/src/client.ts @@ -1,6 +1,7 @@ import { RouteBases, Routes } from "@discloudapp/api-types/v2"; import EventEmitter from "events"; import WebSocket from "ws"; +import { commitAction } from "./actions/commit"; import { uploadAction } from "./actions/upload"; import { DEFAULT_CHUNK_SIZE, MAX_FILE_SIZE, NETWORK_UNREACHABLE_CODE, SOCKET_ABNORMAL_CLOSURE, SOCKET_UNAUTHORIZED_CODE } from "./constants"; import { SocketEvents } from "./enum"; @@ -8,11 +9,27 @@ import { BufferOverflowError } from "./errors/BufferOverflow"; import ClosedError from "./errors/Closed"; import { NetworkUnreachableError } from "./errors/NetworkUnreachable"; import { UnauthorizedError } from "./errors/Unauthorized"; -import { type BufferLike, type OnProgressCallback, type ProgressData, type SocketEventsMap, type SocketEventUploadData, type SocketOptions, type SocketUploadActionOptions } from "./types"; +import { type BufferLike, type OnProgressCallback, type ProgressData, type SocketCommitActionOptions, type SocketEventCommitData, type SocketEventsMap, type SocketEventUploadData, type SocketOptions, type SocketUploadActionOptions } from "./types"; export class SocketClient | any[] = Record | any[]> extends EventEmitter> implements Disposable { + static async teamCommit(appId: string, buffer: Buffer, options: SocketCommitActionOptions) { + const url = new URL(`${RouteBases.api}/ws${Routes.teamCommit(appId)}`); + + const socket = new SocketClient(url); + + return commitAction(socket, buffer, options); + } + + static async userCommit(appId: string, buffer: Buffer, options: SocketCommitActionOptions) { + const url = new URL(`${RouteBases.api}/ws${Routes.appCommit(appId)}`); + + const socket = new SocketClient(url); + + return commitAction(socket, buffer, options); + } + static async upload(buffer: Buffer, options: SocketUploadActionOptions) { const url = new URL(`${RouteBases.api}/ws${Routes.upload()}`); diff --git a/packages/ws/src/types/actions.ts b/packages/ws/src/types/actions.ts index 6c4b3caca..26fa435bc 100644 --- a/packages/ws/src/types/actions.ts +++ b/packages/ws/src/types/actions.ts @@ -1,6 +1,6 @@ -import type { OnProgressCallback, SocketEventUploadData } from "."; +import type { OnProgressCallback, SocketEventCommitData, SocketEventUploadData } from "."; -/** *Note* Setting the `options.onError` property prevents promise rejection */ +/** *Note* Setting the `onError` property prevents promise rejection */ export interface SocketActionOptions { onClose?: (code: number, reason: Buffer) => unknown onConnected?: () => unknown @@ -10,7 +10,12 @@ export interface SocketActionOptions { onError?: (error: Error) => unknown } -/** *Note* Setting the `options.onError` property prevents promise rejection */ +/** *Note* Setting the `onError` property prevents promise rejection */ +export interface SocketCommitActionOptions extends SocketActionOptions { + onProgress?: OnProgressCallback +} + +/** *Note* Setting the `onError` property prevents promise rejection */ export interface SocketUploadActionOptions extends SocketActionOptions { onProgress?: OnProgressCallback } diff --git a/packages/ws/src/types/payload.ts b/packages/ws/src/types/payload.ts index eb3a0816b..3e598b31f 100644 --- a/packages/ws/src/types/payload.ts +++ b/packages/ws/src/types/payload.ts @@ -2,6 +2,14 @@ import type { ApiApp } from "@discloudapp/api-types/v2"; export type BufferLike = Parameters[0] +export interface SocketEventCommitData { + logs?: string + message: string | null + progress: SocketProgressData + status: "ok" | "error" + statusCode: number +} + export interface SocketEventUploadData { app?: ApiApp logs?: string