Files
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

131 lines
6.3 KiB
Markdown

# 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
```ts
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
```ts
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
```ts
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.