Files
SnowLuma/packages/sdk/STREAMING.md
T
TanTan fa9c73363d
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
SnowLuma v2.4.2 source snapshot
2026-11-13 09:02:00 +08:00

6.3 KiB

Streaming files

Both SnowLumaHttpClient and SnowLumaWebSocketClient support incremental uploads and downloads. Existing request(), call(), raw(), rawResponse() and downloadFile() keep their single-response behavior. Use the new methods below when a response contains multiple frames.

Upload

import { createReadStream } from 'node:fs';
import { stat } from 'node:fs/promises';
import { SnowLumaHttpClient } from '@snowluma/sdk';

const client = new SnowLumaHttpClient({ baseUrl: 'http://127.0.0.1:3000/' });
const controller = new AbortController();
const path = './photo.png';
const file = await client.uploadFileStream(
    createReadStream(path, { signal: controller.signal }),
    {
        fileSize: (await stat(path)).size,
        filename: 'photo.png',
        signal: controller.signal,
        onProgress: ({ uploadedBytes, fileSize }) => {
            console.log(`${uploadedBytes}/${fileSize}`);
        },
    },
);
console.log(file.file_path, file.file_size, file.sha256);

uploadFileStream(source, options?) accepts Uint8Array (including Node.js Buffer), Blob, ReadableStream<Uint8Array> or AsyncIterable<Uint8Array>. Byte arrays and blobs provide their own size. Stream sources require fileSize: the server needs the total chunk count before accepting the first chunk. The SDK rejects input whose actual length differs from the declared size.

The default chunk size is 64 KiB. chunkSize accepts 1 through 1048576 bytes, keeping each encoded request below the standard server HTTP body limit. Input is read incrementally; chunks are acknowledged sequentially, and onProgress is awaited before continuing. Empty files are supported. Completion is a separate request and returns UploadFileStreamComplete, including the server's local file path; it does not write a local client file or send a QQ message.

Optional settings are filename, expectedSha256, fileRetention (milliseconds; zero disables server expiry), and streamId. The default server retention is five minutes. A generated ID identifies each upload; an explicit ID must be fresh, not an existing upload. The helper does not resume or automatically retry transfers. The server validates expectedSha256 when provided; the SDK checks the completed metadata and returned digest against that expectation.

signal cancels requests and closes readable sources. For custom async iterables, pass the same signal to the source and ensure pending reads and iterator cleanup can finish on cancellation (as the createReadStream example does). Cancellation cannot force an arbitrary producer to resolve its own promises. A failed or cancelled upload is not finalized; partial server uploads expire after the server's idle timeout (currently ten minutes). The helper does not issue a reset request or delete a completed server file automatically.

Download

import { createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';

await pipeline(
    client.downloadFileStream(
        { file: file.file_path, chunk_size: 64 * 1024 },
        { onInfo: (info) => console.log(info.file_name, info.file_size) },
    ),
    createWriteStream('./downloaded.png'),
);

downloadFileStream(params, options?) yields Uint8Array chunks and returns DownloadFileStreamComplete as the iterator's final value. for await and pipeline() consume chunks and verify completion, but discard that return value; call iterator.next() manually if you need total_bytes and total_chunks from the final result. onInfo runs before any file bytes are yielded.

Parameters accept a server-side file or file_id, plus optional chunk_size. The action option selects download_file_stream (default), download_file_image_stream, or download_file_record_stream.

The helper decodes base64 and checks chunk order, chunk lengths and final counts. A zero initial file size can mean unknown size; final byte and chunk totals still must match. Missing metadata, malformed frames, truncation and server failures throw; bytes already consumed are not rolled back. For durable output, write to a temporary file and rename it after the iteration succeeds.

Individual frames

for await (const frame of client.stream('test_download_stream', {})) {
    console.log(frame.type, frame.data_type);
}

for await (const response of client.requestStream('test_download_stream', {})) {
    console.log(response.status, response.retcode, response.data);
}
  • requestStream() yields response envelopes, including a final failed envelope.
  • stream() yields typed payloads and throws SnowLumaApiError (or its auth subtype) for failed envelopes, including errors after earlier chunks.
  • Both support the five streaming actions: the three downloads, test_download_stream, and upload_file_stream. Each upload request returns just one acknowledgement, including type: 'stream' chunk ACKs. Low-level callers must finalize in a separate request. Reset retains the server's existing failed-envelope behavior.
  • AbortSignal, echo, and timeoutMs work as on ordinary requests. Timeout covers an active streaming request (including WebSocket connection setup); uploads apply it to each chunk/finalization request. Zero disables timeout. Source reads and application callbacks are not timed by the request timeout.
  • Stopping iteration releases client resources. HTTP also cancels its response body. WebSocket stops collecting that request without closing the shared socket; the protocol has no per-request download cancellation message, so the server may continue sending remaining frames. Use unique echoes for transfers; do not reuse an aborted transfer's echo while late frames may still arrive.
  • WebSocket disconnection fails active transfers; reconnect does not replay them.
  • maxBufferBytes defaults to 32 MiB and bounds each HTTP frame or the queued WebSocket payloads. maxBufferedFrames defaults to 256 for WebSocket. An overflow throws instead of dropping frames or buffering the entire file. HTTP reads follow consumption; WebSocket has no per-request backpressure, so prefer HTTP for large downloads to a slow destination.

All new types are exported from @snowluma/sdk and @snowluma/sdk/types. Custom SnowLumaApiClient subclasses can opt in by implementing requestStream(); request-only subclasses remain compatible and receive an explicit unsupported error if streaming is attempted.