@qoniai/qoni 0.4.1 → 0.4.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/CHANGELOG.md +21 -0
- package/README.md +21 -1
- package/README.zh-CN.md +14 -1
- package/dist/index.cjs +201 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +201 -105
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ This project follows semantic versioning. Before `1.0.0`, minor versions may
|
|
|
6
6
|
include SDK API changes, while patch versions are reserved for backward
|
|
7
7
|
compatible fixes.
|
|
8
8
|
|
|
9
|
+
## 0.4.2 - 2026-09-09
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Require authoritative run details when settling `wait()`: preserve detail
|
|
14
|
+
errors and reject unconfirmed terminal states instead of inventing success.
|
|
15
|
+
- Reuse terminal details read after a clean stream close and resume from every
|
|
16
|
+
wire event cursor, including frames filtered from semantic events.
|
|
17
|
+
- Propagate wait cancellation to detail and artifact-list GETs. Bound complete
|
|
18
|
+
JSON, NDJSON, binary, and HTTP error body reads by the request timeout, cancel
|
|
19
|
+
stalled readers, and discard late responses without starting replacement runs.
|
|
20
|
+
- Clean up wait timers and SSE abort listeners. Cancel idle SSE readers directly
|
|
21
|
+
and release their locks without waiting indefinitely for source cancellation.
|
|
22
|
+
- Retry HTTP 408 stream failures within the configured reconnect budget and stop
|
|
23
|
+
delivering buffered events after cancellation.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Optional abort signals on product run-detail and artifact-list wire reads;
|
|
28
|
+
existing calls without a signal remain compatible.
|
|
29
|
+
|
|
9
30
|
## Unreleased
|
|
10
31
|
|
|
11
32
|
### Breaking
|
package/README.md
CHANGED
|
@@ -512,7 +512,7 @@ const qoni = new Qoni({
|
|
|
512
512
|
| `secretKey` | `string` | Yes | Qoni secret key from Qoni Console. |
|
|
513
513
|
| `host` | `string` | No | Optional Qoni Console/SDK gateway override for private or local deployments. |
|
|
514
514
|
| `fetch` | `typeof fetch` | No | Custom transport implementation. |
|
|
515
|
-
| `timeoutMs` | `number` | No | Per-request timeout (connect +
|
|
515
|
+
| `timeoutMs` | `number` | No | Per-request timeout (connect + headers + body for one-shot requests; headers only for successful SSE connections). Defaults to `30000`. |
|
|
516
516
|
|
|
517
517
|
> **`timeoutMs` does not bound a long-running run.** It applies to individual
|
|
518
518
|
> one-shot HTTP requests (delegation, `status()`, a single POST). The live
|
|
@@ -550,6 +550,26 @@ await run.cancel(reason?) // idempotent
|
|
|
550
550
|
|
|
551
551
|
`await run.wait(...)` settles to a `RunResult`:
|
|
552
552
|
|
|
553
|
+
The run-detail response must confirm a terminal state. Detail read errors propagate
|
|
554
|
+
unchanged; `wait()` never substitutes an SSE completion payload for the authoritative
|
|
555
|
+
result. A nonterminal or incomplete detail rejects with a retryable `upstream.failed`
|
|
556
|
+
error; use `status()` or re-attach to recover.
|
|
557
|
+
|
|
558
|
+
`timeoutMs` and `signal` stop waiting for detail/artifact reads and promises returned
|
|
559
|
+
by `onEvent`, `onWireEvent`, `onInteraction`, and `onScreenshot`. They cannot preempt
|
|
560
|
+
synchronous JavaScript or stop work already started inside a callback. `onReconnect`
|
|
561
|
+
is synchronous telemetry: its return value is not awaited and returned promise
|
|
562
|
+
rejections are not handled. Do not pass an async callback there; synchronous throws
|
|
563
|
+
propagate. Reconnect backoff remains abortable and subject to the wait deadline.
|
|
564
|
+
|
|
565
|
+
Detail/artifact-list GETs receive the wait signal: aborting or timing out a wait aborts
|
|
566
|
+
the request and cancels its body reader. The client's per-request timer also covers
|
|
567
|
+
complete JSON/NDJSON and binary body reads, including HTTP error bodies. A late response
|
|
568
|
+
from a custom fetch that ignores abort has its body canceled and cannot resume settlement
|
|
569
|
+
or start subsequent run requests. Custom transports must honor `RequestInit.signal` to
|
|
570
|
+
stop their own in-flight network work. Stopping a wait neither cancels the server-side
|
|
571
|
+
run nor creates a replacement task; use `cancel()` explicitly to stop the existing run.
|
|
572
|
+
|
|
553
573
|
```ts
|
|
554
574
|
interface RunResult {
|
|
555
575
|
runId: string;
|
package/README.zh-CN.md
CHANGED
|
@@ -482,7 +482,7 @@ const qoni = new Qoni({
|
|
|
482
482
|
| `secretKey` | `string` | 是 | Qoni Console 创建的 secret key。 |
|
|
483
483
|
| `host` | `string` | 否 | 私有化或本地部署时可选的 Qoni Console/SDK 网关覆盖地址。 |
|
|
484
484
|
| `fetch` | `typeof fetch` | 否 | 自定义传输实现。 |
|
|
485
|
-
| `timeoutMs` | `number` | 否 |
|
|
485
|
+
| `timeoutMs` | `number` | 否 | 单次请求超时(一次性请求覆盖连接、响应头及响应体;成功建立的 SSE 仅覆盖到响应头),默认 `30000`。 |
|
|
486
486
|
|
|
487
487
|
> **`timeoutMs` 不会限制长任务。** 它只作用于单次一次性 HTTP 请求(委托、`status()`、单个 POST)。事件流(`events()` / `wait()`)**不受它约束**——流的响应头一到,这个单次计时器就被清掉了,所以一个跑一小时的 Deep Research 在默认 30s 客户端超时下也能正常流式。要限制等待时长用 `wait({ timeoutMs })`;不传则 `wait()` 一直等到终态。`wait()` 超时**不会停止 run**——它在服务端继续跑,可以再 `attach()` 回去。
|
|
488
488
|
|
|
@@ -512,6 +512,19 @@ await run.cancel(reason?) // 幂等
|
|
|
512
512
|
|
|
513
513
|
`await run.wait(...)` 结算成一个 `RunResult`:
|
|
514
514
|
|
|
515
|
+
必须由 run-detail 响应确认终态。详情读取错误原样抛出,`wait()` 不会用 SSE 完成事件替代权威结果。
|
|
516
|
+
详情仍为非终态或缺少终态信息时,抛出可重试的 `upstream.failed`;可通过 `status()` 或重新 `attach()` 恢复。
|
|
517
|
+
|
|
518
|
+
`timeoutMs` 与 `signal` 会结束详情、交付物读取及 `onEvent`、`onWireEvent`、`onInteraction`、
|
|
519
|
+
`onScreenshot` 返回的 Promise 的等待,但不能抢占同步 JavaScript,也不能停止回调内部已启动的工作。
|
|
520
|
+
`onReconnect` 是同步遥测通知:返回值不被等待,返回的 Promise 的 rejection 也不被处理,因此不要传 async
|
|
521
|
+
回调;同步抛错会向上传播。重连退避等待仍可被 signal 或 wait 超时中断。
|
|
522
|
+
|
|
523
|
+
详情/交付物列表 GET 会接收 wait signal:wait 中止或超时时会中止请求、取消并释放响应体 reader。
|
|
524
|
+
客户端单次请求计时器也覆盖完整 JSON/NDJSON、二进制响应体及 HTTP 错误体读取。即使自定义 fetch 忽略
|
|
525
|
+
abort,SDK 也会取消其迟到响应体,不恢复结算或启动后续 run 请求;自定义传输须遵守 `RequestInit.signal`
|
|
526
|
+
才能停止其内部网络工作。停止 wait 既不会取消服务端 run,也不会创建替代任务;需要停止原 run 时须显式调用 `cancel()`。
|
|
527
|
+
|
|
515
528
|
```ts
|
|
516
529
|
interface RunResult {
|
|
517
530
|
runId: string;
|
package/dist/index.cjs
CHANGED
|
@@ -816,11 +816,11 @@ var RunHandle = class {
|
|
|
816
816
|
}
|
|
817
817
|
/** Drive the run to a terminal state and return the settled result. */
|
|
818
818
|
async wait(opts = {}) {
|
|
819
|
-
const { signal, timedOut } = withTimeout(opts.signal, opts.timeoutMs);
|
|
819
|
+
const { signal, timedOut, dispose } = withTimeout(opts.signal, opts.timeoutMs);
|
|
820
820
|
let lastEventId;
|
|
821
821
|
let eventIdAtLastClose;
|
|
822
822
|
let sawCleanClose = false;
|
|
823
|
-
let
|
|
823
|
+
let terminalDetail;
|
|
824
824
|
let terminalDoneReason;
|
|
825
825
|
let screenshotIndex = 0;
|
|
826
826
|
try {
|
|
@@ -828,24 +828,28 @@ var RunHandle = class {
|
|
|
828
828
|
for await (const event of this.events({
|
|
829
829
|
signal,
|
|
830
830
|
lastEventId,
|
|
831
|
-
onWireEvent:
|
|
831
|
+
onWireEvent: (wire) => waitWithSignal(() => {
|
|
832
|
+
if (wire.id !== void 0) lastEventId = wire.id;
|
|
833
|
+
return opts.onWireEvent?.(wire);
|
|
834
|
+
}, signal),
|
|
832
835
|
onReconnect: opts.onReconnect
|
|
833
836
|
})) {
|
|
834
|
-
|
|
835
|
-
await opts.onEvent?.(event);
|
|
837
|
+
await waitWithSignal(() => opts.onEvent?.(event), signal);
|
|
836
838
|
if (event.type === "screenshot") {
|
|
837
|
-
await opts.onScreenshot?.(event.image, screenshotIndex++);
|
|
839
|
+
await waitWithSignal(() => opts.onScreenshot?.(event.image, screenshotIndex++), signal);
|
|
838
840
|
} else if (event.type === "interaction") {
|
|
839
|
-
await opts.onInteraction?.(this.interactionHandle(event.data), event);
|
|
841
|
+
await waitWithSignal(() => opts.onInteraction?.(this.interactionHandle(event.data), event), signal);
|
|
840
842
|
}
|
|
841
843
|
if (event.isTerminal) {
|
|
842
|
-
terminalPayload = isJsonObject(event.raw.data) ? event.raw.data : void 0;
|
|
843
844
|
if (event.type === "done") terminalDoneReason = event.data.terminalReason || void 0;
|
|
844
845
|
break settled;
|
|
845
846
|
}
|
|
846
847
|
}
|
|
847
|
-
const detail2 = await this.ops.getDetail();
|
|
848
|
-
if (isTerminalRunStatus(detail2.status))
|
|
848
|
+
const detail2 = await waitWithSignal(() => this.ops.getDetail(signal), signal);
|
|
849
|
+
if (isTerminalRunStatus(normalizeRunStatus(this.id, detail2).status)) {
|
|
850
|
+
terminalDetail = detail2;
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
849
853
|
if (sawCleanClose && lastEventId === eventIdAtLastClose) {
|
|
850
854
|
throw new QoniError(
|
|
851
855
|
`run event stream ended before a terminal event while the run is still "${String(detail2.status)}" \u2014 re-attach or poll status()`,
|
|
@@ -855,6 +859,11 @@ var RunHandle = class {
|
|
|
855
859
|
sawCleanClose = true;
|
|
856
860
|
eventIdAtLastClose = lastEventId;
|
|
857
861
|
}
|
|
862
|
+
const detail = terminalDetail ?? await waitWithSignal(() => this.ops.getDetail(signal), signal);
|
|
863
|
+
this.#adoptSessionRef(detail);
|
|
864
|
+
const result = settleRunResult(this.id, detail, [], terminalDoneReason);
|
|
865
|
+
result.artifacts = this.ops.listArtifacts ? await waitWithSignal(() => this.ops.listArtifacts(signal), signal) : [];
|
|
866
|
+
return result;
|
|
858
867
|
} catch (err) {
|
|
859
868
|
if (timedOut() && !opts.signal?.aborted) {
|
|
860
869
|
throw new QoniTimeoutError(
|
|
@@ -863,17 +872,9 @@ var RunHandle = class {
|
|
|
863
872
|
);
|
|
864
873
|
}
|
|
865
874
|
throw err;
|
|
875
|
+
} finally {
|
|
876
|
+
dispose();
|
|
866
877
|
}
|
|
867
|
-
let detail;
|
|
868
|
-
try {
|
|
869
|
-
detail = await this.ops.getDetail();
|
|
870
|
-
} catch (err) {
|
|
871
|
-
if (!terminalPayload) throw err;
|
|
872
|
-
detail = terminalPayload;
|
|
873
|
-
}
|
|
874
|
-
this.#adoptSessionRef(detail);
|
|
875
|
-
const artifacts = this.ops.listArtifacts ? await this.ops.listArtifacts() : [];
|
|
876
|
-
return settleRunResult(this.id, detail, artifacts, terminalDoneReason);
|
|
877
878
|
}
|
|
878
879
|
/**
|
|
879
880
|
* Wrap an {@link Interaction} (from `event.data` of an `interaction` event)
|
|
@@ -926,15 +927,12 @@ function normalizeRunStatus(id, detail) {
|
|
|
926
927
|
function settleRunResult(runId, detail, artifacts, fallbackTerminalReason) {
|
|
927
928
|
const terminalReason = (typeof detail.terminal_reason === "string" ? detail.terminal_reason : void 0) ?? fallbackTerminalReason;
|
|
928
929
|
const isTaskSuccessful = typeof detail.is_task_successful === "boolean" ? detail.is_task_successful : void 0;
|
|
929
|
-
|
|
930
|
-
if (isTerminalRunStatus(
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
status = "failed";
|
|
936
|
-
} else {
|
|
937
|
-
status = "succeeded";
|
|
930
|
+
const status = normalizeRunStatus(runId, detail).status;
|
|
931
|
+
if (!isTerminalRunStatus(status)) {
|
|
932
|
+
throw new QoniError(
|
|
933
|
+
"run detail did not confirm a terminal state \u2014 re-attach or poll status()",
|
|
934
|
+
{ code: "upstream.failed", retryable: true, body: detail }
|
|
935
|
+
);
|
|
938
936
|
}
|
|
939
937
|
return {
|
|
940
938
|
runId,
|
|
@@ -961,19 +959,40 @@ function isVideoFrame(wire) {
|
|
|
961
959
|
return type === "browser_video_frame" || type === "run.browser_video_frame";
|
|
962
960
|
}
|
|
963
961
|
function withTimeout(signal, timeoutMs) {
|
|
964
|
-
if (!timeoutMs) return { signal, timedOut: () => false
|
|
962
|
+
if (!timeoutMs) return { signal, timedOut: () => false, dispose: () => {
|
|
963
|
+
} };
|
|
965
964
|
const controller = new AbortController();
|
|
966
965
|
let fired = false;
|
|
967
966
|
const timer = setTimeout(() => {
|
|
968
967
|
fired = true;
|
|
969
968
|
controller.abort();
|
|
970
969
|
}, timeoutMs);
|
|
971
|
-
|
|
970
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
971
|
+
const dispose = () => {
|
|
972
|
+
clearTimeout(timer);
|
|
973
|
+
signal?.removeEventListener("abort", onAbort);
|
|
974
|
+
};
|
|
975
|
+
controller.signal.addEventListener("abort", dispose, { once: true });
|
|
972
976
|
if (signal) {
|
|
973
|
-
if (signal.aborted)
|
|
974
|
-
else signal.addEventListener("abort",
|
|
977
|
+
if (signal.aborted) onAbort();
|
|
978
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
975
979
|
}
|
|
976
|
-
return { signal: controller.signal, timedOut: () => fired };
|
|
980
|
+
return { signal: controller.signal, timedOut: () => fired, dispose };
|
|
981
|
+
}
|
|
982
|
+
function waitWithSignal(operation, signal) {
|
|
983
|
+
if (!signal) return Promise.resolve().then(operation);
|
|
984
|
+
return new Promise((resolve, reject) => {
|
|
985
|
+
const onAbort = () => reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
986
|
+
if (signal.aborted) {
|
|
987
|
+
onAbort();
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
991
|
+
Promise.resolve().then(() => {
|
|
992
|
+
signal.throwIfAborted();
|
|
993
|
+
return operation();
|
|
994
|
+
}).then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
995
|
+
});
|
|
977
996
|
}
|
|
978
997
|
|
|
979
998
|
// src/deep-research.ts
|
|
@@ -988,6 +1007,7 @@ function createDeepResearchNamespace(transport) {
|
|
|
988
1007
|
`/deep_research/runs/${encodeURIComponent(input.runId)}`,
|
|
989
1008
|
input.token,
|
|
990
1009
|
{
|
|
1010
|
+
signal: input.signal,
|
|
991
1011
|
requiredScopes: ["webagent.deep_research:read"]
|
|
992
1012
|
}
|
|
993
1013
|
),
|
|
@@ -1033,6 +1053,7 @@ function createDeepResearchNamespace(transport) {
|
|
|
1033
1053
|
`/deep_research/runs/${encodeURIComponent(input.runId)}/artifacts`,
|
|
1034
1054
|
input.token,
|
|
1035
1055
|
{
|
|
1056
|
+
signal: input.signal,
|
|
1036
1057
|
requiredScopes: ["webagent.deep_research:read"]
|
|
1037
1058
|
}
|
|
1038
1059
|
),
|
|
@@ -1047,7 +1068,7 @@ function createDeepResearchNamespace(transport) {
|
|
|
1047
1068
|
};
|
|
1048
1069
|
function buildOps(token, runId) {
|
|
1049
1070
|
return {
|
|
1050
|
-
getDetail: async () => asRecord((await api.get({ token, runId })).data),
|
|
1071
|
+
getDetail: async (signal) => asRecord((await api.get({ token, runId, signal })).data),
|
|
1051
1072
|
streamEvents: (opts) => api.events({
|
|
1052
1073
|
token,
|
|
1053
1074
|
runId,
|
|
@@ -1067,8 +1088,8 @@ function createDeepResearchNamespace(transport) {
|
|
|
1067
1088
|
);
|
|
1068
1089
|
},
|
|
1069
1090
|
cancel: async () => asRecord((await api.cancel({ token, runId })).data),
|
|
1070
|
-
listArtifacts: async () => {
|
|
1071
|
-
const res = await api.listArtifacts({ token, runId });
|
|
1091
|
+
listArtifacts: async (signal) => {
|
|
1092
|
+
const res = await api.listArtifacts({ token, runId, signal });
|
|
1072
1093
|
const items = Array.isArray(res.data?.items) ? res.data.items : [];
|
|
1073
1094
|
return items.filter(isJsonObject).map((item) => toArtifact(item, token, runId));
|
|
1074
1095
|
}
|
|
@@ -1188,6 +1209,7 @@ function createDoAnythingNamespace(transport) {
|
|
|
1188
1209
|
`/do_anything/runs/${encodeURIComponent(input.runId)}`,
|
|
1189
1210
|
input.token,
|
|
1190
1211
|
{
|
|
1212
|
+
signal: input.signal,
|
|
1191
1213
|
requiredScopes: ["webagent.do_anything:read"]
|
|
1192
1214
|
}
|
|
1193
1215
|
),
|
|
@@ -1245,7 +1267,7 @@ function createDoAnythingNamespace(transport) {
|
|
|
1245
1267
|
};
|
|
1246
1268
|
function buildOps(token, runId, capture) {
|
|
1247
1269
|
return {
|
|
1248
|
-
getDetail: async () => asRecord2((await api.getRun({ token, runId })).data),
|
|
1270
|
+
getDetail: async (signal) => asRecord2((await api.getRun({ token, runId, signal })).data),
|
|
1249
1271
|
streamEvents: (opts) => api.events({
|
|
1250
1272
|
token,
|
|
1251
1273
|
runId,
|
|
@@ -1839,6 +1861,7 @@ function createWebSearchNamespace(transport) {
|
|
|
1839
1861
|
`/web_search/runs/${encodeURIComponent(input.runId)}`,
|
|
1840
1862
|
input.token,
|
|
1841
1863
|
{
|
|
1864
|
+
signal: input.signal,
|
|
1842
1865
|
requiredScopes: ["webagent.web_search:read"]
|
|
1843
1866
|
}
|
|
1844
1867
|
),
|
|
@@ -1864,7 +1887,7 @@ function createWebSearchNamespace(transport) {
|
|
|
1864
1887
|
};
|
|
1865
1888
|
function buildOps(token, runId) {
|
|
1866
1889
|
return {
|
|
1867
|
-
getDetail: async () => asRecord4((await api.get({ token, runId })).data),
|
|
1890
|
+
getDetail: async (signal) => asRecord4((await api.get({ token, runId, signal })).data),
|
|
1868
1891
|
streamEvents: (opts) => api.events({
|
|
1869
1892
|
token,
|
|
1870
1893
|
runId,
|
|
@@ -2137,7 +2160,9 @@ var Qoni = class {
|
|
|
2137
2160
|
}
|
|
2138
2161
|
}
|
|
2139
2162
|
async webAgentJson(method, path, token, payload = {}) {
|
|
2163
|
+
payload.signal?.throwIfAborted();
|
|
2140
2164
|
const runtimeToken = await this.productTokenFor("webagent", token, payload.requiredScopes);
|
|
2165
|
+
payload.signal?.throwIfAborted();
|
|
2141
2166
|
const baseUrl = await this.baseUrlFor("webagent");
|
|
2142
2167
|
return this.jsonRequest(
|
|
2143
2168
|
baseUrl,
|
|
@@ -2148,9 +2173,11 @@ var Qoni = class {
|
|
|
2148
2173
|
// Binary GET against the WebAgent API — artifact content downloads. JSON
|
|
2149
2174
|
// error payloads still map to typed QoniErrors.
|
|
2150
2175
|
async webAgentBytes(path, token, payload = {}) {
|
|
2176
|
+
payload.signal?.throwIfAborted();
|
|
2151
2177
|
const runtimeToken = await this.productTokenFor("webagent", token, payload.requiredScopes);
|
|
2178
|
+
payload.signal?.throwIfAborted();
|
|
2152
2179
|
const baseUrl = await this.baseUrlFor("webagent");
|
|
2153
|
-
|
|
2180
|
+
return this.fetchWithTimeout(
|
|
2154
2181
|
urlWithBasePath(baseUrl, this.webAgentPath(runtimeToken, path), payload.query),
|
|
2155
2182
|
{
|
|
2156
2183
|
method: "GET",
|
|
@@ -2159,20 +2186,22 @@ var Qoni = class {
|
|
|
2159
2186
|
authorization: `Bearer ${runtimeToken}`,
|
|
2160
2187
|
...payload.headers || {}
|
|
2161
2188
|
}
|
|
2162
|
-
}
|
|
2189
|
+
},
|
|
2190
|
+
async (response, signal) => {
|
|
2191
|
+
if (!response.ok) {
|
|
2192
|
+
throw errorFromPayload(
|
|
2193
|
+
response.status,
|
|
2194
|
+
await readJson(response, signal),
|
|
2195
|
+
`Qoni request failed with HTTP ${response.status}`
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
return {
|
|
2199
|
+
bytes: await readBody(response, signal),
|
|
2200
|
+
mime: response.headers.get("content-type") || "application/octet-stream"
|
|
2201
|
+
};
|
|
2202
|
+
},
|
|
2203
|
+
payload.signal
|
|
2163
2204
|
);
|
|
2164
|
-
if (!response.ok) {
|
|
2165
|
-
throw errorFromPayload(
|
|
2166
|
-
response.status,
|
|
2167
|
-
await readJson(response),
|
|
2168
|
-
`Qoni request failed with HTTP ${response.status}`
|
|
2169
|
-
);
|
|
2170
|
-
}
|
|
2171
|
-
const buffer = await response.arrayBuffer();
|
|
2172
|
-
return {
|
|
2173
|
-
bytes: new Uint8Array(buffer),
|
|
2174
|
-
mime: response.headers.get("content-type") || "application/octet-stream"
|
|
2175
|
-
};
|
|
2176
2205
|
}
|
|
2177
2206
|
async *webAgentSSE(path, token, payload = {}) {
|
|
2178
2207
|
const runtimeToken = await this.productTokenFor("webagent", token, payload.requiredScopes);
|
|
@@ -2212,15 +2241,16 @@ var Qoni = class {
|
|
|
2212
2241
|
});
|
|
2213
2242
|
}
|
|
2214
2243
|
async jsonRequest(baseUrl, pathname, options) {
|
|
2215
|
-
const response = await this.fetchWithTimeout(
|
|
2244
|
+
const { response, payload } = await this.fetchWithTimeout(
|
|
2216
2245
|
urlWithBasePath(baseUrl, pathname, options.query),
|
|
2217
2246
|
{
|
|
2218
2247
|
method: options.method,
|
|
2219
2248
|
headers: requestHeaders(options),
|
|
2220
2249
|
body: requestBody(options.body)
|
|
2221
|
-
}
|
|
2250
|
+
},
|
|
2251
|
+
async (response2, signal) => ({ response: response2, payload: await readJson(response2, signal) }),
|
|
2252
|
+
options.signal
|
|
2222
2253
|
);
|
|
2223
|
-
const payload = await readJson(response);
|
|
2224
2254
|
if (!response.ok) {
|
|
2225
2255
|
throw errorFromPayload(
|
|
2226
2256
|
response.status,
|
|
@@ -2270,45 +2300,55 @@ var Qoni = class {
|
|
|
2270
2300
|
}
|
|
2271
2301
|
}
|
|
2272
2302
|
async *sseConnectOnce(baseUrl, pathname, options) {
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
accept: "text/event-stream",
|
|
2279
|
-
authorization: `Bearer ${options.token}`,
|
|
2280
|
-
...options.headers || {},
|
|
2281
|
-
...options.lastEventId ? { "last-event-id": options.lastEventId } : {}
|
|
2282
|
-
}
|
|
2283
|
-
},
|
|
2284
|
-
options.signal,
|
|
2285
|
-
{ streamBody: true }
|
|
2286
|
-
);
|
|
2287
|
-
if (!response.ok) {
|
|
2288
|
-
throw errorFromPayload(
|
|
2289
|
-
response.status,
|
|
2290
|
-
await readJson(response),
|
|
2291
|
-
`Qoni stream failed with HTTP ${response.status}`
|
|
2292
|
-
);
|
|
2293
|
-
}
|
|
2294
|
-
if (!response.body) {
|
|
2295
|
-
throw errorFromPayload(502, void 0, "Qoni stream response has no body");
|
|
2296
|
-
}
|
|
2297
|
-
const reader = response.body.getReader();
|
|
2303
|
+
let cleanup;
|
|
2304
|
+
let reader;
|
|
2305
|
+
const onAbort = () => {
|
|
2306
|
+
void reader?.cancel(options.signal?.reason).catch(() => void 0);
|
|
2307
|
+
};
|
|
2298
2308
|
const decoder = new TextDecoder();
|
|
2299
2309
|
let buffer = "";
|
|
2300
2310
|
try {
|
|
2311
|
+
const response = await this.fetchWithTimeout(
|
|
2312
|
+
urlWithBasePath(baseUrl, pathname, options.query),
|
|
2313
|
+
{
|
|
2314
|
+
method: "GET",
|
|
2315
|
+
headers: {
|
|
2316
|
+
accept: "text/event-stream",
|
|
2317
|
+
authorization: `Bearer ${options.token}`,
|
|
2318
|
+
...options.headers || {},
|
|
2319
|
+
...options.lastEventId ? { "last-event-id": options.lastEventId } : {}
|
|
2320
|
+
}
|
|
2321
|
+
},
|
|
2322
|
+
async (response2, signal) => {
|
|
2323
|
+
if (!response2.ok) {
|
|
2324
|
+
throw errorFromPayload(
|
|
2325
|
+
response2.status,
|
|
2326
|
+
await readJson(response2, signal),
|
|
2327
|
+
`Qoni stream failed with HTTP ${response2.status}`
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
2330
|
+
return response2;
|
|
2331
|
+
},
|
|
2332
|
+
options.signal,
|
|
2333
|
+
{ streamCleanup: (dispose) => {
|
|
2334
|
+
cleanup = dispose;
|
|
2335
|
+
} }
|
|
2336
|
+
);
|
|
2337
|
+
if (!response.body) {
|
|
2338
|
+
throw errorFromPayload(502, void 0, "Qoni stream response has no body");
|
|
2339
|
+
}
|
|
2340
|
+
reader = response.body.getReader();
|
|
2341
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2301
2342
|
while (true) {
|
|
2302
|
-
|
|
2303
|
-
await reader.cancel().catch(() => void 0);
|
|
2304
|
-
throw options.signal.reason || new DOMException("Aborted", "AbortError");
|
|
2305
|
-
}
|
|
2343
|
+
options.signal?.throwIfAborted();
|
|
2306
2344
|
const { done, value } = await reader.read();
|
|
2345
|
+
options.signal?.throwIfAborted();
|
|
2307
2346
|
if (done) break;
|
|
2308
2347
|
buffer += decoder.decode(value, { stream: true });
|
|
2309
2348
|
const chunks = buffer.split(/\r?\n\r?\n/);
|
|
2310
2349
|
buffer = chunks.pop() || "";
|
|
2311
2350
|
for (const chunk of chunks) {
|
|
2351
|
+
options.signal?.throwIfAborted();
|
|
2312
2352
|
const event = parseSSEEvent(chunk);
|
|
2313
2353
|
if (event) yield event;
|
|
2314
2354
|
}
|
|
@@ -2316,14 +2356,16 @@ var Qoni = class {
|
|
|
2316
2356
|
const finalEvent = parseSSEEvent(buffer);
|
|
2317
2357
|
if (finalEvent) yield finalEvent;
|
|
2318
2358
|
} finally {
|
|
2319
|
-
|
|
2359
|
+
cleanup?.();
|
|
2360
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
2361
|
+
void reader?.cancel().catch(() => void 0);
|
|
2320
2362
|
try {
|
|
2321
|
-
reader
|
|
2363
|
+
reader?.releaseLock();
|
|
2322
2364
|
} catch {
|
|
2323
2365
|
}
|
|
2324
2366
|
}
|
|
2325
2367
|
}
|
|
2326
|
-
async fetchWithTimeout(input, init, signal, opts) {
|
|
2368
|
+
async fetchWithTimeout(input, init, consume, signal, opts) {
|
|
2327
2369
|
if (signal?.aborted) throw signal.reason || new DOMException("Aborted", "AbortError");
|
|
2328
2370
|
const controller = new AbortController();
|
|
2329
2371
|
const resolved = resolveLocalGenauthRequest(input);
|
|
@@ -2338,17 +2380,39 @@ var Qoni = class {
|
|
|
2338
2380
|
didTimeout = true;
|
|
2339
2381
|
controller.abort();
|
|
2340
2382
|
}, this.timeoutMs);
|
|
2383
|
+
let response;
|
|
2341
2384
|
try {
|
|
2342
|
-
|
|
2385
|
+
response = await new Promise((resolve, reject) => {
|
|
2386
|
+
const onAbort = () => reject(controller.signal.reason);
|
|
2387
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
2388
|
+
Promise.resolve().then(() => {
|
|
2389
|
+
controller.signal.throwIfAborted();
|
|
2390
|
+
return this.fetchImpl(resolved.url, { ...requestInit, signal: controller.signal });
|
|
2391
|
+
}).then((response2) => {
|
|
2392
|
+
if (controller.signal.aborted) {
|
|
2393
|
+
void response2.body?.cancel(controller.signal.reason).catch(() => void 0);
|
|
2394
|
+
reject(controller.signal.reason);
|
|
2395
|
+
} else {
|
|
2396
|
+
resolve(response2);
|
|
2397
|
+
}
|
|
2398
|
+
}, reject).finally(() => controller.signal.removeEventListener("abort", onAbort));
|
|
2399
|
+
});
|
|
2400
|
+
const result = await consume(response, controller.signal);
|
|
2401
|
+
controller.signal.throwIfAborted();
|
|
2402
|
+
return result;
|
|
2343
2403
|
} catch (error) {
|
|
2344
|
-
|
|
2404
|
+
void response?.body?.cancel().catch(() => void 0);
|
|
2405
|
+
if (signal?.aborted) throw signal.reason;
|
|
2406
|
+
if (didTimeout || isConnectTimeoutError(error)) {
|
|
2345
2407
|
throw timeoutError(error);
|
|
2346
2408
|
}
|
|
2347
2409
|
if (isFetchNetworkError(error)) throw networkError(error);
|
|
2348
2410
|
throw error;
|
|
2349
2411
|
} finally {
|
|
2350
2412
|
clearTimeout(timeout);
|
|
2351
|
-
|
|
2413
|
+
const cleanup = () => signal?.removeEventListener("abort", abortFromSignal);
|
|
2414
|
+
if (opts?.streamCleanup) opts.streamCleanup(cleanup);
|
|
2415
|
+
else cleanup();
|
|
2352
2416
|
}
|
|
2353
2417
|
}
|
|
2354
2418
|
async baseUrlFor(service) {
|
|
@@ -2404,7 +2468,7 @@ var Qoni = class {
|
|
|
2404
2468
|
pathname,
|
|
2405
2469
|
{}
|
|
2406
2470
|
);
|
|
2407
|
-
const response = await this.fetchWithTimeout(
|
|
2471
|
+
const { response, payload } = await this.fetchWithTimeout(
|
|
2408
2472
|
urlWithBasePath(this.host, pathname),
|
|
2409
2473
|
{
|
|
2410
2474
|
method: "GET",
|
|
@@ -2412,9 +2476,9 @@ var Qoni = class {
|
|
|
2412
2476
|
accept: "application/json",
|
|
2413
2477
|
...headers
|
|
2414
2478
|
}
|
|
2415
|
-
}
|
|
2479
|
+
},
|
|
2480
|
+
async (response2, signal) => ({ response: response2, payload: await readJson(response2, signal) })
|
|
2416
2481
|
);
|
|
2417
|
-
const payload = await readJson(response);
|
|
2418
2482
|
if (!response.ok) {
|
|
2419
2483
|
throw errorFromPayload(
|
|
2420
2484
|
response.status,
|
|
@@ -2508,18 +2572,52 @@ function requestBody(body) {
|
|
|
2508
2572
|
function isBodyInit(value) {
|
|
2509
2573
|
return typeof value === "string" || value instanceof ArrayBuffer || value instanceof Blob || value instanceof FormData || value instanceof URLSearchParams;
|
|
2510
2574
|
}
|
|
2511
|
-
async function
|
|
2575
|
+
async function readBody(response, signal) {
|
|
2576
|
+
if (!response.body) {
|
|
2577
|
+
signal.throwIfAborted();
|
|
2578
|
+
return new Uint8Array();
|
|
2579
|
+
}
|
|
2580
|
+
const reader = response.body.getReader();
|
|
2581
|
+
const onAbort = () => {
|
|
2582
|
+
void reader.cancel(signal.reason).catch(() => void 0);
|
|
2583
|
+
};
|
|
2584
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2585
|
+
try {
|
|
2586
|
+
const chunks = [];
|
|
2587
|
+
let size = 0;
|
|
2588
|
+
for (; ; ) {
|
|
2589
|
+
signal.throwIfAborted();
|
|
2590
|
+
const { done, value } = await reader.read();
|
|
2591
|
+
signal.throwIfAborted();
|
|
2592
|
+
if (done) break;
|
|
2593
|
+
chunks.push(value);
|
|
2594
|
+
size += value.byteLength;
|
|
2595
|
+
}
|
|
2596
|
+
const bytes = new Uint8Array(size);
|
|
2597
|
+
let offset = 0;
|
|
2598
|
+
for (const chunk of chunks) {
|
|
2599
|
+
bytes.set(chunk, offset);
|
|
2600
|
+
offset += chunk.byteLength;
|
|
2601
|
+
}
|
|
2602
|
+
return bytes;
|
|
2603
|
+
} finally {
|
|
2604
|
+
signal.removeEventListener("abort", onAbort);
|
|
2605
|
+
void reader.cancel().catch(() => void 0);
|
|
2606
|
+
reader.releaseLock();
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
async function readJson(response, signal) {
|
|
2512
2610
|
const contentType = response.headers.get("content-type")?.toLowerCase() || "";
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
if (
|
|
2516
|
-
|
|
2611
|
+
const body = new TextDecoder().decode(await readBody(response, signal));
|
|
2612
|
+
try {
|
|
2613
|
+
if (contentType.includes("ndjson") || contentType.includes("jsonl")) {
|
|
2614
|
+
if (!body.trim()) return [];
|
|
2517
2615
|
return body.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
2518
|
-
} catch {
|
|
2519
|
-
return void 0;
|
|
2520
2616
|
}
|
|
2617
|
+
return JSON.parse(body);
|
|
2618
|
+
} catch {
|
|
2619
|
+
return void 0;
|
|
2521
2620
|
}
|
|
2522
|
-
return response.json().catch(() => void 0);
|
|
2523
2621
|
}
|
|
2524
2622
|
function normalizeResponse(payload, _service) {
|
|
2525
2623
|
if (isJsonObject(payload) && ("data" in payload || "meta" in payload)) {
|
|
@@ -2762,6 +2860,7 @@ function isRetryableStreamError(err) {
|
|
|
2762
2860
|
if (err && typeof err === "object") {
|
|
2763
2861
|
const e = err;
|
|
2764
2862
|
if (e.name === "AbortError") return false;
|
|
2863
|
+
if (e.status === 408) return true;
|
|
2765
2864
|
if (typeof e.retryable === "boolean") return e.retryable;
|
|
2766
2865
|
if (typeof e.status === "number") {
|
|
2767
2866
|
return e.status >= 500 || e.status === 408 || e.status === 429;
|
|
@@ -2868,9 +2967,6 @@ function normalizeDelegateAgentTokenResponse(response) {
|
|
|
2868
2967
|
}
|
|
2869
2968
|
};
|
|
2870
2969
|
}
|
|
2871
|
-
function isAbortError(error) {
|
|
2872
|
-
return error instanceof DOMException && error.name === "AbortError";
|
|
2873
|
-
}
|
|
2874
2970
|
function isConnectTimeoutError(error) {
|
|
2875
2971
|
const cause = errorCause(error);
|
|
2876
2972
|
return errorCode(cause) === "UND_ERR_CONNECT_TIMEOUT" || errorName(cause) === "ConnectTimeoutError";
|