@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/dist/index.d.cts CHANGED
@@ -224,6 +224,7 @@ interface RawRequestInput extends TokenInput {
224
224
  headers?: Record<string, string>;
225
225
  }
226
226
  interface RequestPayload {
227
+ signal?: AbortSignal;
227
228
  query?: JsonObject;
228
229
  body?: JsonObject | BodyInit;
229
230
  headers?: Record<string, string>;
@@ -638,7 +639,7 @@ interface RunWaitOptions {
638
639
  * with the token + run addressing already bound.
639
640
  */
640
641
  interface RunWireOps {
641
- getDetail(): Promise<JsonObject>;
642
+ getDetail(signal?: AbortSignal): Promise<JsonObject>;
642
643
  streamEvents(opts: RunEventsOptions): AsyncIterable<QoniEvent<unknown>>;
643
644
  /**
644
645
  * Invoke an interaction action by posting to its declared endpoint. The
@@ -648,7 +649,7 @@ interface RunWireOps {
648
649
  postAction(action: Action, body: JsonObject | undefined): Promise<void>;
649
650
  cancel(reason?: string): Promise<JsonObject>;
650
651
  /** Products without artifacts leave this undefined → `result.artifacts` is []. */
651
- listArtifacts?(): Promise<Artifact[]>;
652
+ listArtifacts?(signal?: AbortSignal): Promise<Artifact[]>;
652
653
  }
653
654
  /**
654
655
  * Handle to a single run. Generic over the product's event set (分型): a
@@ -782,6 +783,7 @@ declare function createDoAnythingNamespace(transport: QoniTransport): {
782
783
  createRun: <T = unknown>(input: RuntimeTokenInput & JsonObject) => Promise<QoniResponse<T>>;
783
784
  getRun: <T = unknown>(input: RuntimeTokenInput & {
784
785
  runId: string;
786
+ signal?: AbortSignal;
785
787
  }) => Promise<QoniResponse<T>>;
786
788
  events: <T = unknown>(input: RuntimeTokenInput & {
787
789
  runId: string;
@@ -862,6 +864,7 @@ declare function createDeepResearchNamespace(transport: QoniTransport): {
862
864
  run: <T = unknown>(input: RuntimeTokenInput & JsonObject) => Promise<QoniResponse<T>>;
863
865
  get: <T = unknown>(input: RuntimeTokenInput & {
864
866
  runId: string;
867
+ signal?: AbortSignal;
865
868
  }) => Promise<QoniResponse<T>>;
866
869
  events: <T = unknown>(input: RuntimeTokenInput & {
867
870
  runId: string;
@@ -885,6 +888,7 @@ declare function createDeepResearchNamespace(transport: QoniTransport): {
885
888
  } & JsonObject) => Promise<QoniResponse<T>>;
886
889
  listArtifacts: <T = unknown>(input: RuntimeTokenInput & {
887
890
  runId: string;
891
+ signal?: AbortSignal;
888
892
  }) => Promise<QoniResponse<T>>;
889
893
  getArtifact: <T = unknown>(input: RuntimeTokenInput & {
890
894
  runId: string;
@@ -1190,6 +1194,7 @@ declare function createWebSearchNamespace(transport: QoniTransport): {
1190
1194
  run: <T = unknown>(input: RuntimeTokenInput & JsonObject) => Promise<QoniResponse<T>>;
1191
1195
  get: <T = unknown>(input: RuntimeTokenInput & {
1192
1196
  runId: string;
1197
+ signal?: AbortSignal;
1193
1198
  }) => Promise<QoniResponse<T>>;
1194
1199
  events: <T = unknown>(input: RuntimeTokenInput & {
1195
1200
  runId: string;
package/dist/index.d.ts CHANGED
@@ -224,6 +224,7 @@ interface RawRequestInput extends TokenInput {
224
224
  headers?: Record<string, string>;
225
225
  }
226
226
  interface RequestPayload {
227
+ signal?: AbortSignal;
227
228
  query?: JsonObject;
228
229
  body?: JsonObject | BodyInit;
229
230
  headers?: Record<string, string>;
@@ -638,7 +639,7 @@ interface RunWaitOptions {
638
639
  * with the token + run addressing already bound.
639
640
  */
640
641
  interface RunWireOps {
641
- getDetail(): Promise<JsonObject>;
642
+ getDetail(signal?: AbortSignal): Promise<JsonObject>;
642
643
  streamEvents(opts: RunEventsOptions): AsyncIterable<QoniEvent<unknown>>;
643
644
  /**
644
645
  * Invoke an interaction action by posting to its declared endpoint. The
@@ -648,7 +649,7 @@ interface RunWireOps {
648
649
  postAction(action: Action, body: JsonObject | undefined): Promise<void>;
649
650
  cancel(reason?: string): Promise<JsonObject>;
650
651
  /** Products without artifacts leave this undefined → `result.artifacts` is []. */
651
- listArtifacts?(): Promise<Artifact[]>;
652
+ listArtifacts?(signal?: AbortSignal): Promise<Artifact[]>;
652
653
  }
653
654
  /**
654
655
  * Handle to a single run. Generic over the product's event set (分型): a
@@ -782,6 +783,7 @@ declare function createDoAnythingNamespace(transport: QoniTransport): {
782
783
  createRun: <T = unknown>(input: RuntimeTokenInput & JsonObject) => Promise<QoniResponse<T>>;
783
784
  getRun: <T = unknown>(input: RuntimeTokenInput & {
784
785
  runId: string;
786
+ signal?: AbortSignal;
785
787
  }) => Promise<QoniResponse<T>>;
786
788
  events: <T = unknown>(input: RuntimeTokenInput & {
787
789
  runId: string;
@@ -862,6 +864,7 @@ declare function createDeepResearchNamespace(transport: QoniTransport): {
862
864
  run: <T = unknown>(input: RuntimeTokenInput & JsonObject) => Promise<QoniResponse<T>>;
863
865
  get: <T = unknown>(input: RuntimeTokenInput & {
864
866
  runId: string;
867
+ signal?: AbortSignal;
865
868
  }) => Promise<QoniResponse<T>>;
866
869
  events: <T = unknown>(input: RuntimeTokenInput & {
867
870
  runId: string;
@@ -885,6 +888,7 @@ declare function createDeepResearchNamespace(transport: QoniTransport): {
885
888
  } & JsonObject) => Promise<QoniResponse<T>>;
886
889
  listArtifacts: <T = unknown>(input: RuntimeTokenInput & {
887
890
  runId: string;
891
+ signal?: AbortSignal;
888
892
  }) => Promise<QoniResponse<T>>;
889
893
  getArtifact: <T = unknown>(input: RuntimeTokenInput & {
890
894
  runId: string;
@@ -1190,6 +1194,7 @@ declare function createWebSearchNamespace(transport: QoniTransport): {
1190
1194
  run: <T = unknown>(input: RuntimeTokenInput & JsonObject) => Promise<QoniResponse<T>>;
1191
1195
  get: <T = unknown>(input: RuntimeTokenInput & {
1192
1196
  runId: string;
1197
+ signal?: AbortSignal;
1193
1198
  }) => Promise<QoniResponse<T>>;
1194
1199
  events: <T = unknown>(input: RuntimeTokenInput & {
1195
1200
  runId: string;
package/dist/index.js CHANGED
@@ -768,11 +768,11 @@ var RunHandle = class {
768
768
  }
769
769
  /** Drive the run to a terminal state and return the settled result. */
770
770
  async wait(opts = {}) {
771
- const { signal, timedOut } = withTimeout(opts.signal, opts.timeoutMs);
771
+ const { signal, timedOut, dispose } = withTimeout(opts.signal, opts.timeoutMs);
772
772
  let lastEventId;
773
773
  let eventIdAtLastClose;
774
774
  let sawCleanClose = false;
775
- let terminalPayload;
775
+ let terminalDetail;
776
776
  let terminalDoneReason;
777
777
  let screenshotIndex = 0;
778
778
  try {
@@ -780,24 +780,28 @@ var RunHandle = class {
780
780
  for await (const event of this.events({
781
781
  signal,
782
782
  lastEventId,
783
- onWireEvent: opts.onWireEvent,
783
+ onWireEvent: (wire) => waitWithSignal(() => {
784
+ if (wire.id !== void 0) lastEventId = wire.id;
785
+ return opts.onWireEvent?.(wire);
786
+ }, signal),
784
787
  onReconnect: opts.onReconnect
785
788
  })) {
786
- if (event.raw.id) lastEventId = event.raw.id;
787
- await opts.onEvent?.(event);
789
+ await waitWithSignal(() => opts.onEvent?.(event), signal);
788
790
  if (event.type === "screenshot") {
789
- await opts.onScreenshot?.(event.image, screenshotIndex++);
791
+ await waitWithSignal(() => opts.onScreenshot?.(event.image, screenshotIndex++), signal);
790
792
  } else if (event.type === "interaction") {
791
- await opts.onInteraction?.(this.interactionHandle(event.data), event);
793
+ await waitWithSignal(() => opts.onInteraction?.(this.interactionHandle(event.data), event), signal);
792
794
  }
793
795
  if (event.isTerminal) {
794
- terminalPayload = isJsonObject(event.raw.data) ? event.raw.data : void 0;
795
796
  if (event.type === "done") terminalDoneReason = event.data.terminalReason || void 0;
796
797
  break settled;
797
798
  }
798
799
  }
799
- const detail2 = await this.ops.getDetail();
800
- if (isTerminalRunStatus(detail2.status)) break;
800
+ const detail2 = await waitWithSignal(() => this.ops.getDetail(signal), signal);
801
+ if (isTerminalRunStatus(normalizeRunStatus(this.id, detail2).status)) {
802
+ terminalDetail = detail2;
803
+ break;
804
+ }
801
805
  if (sawCleanClose && lastEventId === eventIdAtLastClose) {
802
806
  throw new QoniError(
803
807
  `run event stream ended before a terminal event while the run is still "${String(detail2.status)}" \u2014 re-attach or poll status()`,
@@ -807,6 +811,11 @@ var RunHandle = class {
807
811
  sawCleanClose = true;
808
812
  eventIdAtLastClose = lastEventId;
809
813
  }
814
+ const detail = terminalDetail ?? await waitWithSignal(() => this.ops.getDetail(signal), signal);
815
+ this.#adoptSessionRef(detail);
816
+ const result = settleRunResult(this.id, detail, [], terminalDoneReason);
817
+ result.artifacts = this.ops.listArtifacts ? await waitWithSignal(() => this.ops.listArtifacts(signal), signal) : [];
818
+ return result;
810
819
  } catch (err) {
811
820
  if (timedOut() && !opts.signal?.aborted) {
812
821
  throw new QoniTimeoutError(
@@ -815,17 +824,9 @@ var RunHandle = class {
815
824
  );
816
825
  }
817
826
  throw err;
827
+ } finally {
828
+ dispose();
818
829
  }
819
- let detail;
820
- try {
821
- detail = await this.ops.getDetail();
822
- } catch (err) {
823
- if (!terminalPayload) throw err;
824
- detail = terminalPayload;
825
- }
826
- this.#adoptSessionRef(detail);
827
- const artifacts = this.ops.listArtifacts ? await this.ops.listArtifacts() : [];
828
- return settleRunResult(this.id, detail, artifacts, terminalDoneReason);
829
830
  }
830
831
  /**
831
832
  * Wrap an {@link Interaction} (from `event.data` of an `interaction` event)
@@ -878,15 +879,12 @@ function normalizeRunStatus(id, detail) {
878
879
  function settleRunResult(runId, detail, artifacts, fallbackTerminalReason) {
879
880
  const terminalReason = (typeof detail.terminal_reason === "string" ? detail.terminal_reason : void 0) ?? fallbackTerminalReason;
880
881
  const isTaskSuccessful = typeof detail.is_task_successful === "boolean" ? detail.is_task_successful : void 0;
881
- let status;
882
- if (isTerminalRunStatus(detail.status)) {
883
- status = detail.status;
884
- } else if (terminalReason === "canceled") {
885
- status = "canceled";
886
- } else if (terminalReason === "failed" || terminalReason === "expired" || isTaskSuccessful === false) {
887
- status = "failed";
888
- } else {
889
- status = "succeeded";
882
+ const status = normalizeRunStatus(runId, detail).status;
883
+ if (!isTerminalRunStatus(status)) {
884
+ throw new QoniError(
885
+ "run detail did not confirm a terminal state \u2014 re-attach or poll status()",
886
+ { code: "upstream.failed", retryable: true, body: detail }
887
+ );
890
888
  }
891
889
  return {
892
890
  runId,
@@ -913,19 +911,40 @@ function isVideoFrame(wire) {
913
911
  return type === "browser_video_frame" || type === "run.browser_video_frame";
914
912
  }
915
913
  function withTimeout(signal, timeoutMs) {
916
- if (!timeoutMs) return { signal, timedOut: () => false };
914
+ if (!timeoutMs) return { signal, timedOut: () => false, dispose: () => {
915
+ } };
917
916
  const controller = new AbortController();
918
917
  let fired = false;
919
918
  const timer = setTimeout(() => {
920
919
  fired = true;
921
920
  controller.abort();
922
921
  }, timeoutMs);
923
- controller.signal.addEventListener("abort", () => clearTimeout(timer), { once: true });
922
+ const onAbort = () => controller.abort(signal?.reason);
923
+ const dispose = () => {
924
+ clearTimeout(timer);
925
+ signal?.removeEventListener("abort", onAbort);
926
+ };
927
+ controller.signal.addEventListener("abort", dispose, { once: true });
924
928
  if (signal) {
925
- if (signal.aborted) controller.abort();
926
- else signal.addEventListener("abort", () => controller.abort(), { once: true });
929
+ if (signal.aborted) onAbort();
930
+ else signal.addEventListener("abort", onAbort, { once: true });
927
931
  }
928
- return { signal: controller.signal, timedOut: () => fired };
932
+ return { signal: controller.signal, timedOut: () => fired, dispose };
933
+ }
934
+ function waitWithSignal(operation, signal) {
935
+ if (!signal) return Promise.resolve().then(operation);
936
+ return new Promise((resolve, reject) => {
937
+ const onAbort = () => reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
938
+ if (signal.aborted) {
939
+ onAbort();
940
+ return;
941
+ }
942
+ signal.addEventListener("abort", onAbort, { once: true });
943
+ Promise.resolve().then(() => {
944
+ signal.throwIfAborted();
945
+ return operation();
946
+ }).then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
947
+ });
929
948
  }
930
949
 
931
950
  // src/deep-research.ts
@@ -940,6 +959,7 @@ function createDeepResearchNamespace(transport) {
940
959
  `/deep_research/runs/${encodeURIComponent(input.runId)}`,
941
960
  input.token,
942
961
  {
962
+ signal: input.signal,
943
963
  requiredScopes: ["webagent.deep_research:read"]
944
964
  }
945
965
  ),
@@ -985,6 +1005,7 @@ function createDeepResearchNamespace(transport) {
985
1005
  `/deep_research/runs/${encodeURIComponent(input.runId)}/artifacts`,
986
1006
  input.token,
987
1007
  {
1008
+ signal: input.signal,
988
1009
  requiredScopes: ["webagent.deep_research:read"]
989
1010
  }
990
1011
  ),
@@ -999,7 +1020,7 @@ function createDeepResearchNamespace(transport) {
999
1020
  };
1000
1021
  function buildOps(token, runId) {
1001
1022
  return {
1002
- getDetail: async () => asRecord((await api.get({ token, runId })).data),
1023
+ getDetail: async (signal) => asRecord((await api.get({ token, runId, signal })).data),
1003
1024
  streamEvents: (opts) => api.events({
1004
1025
  token,
1005
1026
  runId,
@@ -1019,8 +1040,8 @@ function createDeepResearchNamespace(transport) {
1019
1040
  );
1020
1041
  },
1021
1042
  cancel: async () => asRecord((await api.cancel({ token, runId })).data),
1022
- listArtifacts: async () => {
1023
- const res = await api.listArtifacts({ token, runId });
1043
+ listArtifacts: async (signal) => {
1044
+ const res = await api.listArtifacts({ token, runId, signal });
1024
1045
  const items = Array.isArray(res.data?.items) ? res.data.items : [];
1025
1046
  return items.filter(isJsonObject).map((item) => toArtifact(item, token, runId));
1026
1047
  }
@@ -1140,6 +1161,7 @@ function createDoAnythingNamespace(transport) {
1140
1161
  `/do_anything/runs/${encodeURIComponent(input.runId)}`,
1141
1162
  input.token,
1142
1163
  {
1164
+ signal: input.signal,
1143
1165
  requiredScopes: ["webagent.do_anything:read"]
1144
1166
  }
1145
1167
  ),
@@ -1197,7 +1219,7 @@ function createDoAnythingNamespace(transport) {
1197
1219
  };
1198
1220
  function buildOps(token, runId, capture) {
1199
1221
  return {
1200
- getDetail: async () => asRecord2((await api.getRun({ token, runId })).data),
1222
+ getDetail: async (signal) => asRecord2((await api.getRun({ token, runId, signal })).data),
1201
1223
  streamEvents: (opts) => api.events({
1202
1224
  token,
1203
1225
  runId,
@@ -1791,6 +1813,7 @@ function createWebSearchNamespace(transport) {
1791
1813
  `/web_search/runs/${encodeURIComponent(input.runId)}`,
1792
1814
  input.token,
1793
1815
  {
1816
+ signal: input.signal,
1794
1817
  requiredScopes: ["webagent.web_search:read"]
1795
1818
  }
1796
1819
  ),
@@ -1816,7 +1839,7 @@ function createWebSearchNamespace(transport) {
1816
1839
  };
1817
1840
  function buildOps(token, runId) {
1818
1841
  return {
1819
- getDetail: async () => asRecord4((await api.get({ token, runId })).data),
1842
+ getDetail: async (signal) => asRecord4((await api.get({ token, runId, signal })).data),
1820
1843
  streamEvents: (opts) => api.events({
1821
1844
  token,
1822
1845
  runId,
@@ -2089,7 +2112,9 @@ var Qoni = class {
2089
2112
  }
2090
2113
  }
2091
2114
  async webAgentJson(method, path, token, payload = {}) {
2115
+ payload.signal?.throwIfAborted();
2092
2116
  const runtimeToken = await this.productTokenFor("webagent", token, payload.requiredScopes);
2117
+ payload.signal?.throwIfAborted();
2093
2118
  const baseUrl = await this.baseUrlFor("webagent");
2094
2119
  return this.jsonRequest(
2095
2120
  baseUrl,
@@ -2100,9 +2125,11 @@ var Qoni = class {
2100
2125
  // Binary GET against the WebAgent API — artifact content downloads. JSON
2101
2126
  // error payloads still map to typed QoniErrors.
2102
2127
  async webAgentBytes(path, token, payload = {}) {
2128
+ payload.signal?.throwIfAborted();
2103
2129
  const runtimeToken = await this.productTokenFor("webagent", token, payload.requiredScopes);
2130
+ payload.signal?.throwIfAborted();
2104
2131
  const baseUrl = await this.baseUrlFor("webagent");
2105
- const response = await this.fetchWithTimeout(
2132
+ return this.fetchWithTimeout(
2106
2133
  urlWithBasePath(baseUrl, this.webAgentPath(runtimeToken, path), payload.query),
2107
2134
  {
2108
2135
  method: "GET",
@@ -2111,20 +2138,22 @@ var Qoni = class {
2111
2138
  authorization: `Bearer ${runtimeToken}`,
2112
2139
  ...payload.headers || {}
2113
2140
  }
2114
- }
2141
+ },
2142
+ async (response, signal) => {
2143
+ if (!response.ok) {
2144
+ throw errorFromPayload(
2145
+ response.status,
2146
+ await readJson(response, signal),
2147
+ `Qoni request failed with HTTP ${response.status}`
2148
+ );
2149
+ }
2150
+ return {
2151
+ bytes: await readBody(response, signal),
2152
+ mime: response.headers.get("content-type") || "application/octet-stream"
2153
+ };
2154
+ },
2155
+ payload.signal
2115
2156
  );
2116
- if (!response.ok) {
2117
- throw errorFromPayload(
2118
- response.status,
2119
- await readJson(response),
2120
- `Qoni request failed with HTTP ${response.status}`
2121
- );
2122
- }
2123
- const buffer = await response.arrayBuffer();
2124
- return {
2125
- bytes: new Uint8Array(buffer),
2126
- mime: response.headers.get("content-type") || "application/octet-stream"
2127
- };
2128
2157
  }
2129
2158
  async *webAgentSSE(path, token, payload = {}) {
2130
2159
  const runtimeToken = await this.productTokenFor("webagent", token, payload.requiredScopes);
@@ -2164,15 +2193,16 @@ var Qoni = class {
2164
2193
  });
2165
2194
  }
2166
2195
  async jsonRequest(baseUrl, pathname, options) {
2167
- const response = await this.fetchWithTimeout(
2196
+ const { response, payload } = await this.fetchWithTimeout(
2168
2197
  urlWithBasePath(baseUrl, pathname, options.query),
2169
2198
  {
2170
2199
  method: options.method,
2171
2200
  headers: requestHeaders(options),
2172
2201
  body: requestBody(options.body)
2173
- }
2202
+ },
2203
+ async (response2, signal) => ({ response: response2, payload: await readJson(response2, signal) }),
2204
+ options.signal
2174
2205
  );
2175
- const payload = await readJson(response);
2176
2206
  if (!response.ok) {
2177
2207
  throw errorFromPayload(
2178
2208
  response.status,
@@ -2222,45 +2252,55 @@ var Qoni = class {
2222
2252
  }
2223
2253
  }
2224
2254
  async *sseConnectOnce(baseUrl, pathname, options) {
2225
- const response = await this.fetchWithTimeout(
2226
- urlWithBasePath(baseUrl, pathname, options.query),
2227
- {
2228
- method: "GET",
2229
- headers: {
2230
- accept: "text/event-stream",
2231
- authorization: `Bearer ${options.token}`,
2232
- ...options.headers || {},
2233
- ...options.lastEventId ? { "last-event-id": options.lastEventId } : {}
2234
- }
2235
- },
2236
- options.signal,
2237
- { streamBody: true }
2238
- );
2239
- if (!response.ok) {
2240
- throw errorFromPayload(
2241
- response.status,
2242
- await readJson(response),
2243
- `Qoni stream failed with HTTP ${response.status}`
2244
- );
2245
- }
2246
- if (!response.body) {
2247
- throw errorFromPayload(502, void 0, "Qoni stream response has no body");
2248
- }
2249
- const reader = response.body.getReader();
2255
+ let cleanup;
2256
+ let reader;
2257
+ const onAbort = () => {
2258
+ void reader?.cancel(options.signal?.reason).catch(() => void 0);
2259
+ };
2250
2260
  const decoder = new TextDecoder();
2251
2261
  let buffer = "";
2252
2262
  try {
2263
+ const response = await this.fetchWithTimeout(
2264
+ urlWithBasePath(baseUrl, pathname, options.query),
2265
+ {
2266
+ method: "GET",
2267
+ headers: {
2268
+ accept: "text/event-stream",
2269
+ authorization: `Bearer ${options.token}`,
2270
+ ...options.headers || {},
2271
+ ...options.lastEventId ? { "last-event-id": options.lastEventId } : {}
2272
+ }
2273
+ },
2274
+ async (response2, signal) => {
2275
+ if (!response2.ok) {
2276
+ throw errorFromPayload(
2277
+ response2.status,
2278
+ await readJson(response2, signal),
2279
+ `Qoni stream failed with HTTP ${response2.status}`
2280
+ );
2281
+ }
2282
+ return response2;
2283
+ },
2284
+ options.signal,
2285
+ { streamCleanup: (dispose) => {
2286
+ cleanup = dispose;
2287
+ } }
2288
+ );
2289
+ if (!response.body) {
2290
+ throw errorFromPayload(502, void 0, "Qoni stream response has no body");
2291
+ }
2292
+ reader = response.body.getReader();
2293
+ options.signal?.addEventListener("abort", onAbort, { once: true });
2253
2294
  while (true) {
2254
- if (options.signal?.aborted) {
2255
- await reader.cancel().catch(() => void 0);
2256
- throw options.signal.reason || new DOMException("Aborted", "AbortError");
2257
- }
2295
+ options.signal?.throwIfAborted();
2258
2296
  const { done, value } = await reader.read();
2297
+ options.signal?.throwIfAborted();
2259
2298
  if (done) break;
2260
2299
  buffer += decoder.decode(value, { stream: true });
2261
2300
  const chunks = buffer.split(/\r?\n\r?\n/);
2262
2301
  buffer = chunks.pop() || "";
2263
2302
  for (const chunk of chunks) {
2303
+ options.signal?.throwIfAborted();
2264
2304
  const event = parseSSEEvent(chunk);
2265
2305
  if (event) yield event;
2266
2306
  }
@@ -2268,14 +2308,16 @@ var Qoni = class {
2268
2308
  const finalEvent = parseSSEEvent(buffer);
2269
2309
  if (finalEvent) yield finalEvent;
2270
2310
  } finally {
2271
- await reader.cancel().catch(() => void 0);
2311
+ cleanup?.();
2312
+ options.signal?.removeEventListener("abort", onAbort);
2313
+ void reader?.cancel().catch(() => void 0);
2272
2314
  try {
2273
- reader.releaseLock();
2315
+ reader?.releaseLock();
2274
2316
  } catch {
2275
2317
  }
2276
2318
  }
2277
2319
  }
2278
- async fetchWithTimeout(input, init, signal, opts) {
2320
+ async fetchWithTimeout(input, init, consume, signal, opts) {
2279
2321
  if (signal?.aborted) throw signal.reason || new DOMException("Aborted", "AbortError");
2280
2322
  const controller = new AbortController();
2281
2323
  const resolved = resolveLocalGenauthRequest(input);
@@ -2290,17 +2332,39 @@ var Qoni = class {
2290
2332
  didTimeout = true;
2291
2333
  controller.abort();
2292
2334
  }, this.timeoutMs);
2335
+ let response;
2293
2336
  try {
2294
- return await this.fetchImpl(resolved.url, { ...requestInit, signal: controller.signal });
2337
+ response = await new Promise((resolve, reject) => {
2338
+ const onAbort = () => reject(controller.signal.reason);
2339
+ controller.signal.addEventListener("abort", onAbort, { once: true });
2340
+ Promise.resolve().then(() => {
2341
+ controller.signal.throwIfAborted();
2342
+ return this.fetchImpl(resolved.url, { ...requestInit, signal: controller.signal });
2343
+ }).then((response2) => {
2344
+ if (controller.signal.aborted) {
2345
+ void response2.body?.cancel(controller.signal.reason).catch(() => void 0);
2346
+ reject(controller.signal.reason);
2347
+ } else {
2348
+ resolve(response2);
2349
+ }
2350
+ }, reject).finally(() => controller.signal.removeEventListener("abort", onAbort));
2351
+ });
2352
+ const result = await consume(response, controller.signal);
2353
+ controller.signal.throwIfAborted();
2354
+ return result;
2295
2355
  } catch (error) {
2296
- if (isAbortError(error) && didTimeout || isConnectTimeoutError(error)) {
2356
+ void response?.body?.cancel().catch(() => void 0);
2357
+ if (signal?.aborted) throw signal.reason;
2358
+ if (didTimeout || isConnectTimeoutError(error)) {
2297
2359
  throw timeoutError(error);
2298
2360
  }
2299
2361
  if (isFetchNetworkError(error)) throw networkError(error);
2300
2362
  throw error;
2301
2363
  } finally {
2302
2364
  clearTimeout(timeout);
2303
- if (!opts?.streamBody) signal?.removeEventListener("abort", abortFromSignal);
2365
+ const cleanup = () => signal?.removeEventListener("abort", abortFromSignal);
2366
+ if (opts?.streamCleanup) opts.streamCleanup(cleanup);
2367
+ else cleanup();
2304
2368
  }
2305
2369
  }
2306
2370
  async baseUrlFor(service) {
@@ -2356,7 +2420,7 @@ var Qoni = class {
2356
2420
  pathname,
2357
2421
  {}
2358
2422
  );
2359
- const response = await this.fetchWithTimeout(
2423
+ const { response, payload } = await this.fetchWithTimeout(
2360
2424
  urlWithBasePath(this.host, pathname),
2361
2425
  {
2362
2426
  method: "GET",
@@ -2364,9 +2428,9 @@ var Qoni = class {
2364
2428
  accept: "application/json",
2365
2429
  ...headers
2366
2430
  }
2367
- }
2431
+ },
2432
+ async (response2, signal) => ({ response: response2, payload: await readJson(response2, signal) })
2368
2433
  );
2369
- const payload = await readJson(response);
2370
2434
  if (!response.ok) {
2371
2435
  throw errorFromPayload(
2372
2436
  response.status,
@@ -2460,18 +2524,52 @@ function requestBody(body) {
2460
2524
  function isBodyInit(value) {
2461
2525
  return typeof value === "string" || value instanceof ArrayBuffer || value instanceof Blob || value instanceof FormData || value instanceof URLSearchParams;
2462
2526
  }
2463
- async function readJson(response) {
2527
+ async function readBody(response, signal) {
2528
+ if (!response.body) {
2529
+ signal.throwIfAborted();
2530
+ return new Uint8Array();
2531
+ }
2532
+ const reader = response.body.getReader();
2533
+ const onAbort = () => {
2534
+ void reader.cancel(signal.reason).catch(() => void 0);
2535
+ };
2536
+ signal.addEventListener("abort", onAbort, { once: true });
2537
+ try {
2538
+ const chunks = [];
2539
+ let size = 0;
2540
+ for (; ; ) {
2541
+ signal.throwIfAborted();
2542
+ const { done, value } = await reader.read();
2543
+ signal.throwIfAborted();
2544
+ if (done) break;
2545
+ chunks.push(value);
2546
+ size += value.byteLength;
2547
+ }
2548
+ const bytes = new Uint8Array(size);
2549
+ let offset = 0;
2550
+ for (const chunk of chunks) {
2551
+ bytes.set(chunk, offset);
2552
+ offset += chunk.byteLength;
2553
+ }
2554
+ return bytes;
2555
+ } finally {
2556
+ signal.removeEventListener("abort", onAbort);
2557
+ void reader.cancel().catch(() => void 0);
2558
+ reader.releaseLock();
2559
+ }
2560
+ }
2561
+ async function readJson(response, signal) {
2464
2562
  const contentType = response.headers.get("content-type")?.toLowerCase() || "";
2465
- if (contentType.includes("ndjson") || contentType.includes("jsonl")) {
2466
- const body = await response.text();
2467
- if (!body.trim()) return [];
2468
- try {
2563
+ const body = new TextDecoder().decode(await readBody(response, signal));
2564
+ try {
2565
+ if (contentType.includes("ndjson") || contentType.includes("jsonl")) {
2566
+ if (!body.trim()) return [];
2469
2567
  return body.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
2470
- } catch {
2471
- return void 0;
2472
2568
  }
2569
+ return JSON.parse(body);
2570
+ } catch {
2571
+ return void 0;
2473
2572
  }
2474
- return response.json().catch(() => void 0);
2475
2573
  }
2476
2574
  function normalizeResponse(payload, _service) {
2477
2575
  if (isJsonObject(payload) && ("data" in payload || "meta" in payload)) {
@@ -2714,6 +2812,7 @@ function isRetryableStreamError(err) {
2714
2812
  if (err && typeof err === "object") {
2715
2813
  const e = err;
2716
2814
  if (e.name === "AbortError") return false;
2815
+ if (e.status === 408) return true;
2717
2816
  if (typeof e.retryable === "boolean") return e.retryable;
2718
2817
  if (typeof e.status === "number") {
2719
2818
  return e.status >= 500 || e.status === 408 || e.status === 429;
@@ -2820,9 +2919,6 @@ function normalizeDelegateAgentTokenResponse(response) {
2820
2919
  }
2821
2920
  };
2822
2921
  }
2823
- function isAbortError(error) {
2824
- return error instanceof DOMException && error.name === "AbortError";
2825
- }
2826
2922
  function isConnectTimeoutError(error) {
2827
2923
  const cause = errorCause(error);
2828
2924
  return errorCode(cause) === "UND_ERR_CONNECT_TIMEOUT" || errorName(cause) === "ConnectTimeoutError";