@deepseek-ai/dsh-client-file-upload 0.1.3-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +101 -0
- package/README.zh.md +101 -0
- package/lib/client.js +333 -0
- package/lib/index.js +318 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +614 -0
- package/lib/typert.remote-client.d.ts +26 -0
- package/lib/typert.remote-client.js +65 -0
- package/lib/types/client/contract.d.ts +27 -0
- package/lib/types/client/contract.js +2 -0
- package/lib/types/client/index.d.ts +19 -0
- package/lib/types/client/index.js +12 -0
- package/lib/types/client/runtime.d.ts +92 -0
- package/lib/types/client/runtime.js +270 -0
- package/lib/types/http-route.d.ts +10 -0
- package/lib/types/http-route.js +73 -0
- package/lib/types/index.d.ts +84 -0
- package/lib/types/index.js +243 -0
- package/lib/types/protocol.d.ts +3 -0
- package/lib/types/protocol.js +3 -0
- package/lib/types/types.d.ts +30 -0
- package/lib/types/types.js +3 -0
- package/package.json +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/client/file-upload/README.md
|
|
5
|
+
README.md: 2c19e1a564d7b58226e6ccd3cb5b7c81e1b1c14e
|
|
6
|
+
README.zh.md: d1553d40d953dd6378a954c43a9b7a36b7334953
|
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Session-addressed browser file uploads with streaming intake, progress, cancellation, and staged receipts for later prompts."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @deepseek-ai/dsh-client-file-upload
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
This package lets browser features store a `Blob`, exact bytes, or a `ReadableStream<Uint8Array>` for one Session and receive an opaque receipt for a later prompt. Served pages send Blob and stream bodies without aggregating their bytes on the page thread; pages whose Host runs in another execution context supply a Fetch-shaped carrier before Cordis boots. Callers can observe consumed bytes and cancel an active operation. A stream body is consumed once and transfers ownership when it crosses a Worker boundary. The standalone `?fixture` page uses the generated Remote for replayable Blob and exact-byte inputs.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
Mount the package before a consumer that injects `fileUpload`, then call `ctx.fileUpload.upload(sessionId, body, name, signal, onProgress)`. The Session identity addresses both the raw route and generated Remote fallback; callers do not assemble either request.
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
- id: file-upload
|
|
32
|
+
name: '@deepseek-ai/dsh-client-file-upload'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The package has no Cordis configuration fields. A `Blob` uses XMLHttpRequest inside a dedicated Worker so the service can report browser upload progress, including the total when the browser provides it. A `ReadableStream` transfers to that Worker and feeds Fetch incrementally; progress reports consumed bytes without a total. An `AbortSignal` terminates the dedicated Worker or reaches a page-owned carrier. Exact bytes and fixture Blob inputs use the generated Remote.
|
|
36
|
+
|
|
37
|
+
-----
|
|
38
|
+
|
|
39
|
+
<a id="understand-the-implementation"></a>
|
|
40
|
+
## Understand the implementation
|
|
41
|
+
|
|
42
|
+
<details>
|
|
43
|
+
<summary>Implementation internals — click to expand</summary>
|
|
44
|
+
|
|
45
|
+
The Client plugin provides `ctx.fileUpload`. Its `upload()` method receives the owning Session identity, assembles the raw route request, and invokes the generated Remote fallback for replayable inputs. The provider reads the optional pre-Cordis `__DSH_FILE_UPLOAD__` hook once. Without a hook, each non-fixture raw request owns a short-lived Worker and releases it after completion, failure, or cancellation. With the hook, the service sends the body through the page-owned Fetch carrier; the Web Worker runtime transfers stream bodies through its request frame and exposes them to the Host HTTP bridge as backpressured chunks.
|
|
46
|
+
|
|
47
|
+
The Host plugin provides `ctx.fileUploads`. It owns the authenticated streaming route, encoded Remote fallback, command receipt resolver, and staged-receipt lifecycle; encoded admission, attachment-error recognition, and byte storage stay behind `ctx.attachments`. Receipt tables use the receiving Agent's Session object as their key. The Session Controller registers the resolver that can resume a cold ordinary Agent and consumes receipts during prompt admission. Prompt delivery holds each receipt binding in a disposable transaction: disposal restores the previous binding until successful delivery commits it, and queue or history observation then retires the committed receipt.
|
|
48
|
+
|
|
49
|
+
| File | Role |
|
|
50
|
+
|---|---|
|
|
51
|
+
| [`src/index.ts`](src/index.ts) | Host streaming route, attachment-service admission, and Agent-scoped receipt lifecycle |
|
|
52
|
+
| [`src/types.ts`](src/types.ts) | encoded request, receipt, and durable result types |
|
|
53
|
+
| [`src/client/contract.ts`](src/client/contract.ts) | Client upload, progress, and page-hook types |
|
|
54
|
+
| [`src/client/runtime.ts`](src/client/runtime.ts) | Dedicated Worker and page-owned carrier implementations |
|
|
55
|
+
| [`src/client/index.ts`](src/client/index.ts) | Client plugin registration and `ctx.fileUpload` declaration |
|
|
56
|
+
|
|
57
|
+
</details>
|
|
58
|
+
|
|
59
|
+
**Runtime invariant:** No companion is published. Each upload receipt belongs to one exact Session, and each request uses one selected carrier. Unsupported stream carriers fail before the body is sent.
|
|
60
|
+
|
|
61
|
+
-----
|
|
62
|
+
|
|
63
|
+
<a id="further-exploration"></a>
|
|
64
|
+
## Further Exploration
|
|
65
|
+
|
|
66
|
+
- [Connection](../connection/README.md) — authenticated RPC, exact Host routes, and connection generations.
|
|
67
|
+
- [Session Controller](../../api/session-controller/README.md) — prompt admission that consumes staged receipts.
|
|
68
|
+
- [Web Worker runtime](../../experimental/webworker-runtime/README.md) — the page-to-Host Worker request tunnel.
|
|
69
|
+
- [Client group map](../README.md) — browser services and UI feature packages.
|
|
70
|
+
|
|
71
|
+
-----
|
|
72
|
+
|
|
73
|
+
<a id="model-experience"></a>
|
|
74
|
+
## Model Experience
|
|
75
|
+
|
|
76
|
+
None, as this package transfers browser request bodies and contributes no model input.
|
|
77
|
+
|
|
78
|
+
#### KV Cache effect
|
|
79
|
+
|
|
80
|
+
None; this package neither assembles nor sends a provider request.
|
|
81
|
+
|
|
82
|
+
## Known Limitations and Deferred Work
|
|
83
|
+
|
|
84
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
85
|
+
|
|
86
|
+
These limits apply to the transport operation itself.
|
|
87
|
+
|
|
88
|
+
- **Uploads are not resumable** — a failed or cancelled retry starts from the first byte.
|
|
89
|
+
- **Stream bodies are one-shot** — transferring a `ReadableStream` locks the caller's object, so retry requires a newly created stream.
|
|
90
|
+
- **Stream progress has no total** — callers receive consumed-byte counts because the stream API carries no byte length.
|
|
91
|
+
- **The browser Worker is self-contained** — its source is emitted from a function string. Adding runtime imports requires moving it to a standalone Worker entry bundled by tsdown.
|
|
92
|
+
|
|
93
|
+
<a id="dev-note"></a>
|
|
94
|
+
### Dev Note
|
|
95
|
+
|
|
96
|
+
<details>
|
|
97
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
98
|
+
|
|
99
|
+
None.
|
|
100
|
+
|
|
101
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "按 Session 寻址上传浏览器文件,提供流式接收、进度、取消和供后续 prompt 使用的暂存凭证。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @deepseek-ai/dsh-client-file-upload
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
本包让浏览器功能为一个 Session 存储 `Blob`、精确字节或 `ReadableStream<Uint8Array>`,并取得供后续 prompt 使用的不透明凭证。普通服务页面发送 Blob 和 stream 请求体时,不会在页面线程聚合全部字节;Host 位于其他执行上下文中的页面会在 Cordis 启动前提供 Fetch 形式的载体。调用方可以观察已消费字节并取消活动操作。stream 请求体只能消费一次,跨 Worker 边界时会转移所有权。独立的 `?fixture` 页面通过生成的 Remote 处理可重放的 Blob 与精确字节输入。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
在注入 `fileUpload` 的消费方之前挂载本包,再调用 `ctx.fileUpload.upload(sessionId, body, name, signal, onProgress)`。Session 标识同时用于寻址原始路由和生成的 Remote 兜底;调用方不组装这两种请求。
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
- id: file-upload
|
|
32
|
+
name: '@deepseek-ai/dsh-client-file-upload'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
本包没有 Cordis 配置字段。`Blob` 在专用 Worker 内通过 XMLHttpRequest 发送,因此服务可以报告浏览器上传进度,并在浏览器提供总量时一并报告。`ReadableStream` 会转移给该 Worker,再增量传入 Fetch;进度只报告已消费字节,不包含总量。`AbortSignal` 会终止专用 Worker,或传递给页面自己提供的载体。精确字节与 fixture Blob 输入使用生成的 Remote。
|
|
36
|
+
|
|
37
|
+
-----
|
|
38
|
+
|
|
39
|
+
<a id="understand-the-implementation"></a>
|
|
40
|
+
## 理解实现
|
|
41
|
+
|
|
42
|
+
<details>
|
|
43
|
+
<summary>实现细节——点击展开</summary>
|
|
44
|
+
|
|
45
|
+
Client 插件提供 `ctx.fileUpload`。其 `upload()` 方法接收所属 Session 标识,组装原始路由请求,并为可重放输入调用生成的 Remote 兜底。提供方只读取一次可选的 Cordis 启动前 `__DSH_FILE_UPLOAD__` 钩子。没有该钩子时,每个非 fixture 原始请求拥有一个短期 Worker,并在完成、失败或取消后释放。存在该钩子时,服务通过页面自己提供的 Fetch 载体发送请求体;Web Worker runtime 会通过请求帧转移 stream 请求体,再以带背压的分片形式交给 Host HTTP bridge。
|
|
46
|
+
|
|
47
|
+
Host 插件提供 `ctx.fileUploads`。它拥有经过认证的流式路由、编码 Remote 兜底、命令凭证解析器与暂存凭证生命周期;编码准入、附件错误识别与字节存储仍由 `ctx.attachments` 提供。凭证表以接收方 Agent 的 Session 对象为键。Session Controller 注册可恢复休眠普通 Agent 的解析器,并在 prompt 准入时消费凭证。Prompt 投递通过可释放事务持有每个凭证绑定。成功投递提交事务前,释放会恢复原绑定;提交后,队列或历史观察会退休该凭证。
|
|
48
|
+
|
|
49
|
+
| 文件 | 职责 |
|
|
50
|
+
|---|---|
|
|
51
|
+
| [`src/index.ts`](src/index.ts) | Host 流式路由、附件服务准入与按 Agent scope 管理的凭证生命周期 |
|
|
52
|
+
| [`src/types.ts`](src/types.ts) | 编码请求、凭证与持久结果类型 |
|
|
53
|
+
| [`src/client/contract.ts`](src/client/contract.ts) | Client 上传、进度与页面钩子类型 |
|
|
54
|
+
| [`src/client/runtime.ts`](src/client/runtime.ts) | 专用 Worker 与页面自有载体实现 |
|
|
55
|
+
| [`src/client/index.ts`](src/client/index.ts) | Client 插件注册与 `ctx.fileUpload` 声明 |
|
|
56
|
+
|
|
57
|
+
</details>
|
|
58
|
+
|
|
59
|
+
**运行时不变式:** 不发布伴生入口。每个上传凭证只属于一个准确的 Session,每个请求只使用一个已选定载体。载体不支持的 stream 会在发送请求体前失败。
|
|
60
|
+
|
|
61
|
+
-----
|
|
62
|
+
|
|
63
|
+
<a id="further-exploration"></a>
|
|
64
|
+
## 进一步探索
|
|
65
|
+
|
|
66
|
+
- [Connection](../connection/README.zh.md)——认证 RPC、Host 精确路由与 connection generation。
|
|
67
|
+
- [Session Controller](../../api/session-controller/README.zh.md)——消费暂存凭证的 prompt 准入。
|
|
68
|
+
- [Web Worker runtime](../../experimental/webworker-runtime/README.zh.md)——页面到 Host Worker 的请求隧道。
|
|
69
|
+
- [客户端组地图](../README.zh.md)——浏览器服务与 UI 功能包。
|
|
70
|
+
|
|
71
|
+
-----
|
|
72
|
+
|
|
73
|
+
<a id="model-experience"></a>
|
|
74
|
+
## 模型体验
|
|
75
|
+
|
|
76
|
+
无。本包只传输浏览器请求体,不提供模型输入。
|
|
77
|
+
|
|
78
|
+
#### KV Cache 影响
|
|
79
|
+
|
|
80
|
+
无;本包既不组装也不发送提供方请求。
|
|
81
|
+
|
|
82
|
+
## 已知限制与延期工作
|
|
83
|
+
|
|
84
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
85
|
+
|
|
86
|
+
以下限制适用于传输操作本身。
|
|
87
|
+
|
|
88
|
+
- **上传不能断点续传**:失败或取消后的重试会从第一个字节开始。
|
|
89
|
+
- **stream 请求体只能使用一次**:转移 `ReadableStream` 会锁定调用方的对象,因此重试必须重新创建 stream。
|
|
90
|
+
- **stream 进度没有总量**:stream API 不携带字节长度,因此调用方只能收到已消费字节数。
|
|
91
|
+
- **浏览器 Worker 必须自包含**:其源代码由函数字符串生成。如果实现需要运行时 import,就必须迁移为由 tsdown 打包的独立 Worker 入口。
|
|
92
|
+
|
|
93
|
+
<a id="dev-note"></a>
|
|
94
|
+
### 开发备注
|
|
95
|
+
|
|
96
|
+
<details>
|
|
97
|
+
<summary>维护者工作上下文——点击展开</summary>
|
|
98
|
+
|
|
99
|
+
无。
|
|
100
|
+
|
|
101
|
+
</details>
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@deepseek-ai/dsh-client-file-upload",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
|
|
8
|
+
//#region ../../util/crypto/lib/index.js
|
|
9
|
+
/**
|
|
10
|
+
* UUID minting that works in every JavaScript context this repository ships
|
|
11
|
+
* to. `crypto.randomUUID` is a secure-context Web API — a page or worker
|
|
12
|
+
* served over plain HTTP on a LAN address has no such method — while
|
|
13
|
+
* `crypto.getRandomValues` is unrestricted everywhere (browsers, workers,
|
|
14
|
+
* Node ≥ 19). One implementation here replaces per-caller polyfills; the
|
|
15
|
+
* `no-restricted-properties` lint rule points `crypto.randomUUID` callers at
|
|
16
|
+
* this module.
|
|
17
|
+
* @module @deepseek-ai/dsh-util-crypto
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Encode bytes as canonical base64 without overflowing function argument limits.
|
|
21
|
+
* @param data - Bytes to encode.
|
|
22
|
+
* @returns base64 text.
|
|
23
|
+
*/
|
|
24
|
+
function bytesToBase64(data) {
|
|
25
|
+
let binary = "";
|
|
26
|
+
const chunk = 32768;
|
|
27
|
+
for (let offset = 0; offset < data.length; offset += chunk) binary += String.fromCharCode(...data.subarray(offset, offset + chunk));
|
|
28
|
+
return btoa(binary);
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region ../../typert/protocol/lib/index.js
|
|
32
|
+
/** The one Remote failure class shared by owners, the Gateway, and consumers. */
|
|
33
|
+
/**
|
|
34
|
+
* One Remote call failure: a real Error carrying its stable code and typed
|
|
35
|
+
* details. Owners throw it at the failure point; the Host Gateway encodes it
|
|
36
|
+
* onto the wire unchanged; the Client face rebuilds an instance for the
|
|
37
|
+
* `RemoteResult` error branch, so `throw result.error` keeps throw semantics.
|
|
38
|
+
* Discrimination is always by `code`, never by instanceof.
|
|
39
|
+
*/
|
|
40
|
+
var RemoteError = class extends Error {
|
|
41
|
+
code;
|
|
42
|
+
details;
|
|
43
|
+
/** Structural marker: cross-realm/bundle identification never uses instanceof. */
|
|
44
|
+
isDSHRemoteError = true;
|
|
45
|
+
/**
|
|
46
|
+
* @param code - stable failure code declared in {@link RemoteErrorDetailsMap}.
|
|
47
|
+
* @param message - human diagnostic carried across the wire.
|
|
48
|
+
* @param details - structured payload typed by the code.
|
|
49
|
+
* @param options - standard Error options (`cause` survives in-process only).
|
|
50
|
+
*/
|
|
51
|
+
constructor(code, message, details, options) {
|
|
52
|
+
super(message, options);
|
|
53
|
+
this.code = code;
|
|
54
|
+
this.details = details;
|
|
55
|
+
this.name = "RemoteError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region lib/types/protocol.js
|
|
60
|
+
/** Authenticated raw-byte route owned by the file-upload service. */
|
|
61
|
+
const FILE_UPLOAD_PATH = "/api/session/uploadFileBinary";
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region lib/types/client/runtime.js
|
|
64
|
+
/** Background browser upload implementation for Blob and byte-stream bodies. */
|
|
65
|
+
/**
|
|
66
|
+
* Self-contained Worker body; its string form becomes the Blob Worker source.
|
|
67
|
+
* @param scope - Worker global used for requests and progress messages.
|
|
68
|
+
* @param createXhr - XMLHttpRequest factory used for Blob progress.
|
|
69
|
+
* @param doFetch - Fetch carrier used for one-shot ReadableStream bodies.
|
|
70
|
+
*/
|
|
71
|
+
function fileUploadWorker(scope = self, createXhr = () => new XMLHttpRequest(), doFetch = (input, init) => fetch(input, init)) {
|
|
72
|
+
scope.onmessage = (event) => {
|
|
73
|
+
const request = event.data;
|
|
74
|
+
if (request.body instanceof Blob) {
|
|
75
|
+
const xhr = createXhr();
|
|
76
|
+
xhr.open("POST", request.url);
|
|
77
|
+
xhr.withCredentials = true;
|
|
78
|
+
for (const [name, value] of Object.entries(request.headers)) xhr.setRequestHeader(name, value);
|
|
79
|
+
xhr.upload.onprogress = (progress) => {
|
|
80
|
+
scope.postMessage({
|
|
81
|
+
kind: "progress",
|
|
82
|
+
loaded: progress.loaded,
|
|
83
|
+
...progress.lengthComputable ? { total: progress.total } : {}
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
xhr.onload = () => {
|
|
87
|
+
scope.postMessage({
|
|
88
|
+
kind: "complete",
|
|
89
|
+
status: xhr.status,
|
|
90
|
+
body: xhr.responseText
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
xhr.onerror = () => {
|
|
94
|
+
scope.postMessage({
|
|
95
|
+
kind: "error",
|
|
96
|
+
message: "background upload transport failed"
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
xhr.send(request.body);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!(request.body instanceof ReadableStream)) {
|
|
103
|
+
scope.postMessage({
|
|
104
|
+
kind: "error",
|
|
105
|
+
message: "background upload worker received an invalid body"
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const source = request.body;
|
|
110
|
+
(async () => {
|
|
111
|
+
const reader = source.getReader();
|
|
112
|
+
let loaded = 0;
|
|
113
|
+
const body = new ReadableStream({
|
|
114
|
+
async pull(controller) {
|
|
115
|
+
const item = await reader.read();
|
|
116
|
+
if (item.done) {
|
|
117
|
+
controller.close();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!(item.value instanceof Uint8Array)) throw new TypeError("background upload stream produced a non-Uint8Array chunk");
|
|
121
|
+
loaded += item.value.byteLength;
|
|
122
|
+
scope.postMessage({
|
|
123
|
+
kind: "progress",
|
|
124
|
+
loaded
|
|
125
|
+
});
|
|
126
|
+
controller.enqueue(item.value);
|
|
127
|
+
},
|
|
128
|
+
async cancel(reason) {
|
|
129
|
+
await reader.cancel(reason);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
const response = await doFetch(request.url, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: request.headers,
|
|
135
|
+
credentials: "include",
|
|
136
|
+
body,
|
|
137
|
+
duplex: "half"
|
|
138
|
+
});
|
|
139
|
+
scope.postMessage({
|
|
140
|
+
kind: "complete",
|
|
141
|
+
status: response.status,
|
|
142
|
+
body: await response.text()
|
|
143
|
+
});
|
|
144
|
+
})().catch((error) => {
|
|
145
|
+
scope.postMessage({
|
|
146
|
+
kind: "error",
|
|
147
|
+
message: error instanceof Error ? error.message : String(error)
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/** Cordis service that owns one background carrier per upload operation. */
|
|
153
|
+
var FileUploadRuntime = class extends _deepseek_ai_cordis.Service {
|
|
154
|
+
available;
|
|
155
|
+
transport;
|
|
156
|
+
/** @param ctx - providing Client context. */
|
|
157
|
+
constructor(ctx) {
|
|
158
|
+
super(ctx, "fileUpload");
|
|
159
|
+
const hook = globalThis.__DSH_FILE_UPLOAD__;
|
|
160
|
+
this.available = hook !== void 0 || !isFixturePage();
|
|
161
|
+
this.transport = hook === void 0 ? workerTransport() : customTransport(hook.fetch);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Post one body with the carrier selected before Cordis boot.
|
|
165
|
+
* @param request - target, body, cancellation, and progress observer.
|
|
166
|
+
* @returns the response status and text body.
|
|
167
|
+
*/
|
|
168
|
+
post(request) {
|
|
169
|
+
if (!this.available) return Promise.reject(/* @__PURE__ */ new Error("background upload is unavailable in fixture mode"));
|
|
170
|
+
return this.transport.post(request);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Store one file for a Session.
|
|
174
|
+
* @param sessionId - Session that owns the staged receipt.
|
|
175
|
+
* @param data - browser Blob, exact bytes, or a one-shot byte stream.
|
|
176
|
+
* @param name - optional display name.
|
|
177
|
+
* @param signal - optional cancellation for the active upload.
|
|
178
|
+
* @param onProgress - optional byte-progress observer for background bodies.
|
|
179
|
+
* @returns the staged receipt and durable file reference, or a business error.
|
|
180
|
+
*/
|
|
181
|
+
async upload(sessionId, data, name, signal, onProgress) {
|
|
182
|
+
if (!(data instanceof Uint8Array) && this.available) {
|
|
183
|
+
const query = new URLSearchParams({ sessionId });
|
|
184
|
+
if (name !== void 0) query.set("name", name);
|
|
185
|
+
const response = await this.post({
|
|
186
|
+
path: `${FILE_UPLOAD_PATH}?${query.toString()}`,
|
|
187
|
+
body: data,
|
|
188
|
+
headers: { "content-type": "application/octet-stream" },
|
|
189
|
+
...signal === void 0 ? {} : { signal },
|
|
190
|
+
...onProgress === void 0 ? {} : { onProgress }
|
|
191
|
+
});
|
|
192
|
+
if (response.status !== 200) throw new Error(`file upload transport failed with HTTP ${String(response.status)}`);
|
|
193
|
+
return parseFileUploadResult(response.body);
|
|
194
|
+
}
|
|
195
|
+
if (!(data instanceof Uint8Array) && !(data instanceof Blob)) throw new Error("stream file upload requires a background carrier");
|
|
196
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(await data.arrayBuffer());
|
|
197
|
+
return this.ctx.remote.fileUploads.upload(sessionId, {
|
|
198
|
+
data: bytesToBase64(bytes),
|
|
199
|
+
...name === void 0 ? {} : { name }
|
|
200
|
+
}, signal);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
function customTransport(customFetch) {
|
|
204
|
+
return { async post(request) {
|
|
205
|
+
const init = {
|
|
206
|
+
method: "POST",
|
|
207
|
+
...request.headers === void 0 ? {} : { headers: request.headers },
|
|
208
|
+
body: request.body,
|
|
209
|
+
...request.body instanceof ReadableStream ? { duplex: "half" } : {},
|
|
210
|
+
...request.signal === void 0 ? {} : { signal: request.signal }
|
|
211
|
+
};
|
|
212
|
+
const response = await customFetch(resolveUrl(request.path), init);
|
|
213
|
+
return {
|
|
214
|
+
status: response.status,
|
|
215
|
+
body: await response.text()
|
|
216
|
+
};
|
|
217
|
+
} };
|
|
218
|
+
}
|
|
219
|
+
function workerTransport() {
|
|
220
|
+
return { post(request) {
|
|
221
|
+
if (typeof Worker !== "function") return Promise.reject(/* @__PURE__ */ new Error("background upload requires Web Worker support"));
|
|
222
|
+
const workerUrl = URL.createObjectURL(new Blob([`(${fileUploadWorker.toString()})()`], { type: "text/javascript" }));
|
|
223
|
+
const worker = new Worker(workerUrl, { name: "dsh-file-upload" });
|
|
224
|
+
URL.revokeObjectURL(workerUrl);
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
let settled = false;
|
|
227
|
+
const abort = () => {
|
|
228
|
+
settled = true;
|
|
229
|
+
worker.terminate();
|
|
230
|
+
request.signal?.removeEventListener("abort", abort);
|
|
231
|
+
reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
232
|
+
};
|
|
233
|
+
const finish = (settle) => {
|
|
234
|
+
if (settled) return;
|
|
235
|
+
settled = true;
|
|
236
|
+
request.signal?.removeEventListener("abort", abort);
|
|
237
|
+
worker.terminate();
|
|
238
|
+
settle();
|
|
239
|
+
};
|
|
240
|
+
worker.onmessage = (event) => {
|
|
241
|
+
const output = event.data;
|
|
242
|
+
if (output.kind === "progress") request.onProgress?.({
|
|
243
|
+
loaded: output.loaded,
|
|
244
|
+
...output.total === void 0 ? {} : { total: output.total }
|
|
245
|
+
});
|
|
246
|
+
else if (output.kind === "complete") finish(() => {
|
|
247
|
+
resolve({
|
|
248
|
+
status: output.status,
|
|
249
|
+
body: output.body
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
else finish(() => {
|
|
253
|
+
reject(new Error(output.message));
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
worker.onerror = (event) => {
|
|
257
|
+
finish(() => {
|
|
258
|
+
reject(new Error(event.message || "background upload worker failed"));
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
if (request.signal?.aborted === true) {
|
|
262
|
+
abort();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
request.signal?.addEventListener("abort", abort, { once: true });
|
|
266
|
+
const message = {
|
|
267
|
+
url: resolveUrl(request.path).href,
|
|
268
|
+
body: request.body,
|
|
269
|
+
headers: request.headers ?? {}
|
|
270
|
+
};
|
|
271
|
+
if (request.body instanceof ReadableStream) worker.postMessage(message, [request.body]);
|
|
272
|
+
else worker.postMessage(message);
|
|
273
|
+
});
|
|
274
|
+
} };
|
|
275
|
+
}
|
|
276
|
+
function resolveUrl(path) {
|
|
277
|
+
const pageLocation = Reflect.get(globalThis, "location");
|
|
278
|
+
const origin = typeof pageLocation === "object" && pageLocation !== null && "origin" in pageLocation && typeof pageLocation.origin === "string" ? pageLocation.origin : void 0;
|
|
279
|
+
return new URL(path, origin === void 0 || origin === "null" ? "http://dsh.internal" : origin);
|
|
280
|
+
}
|
|
281
|
+
function isFixturePage() {
|
|
282
|
+
const pageLocation = Reflect.get(globalThis, "location");
|
|
283
|
+
return typeof pageLocation === "object" && pageLocation !== null && "search" in pageLocation && typeof pageLocation.search === "string" && new URLSearchParams(pageLocation.search).has("fixture");
|
|
284
|
+
}
|
|
285
|
+
function parseFileUploadResult(body) {
|
|
286
|
+
const value = JSON.parse(body);
|
|
287
|
+
if (!isRecord(value) || typeof value.ok !== "boolean") throw new TypeError("file upload transport returned an invalid result");
|
|
288
|
+
if (!value.ok) {
|
|
289
|
+
const error = value.error;
|
|
290
|
+
if (!isRecord(error) || typeof error.code !== "string" || typeof error.message !== "string" || !isRecord(error.details)) throw new TypeError("file upload transport returned an invalid failure");
|
|
291
|
+
return {
|
|
292
|
+
ok: false,
|
|
293
|
+
error: new RemoteError(error.code, error.message, error.details)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const result = value.value;
|
|
297
|
+
const file = isRecord(result) ? result.file : void 0;
|
|
298
|
+
if (!isRecord(result) || typeof result.receiptId !== "string" || !isRecord(file) || typeof file.attachmentId !== "string" || typeof file.name !== "string" || typeof file.bytes !== "number" || !Number.isSafeInteger(file.bytes) || file.bytes < 0) throw new TypeError("file upload transport returned an invalid receipt");
|
|
299
|
+
return {
|
|
300
|
+
ok: true,
|
|
301
|
+
value: {
|
|
302
|
+
receiptId: result.receiptId,
|
|
303
|
+
file: {
|
|
304
|
+
attachmentId: file.attachmentId,
|
|
305
|
+
name: file.name,
|
|
306
|
+
bytes: file.bytes
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function isRecord(value) {
|
|
312
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region lib/types/client/index.js
|
|
316
|
+
/** Browser background-upload Cordis service. */
|
|
317
|
+
/** The upload service uses the generated Remote fallback. */
|
|
318
|
+
const inject = ["remote"];
|
|
319
|
+
/**
|
|
320
|
+
* Provide the browser background-upload service.
|
|
321
|
+
* @param ctx - Client plugin context.
|
|
322
|
+
*/
|
|
323
|
+
function apply(ctx) {
|
|
324
|
+
ctx.plugin(FileUploadRuntime);
|
|
325
|
+
}
|
|
326
|
+
//#endregion
|
|
327
|
+
exports.apply = apply;
|
|
328
|
+
exports.inject = inject;
|
|
329
|
+
return module.exports;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
//# sourceMappingURL=client.js.map
|