@tinycloud/sdk-services 3.1.0-beta.6 → 3.1.0-beta.9

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/kv/index.js CHANGED
@@ -5,6 +5,10 @@ var ErrorCodes = {
5
5
  AUTH_EXPIRED: "AUTH_EXPIRED",
6
6
  AUTH_REQUIRED: "AUTH_REQUIRED",
7
7
  AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
8
+ /** HTTP 401: the delegation the invocation cites was revoked. */
9
+ AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
10
+ /** HTTP 401: a delegation the cited one rests on was revoked. */
11
+ AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
8
12
  NETWORK_ERROR: "NETWORK_ERROR",
9
13
  TIMEOUT: "TIMEOUT",
10
14
  ABORTED: "ABORTED",
@@ -16,6 +20,10 @@ var ErrorCodes = {
16
20
  KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
17
21
  KV_CONFLICT: "KV_CONFLICT",
18
22
  KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
23
+ /** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
24
+ KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
25
+ /** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
26
+ KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
19
27
  // SQL-specific errors
20
28
  SQL_ERROR: "SQL_ERROR",
21
29
  SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
@@ -163,11 +171,31 @@ function authRequiredError(service) {
163
171
  service
164
172
  };
165
173
  }
166
- function timeoutError(service) {
174
+ var RequestTimeoutError = class extends Error {
175
+ constructor(timeoutMs) {
176
+ super(`Request timed out after ${timeoutMs}ms.`);
177
+ this.name = "TimeoutError";
178
+ this.timeoutMs = timeoutMs;
179
+ }
180
+ };
181
+ function isRequestTimeoutError(error) {
182
+ return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
183
+ }
184
+ function timeoutError(service, timeoutMs, cause) {
185
+ if (timeoutMs === void 0) {
186
+ return {
187
+ code: ErrorCodes.TIMEOUT,
188
+ message: "Request timed out.",
189
+ service,
190
+ ...cause === void 0 ? {} : { cause }
191
+ };
192
+ }
167
193
  return {
168
194
  code: ErrorCodes.TIMEOUT,
169
- message: "Request timed out.",
170
- service
195
+ message: `Request timed out after ${timeoutMs}ms.`,
196
+ service,
197
+ ...cause === void 0 ? {} : { cause },
198
+ meta: { timeoutMs }
171
199
  };
172
200
  }
173
201
  function abortedError(service) {
@@ -314,6 +342,9 @@ function storageRejectionError(service, status, meta, responseText) {
314
342
  }
315
343
  function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
316
344
  if (error instanceof Error) {
345
+ if (isRequestTimeoutError(error)) {
346
+ return timeoutError(service, error.timeoutMs, error);
347
+ }
317
348
  if (error.name === "AbortError") {
318
349
  return abortedError(service);
319
350
  }
@@ -335,6 +366,7 @@ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
335
366
  }
336
367
 
337
368
  // src/base/BaseService.ts
369
+ var MAX_TIMER_DELAY_MS = 2147483647;
338
370
  var BaseService = class {
339
371
  constructor() {
340
372
  /**
@@ -515,6 +547,64 @@ var BaseService = class {
515
547
  }
516
548
  return controller.signal;
517
549
  }
550
+ /**
551
+ * Create the abort signal for one request, bounded by the request timeout.
552
+ *
553
+ * The signal aborts when the service signs out, the context aborts, the
554
+ * caller's `signal` aborts, or the timeout elapses, whichever comes first.
555
+ * A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
556
+ * `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
557
+ * reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
558
+ *
559
+ * The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
560
+ * positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
561
+ * `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
562
+ *
563
+ * Callers must call `dispose()` once the response body has been consumed,
564
+ * which clears the timer and removes the listeners this adds to the
565
+ * long-lived service and context signals.
566
+ *
567
+ * @param signal - The caller's abort signal for this request
568
+ * @param timeoutMs - Per-request timeout override in milliseconds
569
+ */
570
+ createRequestSignal(signal, timeoutMs) {
571
+ const controller = new AbortController();
572
+ const detachers = [];
573
+ let timer;
574
+ const dispose = () => {
575
+ if (timer !== void 0) {
576
+ clearTimeout(timer);
577
+ timer = void 0;
578
+ }
579
+ for (const detach of detachers.splice(0)) {
580
+ detach();
581
+ }
582
+ };
583
+ const abort = (reason) => {
584
+ dispose();
585
+ controller.abort(reason);
586
+ };
587
+ const requestSignal = { signal: controller.signal, dispose };
588
+ const parents = [
589
+ this.abortController.signal,
590
+ this.context?.abortSignal,
591
+ signal
592
+ ].filter((parent) => parent !== void 0);
593
+ for (const parent of parents) {
594
+ if (parent.aborted) {
595
+ abort(parent.reason);
596
+ return requestSignal;
597
+ }
598
+ const onAbort = () => abort(parent.reason);
599
+ parent.addEventListener("abort", onAbort, { once: true });
600
+ detachers.push(() => parent.removeEventListener("abort", onAbort));
601
+ }
602
+ const timeout = timeoutMs ?? this._config.timeout;
603
+ if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
604
+ timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
605
+ }
606
+ return requestSignal;
607
+ }
518
608
  /**
519
609
  * Wrap an operation with error handling and telemetry.
520
610
  *
@@ -623,6 +713,30 @@ var PrefixedKVService = class _PrefixedKVService {
623
713
  removePrefix
624
714
  });
625
715
  }
716
+ /**
717
+ * Read the change feed for everything under this prefix. A view with an
718
+ * empty prefix (`""` or `"/"`) is refused: the node does not serve a
719
+ * whole-space feed.
720
+ */
721
+ async changes(options) {
722
+ if (this._prefix.replace(/\/+$/, "") === "") {
723
+ return err(serviceError(
724
+ ErrorCodes.INVALID_INPUT,
725
+ "KV changes needs a non-empty prefix; this view has none",
726
+ "kv"
727
+ ));
728
+ }
729
+ const syncPrefix = `${this._prefix}/`;
730
+ const response = await this._kv.changes({ ...options, prefix: syncPrefix });
731
+ if (!response.ok) return response;
732
+ return ok({
733
+ ...response.data,
734
+ changes: response.data.changes.map((change) => ({
735
+ ...change,
736
+ key: change.key.startsWith(syncPrefix) ? change.key.slice(syncPrefix.length) : change.key
737
+ }))
738
+ });
739
+ }
626
740
  /**
627
741
  * Delete a key.
628
742
  */
@@ -675,11 +789,18 @@ var KVAction = {
675
789
  PUT: "tinycloud.kv/put",
676
790
  LIST: "tinycloud.kv/list",
677
791
  DELETE: "tinycloud.kv/del",
678
- HEAD: "tinycloud.kv/metadata"
792
+ HEAD: "tinycloud.kv/metadata",
793
+ /** The change feed; never implied by `*` or `tinycloud.kv/*`. */
794
+ SYNC: "tinycloud.kv/sync",
795
+ /** Retention attestation; presented through `retentionGrant`, never invoked or implied. */
796
+ RETAIN: "tinycloud.kv/retain"
679
797
  };
680
798
 
681
799
  // src/kv/KVService.ts
682
800
  var MAX_KV_BATCH_READ_ITEMS = 100;
801
+ function isJsonObject(value) {
802
+ return typeof value === "object" && value !== null && !Array.isArray(value);
803
+ }
683
804
  function encodeKvBatchPartName(path) {
684
805
  return encodeURIComponent(path).replace(
685
806
  /[!'()*]/g,
@@ -775,7 +896,7 @@ var KVService = class extends BaseService {
775
896
  * @param path - Resource path
776
897
  * @param action - KV action
777
898
  * @param body - Optional request body
778
- * @param signal - Optional abort signal
899
+ * @param signal - Request signal from createRequestSignal()
779
900
  * @returns Fetch response
780
901
  */
781
902
  async invokeOperation(path, action, body, signal, extraHeaders) {
@@ -791,7 +912,7 @@ var KVService = class extends BaseService {
791
912
  method: "POST",
792
913
  headers: requestHeaders,
793
914
  body,
794
- signal: this.combineSignals(signal)
915
+ signal
795
916
  });
796
917
  }
797
918
  /**
@@ -968,6 +1089,7 @@ var KVService = class extends BaseService {
968
1089
  "kv"
969
1090
  ));
970
1091
  }
1092
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
971
1093
  try {
972
1094
  const session = this.context.session;
973
1095
  const invocationHeaders = this.context.invokeAny(
@@ -989,7 +1111,7 @@ var KVService = class extends BaseService {
989
1111
  const response = await this.context.fetch(`${this.host}/invoke`, {
990
1112
  method: "POST",
991
1113
  headers,
992
- signal: this.combineSignals(options?.signal)
1114
+ signal: request.signal
993
1115
  });
994
1116
  if (!response.ok) {
995
1117
  const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
@@ -1054,6 +1176,8 @@ var KVService = class extends BaseService {
1054
1176
  return ok({ results, count: results.length });
1055
1177
  } catch (error) {
1056
1178
  return err(wrapError("kv", error));
1179
+ } finally {
1180
+ request.dispose();
1057
1181
  }
1058
1182
  }
1059
1183
  /**
@@ -1186,12 +1310,13 @@ var KVService = class extends BaseService {
1186
1310
  ));
1187
1311
  }
1188
1312
  const path = this.getFullPath(key, options?.prefix);
1313
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1189
1314
  try {
1190
1315
  const response = await this.invokeOperation(
1191
1316
  path,
1192
1317
  KVAction.GET,
1193
1318
  void 0,
1194
- options?.signal,
1319
+ request.signal,
1195
1320
  options?.maxResponseBytes === void 0 ? void 0 : { "x-tinycloud-max-response-bytes": String(options.maxResponseBytes) }
1196
1321
  );
1197
1322
  if (!response.ok) {
@@ -1248,6 +1373,8 @@ var KVService = class extends BaseService {
1248
1373
  });
1249
1374
  } catch (error) {
1250
1375
  return err(wrapError("kv", error));
1376
+ } finally {
1377
+ request.dispose();
1251
1378
  }
1252
1379
  });
1253
1380
  }
@@ -1275,12 +1402,13 @@ var KVService = class extends BaseService {
1275
1402
  }
1276
1403
  const path = this.getFullPath(key, options?.prefix);
1277
1404
  const body = this.serializePutValue(value, options?.contentType);
1405
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1278
1406
  try {
1279
1407
  const response = await this.invokeOperation(
1280
1408
  path,
1281
1409
  KVAction.PUT,
1282
1410
  body,
1283
- options?.signal,
1411
+ request.signal,
1284
1412
  {
1285
1413
  ...options?.ifMatch === void 0 ? {} : { "if-match": options.ifMatch },
1286
1414
  ...options?.ifNoneMatch === void 0 ? {} : { "if-none-match": options.ifNoneMatch }
@@ -1336,6 +1464,8 @@ var KVService = class extends BaseService {
1336
1464
  });
1337
1465
  } catch (error) {
1338
1466
  return err(wrapError("kv", error));
1467
+ } finally {
1468
+ request.dispose();
1339
1469
  }
1340
1470
  });
1341
1471
  }
@@ -1375,6 +1505,7 @@ var KVService = class extends BaseService {
1375
1505
  seen.add(path);
1376
1506
  }
1377
1507
  let requestMayHaveDispatched = false;
1508
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1378
1509
  try {
1379
1510
  const body = new FormData();
1380
1511
  for (let index = 0; index < items.length; index++) {
@@ -1397,7 +1528,7 @@ var KVService = class extends BaseService {
1397
1528
  method: "POST",
1398
1529
  headers,
1399
1530
  body,
1400
- signal: this.combineSignals(options?.signal)
1531
+ signal: request.signal
1401
1532
  };
1402
1533
  const fetchFn = this.context.fetch;
1403
1534
  requestMayHaveDispatched = true;
@@ -1448,14 +1579,16 @@ var KVService = class extends BaseService {
1448
1579
  rawBody = await response.json();
1449
1580
  } catch (jsonError) {
1450
1581
  const cause = jsonError instanceof Error ? jsonError : new Error(String(jsonError));
1582
+ const timedOut = isRequestTimeoutError(cause);
1451
1583
  return err(
1452
1584
  serviceError(
1453
- ErrorCodes.NETWORK_ERROR,
1454
- `KV batchPut response was not valid JSON: ${cause.message}`,
1585
+ timedOut ? ErrorCodes.TIMEOUT : ErrorCodes.NETWORK_ERROR,
1586
+ timedOut ? `KV batchPut response body did not arrive in time: ${cause.message}` : `KV batchPut response was not valid JSON: ${cause.message}`,
1455
1587
  "kv",
1456
1588
  {
1457
1589
  cause,
1458
1590
  meta: {
1591
+ ...timedOut ? { timeoutMs: cause.timeoutMs } : {},
1459
1592
  requestMayHaveDispatched: true,
1460
1593
  responseReceived: true,
1461
1594
  status: response.status,
@@ -1510,6 +1643,8 @@ var KVService = class extends BaseService {
1510
1643
  ...wrapped,
1511
1644
  meta: { ...wrapped.meta, requestMayHaveDispatched }
1512
1645
  });
1646
+ } finally {
1647
+ request.dispose();
1513
1648
  }
1514
1649
  });
1515
1650
  }
@@ -1532,12 +1667,13 @@ var KVService = class extends BaseService {
1532
1667
  if (options?.path) {
1533
1668
  listPath = listPath ? `${listPath}/${options.path}` : options.path;
1534
1669
  }
1670
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1535
1671
  try {
1536
1672
  const response = await this.invokeOperation(
1537
1673
  listPath,
1538
1674
  KVAction.LIST,
1539
1675
  void 0,
1540
- options?.signal,
1676
+ request.signal,
1541
1677
  options?.limit === void 0 && options?.cursor === void 0 ? void 0 : {
1542
1678
  ...options?.limit === void 0 ? {} : { "x-tinycloud-limit": String(options.limit) },
1543
1679
  ...options?.cursor === void 0 ? {} : { "x-tinycloud-cursor": options.cursor }
@@ -1578,9 +1714,174 @@ var KVService = class extends BaseService {
1578
1714
  });
1579
1715
  } catch (error) {
1580
1716
  return err(wrapError("kv", error));
1717
+ } finally {
1718
+ request.dispose();
1719
+ }
1720
+ });
1721
+ }
1722
+ /**
1723
+ * Read one page of the `tinycloud.kv/sync` change feed for a prefix.
1724
+ */
1725
+ async changes(options) {
1726
+ return this.withTelemetry("changes", options?.prefix, async () => {
1727
+ if (!this.requireAuth()) {
1728
+ return err(authRequiredError("kv"));
1729
+ }
1730
+ if (typeof options?.prefix !== "string" || options.prefix.length === 0) {
1731
+ return err(serviceError(
1732
+ ErrorCodes.INVALID_INPUT,
1733
+ "KV changes prefix must be a non-empty string",
1734
+ "kv"
1735
+ ));
1736
+ }
1737
+ if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
1738
+ return err(serviceError(
1739
+ ErrorCodes.INVALID_INPUT,
1740
+ "KV changes limit must be an integer from 1 through 1000",
1741
+ "kv"
1742
+ ));
1743
+ }
1744
+ const headers = {};
1745
+ if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
1746
+ if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
1747
+ if (options.retentionGrant !== void 0) {
1748
+ headers["x-tinycloud-retention-grant"] = options.retentionGrant;
1749
+ }
1750
+ const request = this.createRequestSignal(options.signal, options.timeout);
1751
+ try {
1752
+ const response = await this.invokeOperation(
1753
+ options.prefix,
1754
+ KVAction.SYNC,
1755
+ void 0,
1756
+ request.signal,
1757
+ headers
1758
+ );
1759
+ if (!response.ok) {
1760
+ return await this.changesFailure(response, options.prefix, request.signal);
1761
+ }
1762
+ const text = await response.text();
1763
+ let body;
1764
+ try {
1765
+ body = JSON.parse(text);
1766
+ } catch {
1767
+ body = void 0;
1768
+ }
1769
+ const page = this.normalizeChangesResponse(body);
1770
+ if (!page) {
1771
+ return err(serviceError(
1772
+ ErrorCodes.NETWORK_ERROR,
1773
+ "KV changes response was not a tinycloud.kv/sync page",
1774
+ "kv",
1775
+ { meta: { status: response.status, statusText: response.statusText } }
1776
+ ));
1777
+ }
1778
+ return ok(page);
1779
+ } catch (error) {
1780
+ return err(wrapError("kv", error));
1781
+ } finally {
1782
+ request.dispose();
1581
1783
  }
1582
1784
  });
1583
1785
  }
1786
+ /**
1787
+ * Map a failed `kv/sync` response. 410 and the retention 403 carry
1788
+ * `{"error":{"code","reason"}}`; a revoked grant is a 401 whose text names
1789
+ * `delegation-revoked` or `delegation-ancestor-revoked`. A body read that
1790
+ * fails because the request was cancelled or timed out rethrows, so the
1791
+ * caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
1792
+ */
1793
+ async changesFailure(response, prefix, signal) {
1794
+ if (response.status === 404) {
1795
+ return this.classifyNotFound(response, prefix);
1796
+ }
1797
+ let errorText;
1798
+ try {
1799
+ errorText = await response.text();
1800
+ } catch (error) {
1801
+ if (signal.aborted) throw error;
1802
+ errorText = "";
1803
+ }
1804
+ const meta = { status: response.status, statusText: response.statusText };
1805
+ let reason;
1806
+ let bodyCode;
1807
+ try {
1808
+ const body = JSON.parse(errorText);
1809
+ const error = isJsonObject(body) ? body.error : void 0;
1810
+ if (isJsonObject(error)) {
1811
+ if (typeof error.code === "string") bodyCode = error.code;
1812
+ if (typeof error.reason === "string") reason = error.reason;
1813
+ }
1814
+ } catch {
1815
+ }
1816
+ const context = `Failed to read KV changes for ${JSON.stringify(prefix)}`;
1817
+ if (response.status === 410) {
1818
+ return err(serviceError(
1819
+ ErrorCodes.KV_SYNC_RESET_REQUIRED,
1820
+ `${context}: the node requires a reset (${reason ?? "reason not given"}); discard sync state and restart without a cursor`,
1821
+ "kv",
1822
+ { meta: { ...meta, reason } }
1823
+ ));
1824
+ }
1825
+ if (response.status === 403 && bodyCode === "RETENTION_GRANT_REFUSED") {
1826
+ return err(serviceError(
1827
+ ErrorCodes.KV_RETENTION_GRANT_REFUSED,
1828
+ `${context}: the retention grant was refused (${reason ?? "reason not given"})`,
1829
+ "kv",
1830
+ { meta: { ...meta, reason } }
1831
+ ));
1832
+ }
1833
+ if (response.status === 401) {
1834
+ const revoked = /\bdelegation-ancestor-revoked\b/.test(errorText) ? ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED : /\bdelegation-revoked\b/.test(errorText) ? ErrorCodes.AUTH_DELEGATION_REVOKED : void 0;
1835
+ if (revoked !== void 0) {
1836
+ return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
1837
+ }
1838
+ }
1839
+ if (response.status === 401 || response.status === 403) {
1840
+ return this.authorizationFailure(context, response, errorText, [prefix], KVAction.SYNC);
1841
+ }
1842
+ return err(serviceError(
1843
+ response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR,
1844
+ `${context}: ${response.status} - ${errorText}`,
1845
+ "kv",
1846
+ { meta }
1847
+ ));
1848
+ }
1849
+ normalizeChangesResponse(data) {
1850
+ if (!isJsonObject(data)) return void 0;
1851
+ const { changes, more, cursor, source, authority } = data;
1852
+ if (!Array.isArray(changes) || typeof more !== "boolean" || typeof cursor !== "string" || !isJsonObject(source) || typeof source.nodeDid !== "string" || typeof source.space !== "string" || typeof source.prefix !== "string" || !isJsonObject(authority)) {
1853
+ return void 0;
1854
+ }
1855
+ const { notBefore, expiresAt, retainUntil } = authority;
1856
+ if (notBefore !== null && typeof notBefore !== "string" || expiresAt !== null && typeof expiresAt !== "string" || retainUntil !== null && typeof retainUntil !== "string") {
1857
+ return void 0;
1858
+ }
1859
+ const parsed = [];
1860
+ for (const change of changes) {
1861
+ if (!isJsonObject(change) || typeof change.key !== "string") return void 0;
1862
+ if (change.deleted === true) {
1863
+ parsed.push({ key: change.key, deleted: true });
1864
+ continue;
1865
+ }
1866
+ const { metadata } = change;
1867
+ if (change.deleted !== false || typeof change.etag !== "string" || !isJsonObject(metadata) || !Object.values(metadata).every((value) => typeof value === "string")) {
1868
+ return void 0;
1869
+ }
1870
+ parsed.push({
1871
+ key: change.key,
1872
+ deleted: false,
1873
+ etag: change.etag,
1874
+ metadata
1875
+ });
1876
+ }
1877
+ return {
1878
+ changes: parsed,
1879
+ more,
1880
+ cursor,
1881
+ source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
1882
+ authority: { notBefore, expiresAt, retainUntil }
1883
+ };
1884
+ }
1584
1885
  /**
1585
1886
  * Delete a key.
1586
1887
  */
@@ -1590,12 +1891,13 @@ var KVService = class extends BaseService {
1590
1891
  return err(authRequiredError("kv"));
1591
1892
  }
1592
1893
  const path = this.getFullPath(key, options?.prefix);
1894
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1593
1895
  try {
1594
1896
  const response = await this.invokeOperation(
1595
1897
  path,
1596
1898
  KVAction.DELETE,
1597
1899
  void 0,
1598
- options?.signal,
1900
+ request.signal,
1599
1901
  options?.ifMatch === void 0 ? void 0 : { "if-match": options.ifMatch }
1600
1902
  );
1601
1903
  if (!response.ok) {
@@ -1643,6 +1945,8 @@ var KVService = class extends BaseService {
1643
1945
  });
1644
1946
  } catch (error) {
1645
1947
  return err(wrapError("kv", error));
1948
+ } finally {
1949
+ request.dispose();
1646
1950
  }
1647
1951
  });
1648
1952
  }
@@ -1655,12 +1959,13 @@ var KVService = class extends BaseService {
1655
1959
  return err(authRequiredError("kv"));
1656
1960
  }
1657
1961
  const path = this.getFullPath(key, options?.prefix);
1962
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1658
1963
  try {
1659
1964
  const response = await this.invokeOperation(
1660
1965
  path,
1661
1966
  KVAction.HEAD,
1662
1967
  void 0,
1663
- options?.signal
1968
+ request.signal
1664
1969
  );
1665
1970
  if (!response.ok) {
1666
1971
  if (response.status === 401 || response.status === 403) {
@@ -1691,6 +1996,8 @@ var KVService = class extends BaseService {
1691
1996
  });
1692
1997
  } catch (error) {
1693
1998
  return err(wrapError("kv", error));
1999
+ } finally {
2000
+ request.dispose();
1694
2001
  }
1695
2002
  });
1696
2003
  }
@@ -1728,12 +2035,13 @@ var KVService = class extends BaseService {
1728
2035
  if (options?.etag !== void 0) {
1729
2036
  body.etag = options.etag;
1730
2037
  }
2038
+ const request = this.createRequestSignal(options?.signal, options?.timeout);
1731
2039
  try {
1732
2040
  const response = await this.context.fetch(`${this.host}/signed/kv`, {
1733
2041
  method: "POST",
1734
2042
  headers: this.withJsonContentType(headers),
1735
2043
  body: JSON.stringify(body),
1736
- signal: this.combineSignals(options?.signal)
2044
+ signal: request.signal
1737
2045
  });
1738
2046
  if (!response.ok) {
1739
2047
  return this.createSignedReadUrlError(response, key, path);
@@ -1753,6 +2061,8 @@ var KVService = class extends BaseService {
1753
2061
  return ok(signedUrl);
1754
2062
  } catch (error) {
1755
2063
  return err(wrapError("kv", error));
2064
+ } finally {
2065
+ request.dispose();
1756
2066
  }
1757
2067
  });
1758
2068
  }