feat: stream files as binary frames over WebSocket (#659)
Replace base64-in-JSON file transfers with binary WebSocket frames to
unblock the JS thread when assistant markdown messages contain images.
Receive cost (JSON.parse over multi-MB base64) and persist cost (base64
re-encode to disk) both removed.
- Split daemon-client `exploreFileSystem` into `listDirectory` + `readFile`;
`readFile` returns `{ bytes, mime, size, modifiedAt }`. Encoding stays
private to the seam.
- New `packages/server/src/shared/binary-frames/` module hosts terminal
frames (moved from `terminal-stream-protocol.ts`) and new file-transfer
opcodes (`FileBegin` / `FileChunk` / `FileEnd`) with request-id correlation.
- Add optional `acceptBinary` to `FileExplorerRequestSchema`. New daemons
emit binary frames when set; legacy JSON path remains for old clients.
Compat fallback lives only inside `readFile`.
- New `persistAttachmentFromBytes` writes bytes directly: native via
`expo-file-system` `File.write(Uint8Array)`, web via Blob -> IndexedDB,
desktop via new `write_attachment_bytes` IPC. `persistAttachmentFromBase64`
removed.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { File } from "expo-file-system";
|
||||
import * as FileSystem from "expo-file-system/legacy";
|
||||
import {
|
||||
type AttachmentStore,
|
||||
@@ -6,7 +7,6 @@ import {
|
||||
type SaveAttachmentInput,
|
||||
} from "@/attachments/types";
|
||||
import {
|
||||
blobToBase64,
|
||||
fileUriToPath,
|
||||
generateAttachmentId,
|
||||
getFileExtensionFromName,
|
||||
@@ -45,6 +45,15 @@ async function ensureDirectory(uri: string): Promise<void> {
|
||||
await FileSystem.makeDirectoryAsync(uri, { intermediates: true });
|
||||
}
|
||||
|
||||
async function dataUrlToBytes(dataUrl: string): Promise<Uint8Array> {
|
||||
const response = await fetch(dataUrl);
|
||||
return new Uint8Array(await response.arrayBuffer());
|
||||
}
|
||||
|
||||
async function blobToBytes(blob: Blob): Promise<Uint8Array> {
|
||||
return new Uint8Array(await blob.arrayBuffer());
|
||||
}
|
||||
|
||||
async function writeFromSource(input: {
|
||||
source: SaveAttachmentInput["source"];
|
||||
targetUri: string;
|
||||
@@ -59,30 +68,16 @@ async function writeFromSource(input: {
|
||||
return;
|
||||
}
|
||||
|
||||
let bytes: Uint8Array;
|
||||
if (input.source.kind === "data_url") {
|
||||
const parsed = parseDataUrl(input.source.dataUrl);
|
||||
const mimeType = normalizeMimeType(parsed.mimeType || input.mimeType);
|
||||
const base64 = parsed.base64;
|
||||
await FileSystem.writeAsStringAsync(input.targetUri, base64, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
if (mimeType !== input.mimeType) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
bytes = await dataUrlToBytes(input.source.dataUrl);
|
||||
} else if (input.source.kind === "blob") {
|
||||
bytes = await blobToBytes(input.source.blob);
|
||||
} else {
|
||||
bytes = input.source.bytes;
|
||||
}
|
||||
|
||||
if (input.source.kind === "base64") {
|
||||
await FileSystem.writeAsStringAsync(input.targetUri, input.source.base64, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const base64 = await blobToBase64(input.source.blob);
|
||||
await FileSystem.writeAsStringAsync(input.targetUri, base64, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
new File(input.targetUri).write(bytes);
|
||||
}
|
||||
|
||||
function attachmentUri(metadata: AttachmentMetadata): string {
|
||||
|
||||
Reference in New Issue
Block a user