@tinycloud/sdk-services 3.3.0-beta.11 → 3.3.0-beta.39

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.
Files changed (39) hide show
  1. package/dist/{BaseService-Cc6Ueicm.d.cts → BaseService-BhfKzKke.d.cts} +5 -1
  2. package/dist/{BaseService-DOdXd7sr.d.ts → BaseService-x4AFZiEa.d.ts} +5 -1
  3. package/dist/encryption/index.cjs +18 -5
  4. package/dist/encryption/index.cjs.map +1 -1
  5. package/dist/encryption/index.d.cts +2 -2
  6. package/dist/encryption/index.d.ts +2 -2
  7. package/dist/encryption/index.js +18 -5
  8. package/dist/encryption/index.js.map +1 -1
  9. package/dist/index.cjs +660 -651
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +17 -10
  12. package/dist/index.d.ts +17 -10
  13. package/dist/index.js +660 -651
  14. package/dist/index.js.map +1 -1
  15. package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
  16. package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
  17. package/dist/kv/index.cjs +72 -80
  18. package/dist/kv/index.cjs.map +1 -1
  19. package/dist/kv/index.d.cts +4 -5
  20. package/dist/kv/index.d.ts +4 -5
  21. package/dist/kv/index.js +72 -80
  22. package/dist/kv/index.js.map +1 -1
  23. package/dist/kv/replication/index.cjs +6 -5
  24. package/dist/kv/replication/index.cjs.map +1 -1
  25. package/dist/kv/replication/index.d.cts +3 -3
  26. package/dist/kv/replication/index.d.ts +3 -3
  27. package/dist/kv/replication/index.js +6 -5
  28. package/dist/kv/replication/index.js.map +1 -1
  29. package/dist/{scope-B-m6cKH-.d.ts → scope-BHTkk5Yk.d.ts} +13 -2
  30. package/dist/{scope-B7oeKdLM.d.cts → scope-DMhalRFn.d.cts} +13 -2
  31. package/dist/sql/index.cjs +30 -16
  32. package/dist/sql/index.cjs.map +1 -1
  33. package/dist/sql/index.d.cts +2 -2
  34. package/dist/sql/index.d.ts +2 -2
  35. package/dist/sql/index.js +30 -16
  36. package/dist/sql/index.js.map +1 -1
  37. package/dist/{types-BjH0ZtGy.d.cts → types-eVS1Whuf.d.cts} +24 -4
  38. package/dist/{types-BjH0ZtGy.d.ts → types-eVS1Whuf.d.ts} +24 -4
  39. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ var ErrorCodes = {
16
16
  PERMISSION_DENIED: "PERMISSION_DENIED",
17
17
  // Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
18
18
  // retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
19
- /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`: nothing was applied; retry after `meta.retryAfterMs`. */
19
+ /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */
20
20
  NODE_BUSY: "NODE_BUSY",
21
21
  /** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
22
22
  REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
@@ -57,11 +57,12 @@ var ErrorCodes = {
57
57
  DUCKDB_READONLY_VIOLATION: "DUCKDB_READONLY_VIOLATION"
58
58
  };
59
59
  var defaultRetryPolicy = {
60
- maxAttempts: 3,
60
+ maxAttempts: 4,
61
61
  backoff: "exponential",
62
- baseDelayMs: 1e3,
63
- maxDelayMs: 1e4,
64
- retryableErrors: [ErrorCodes.NETWORK_ERROR, ErrorCodes.TIMEOUT]
62
+ baseDelayMs: 250,
63
+ maxDelayMs: 4e3,
64
+ retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],
65
+ maxTotalMs: 12e3
65
66
  };
66
67
  var TelemetryEvents = {
67
68
  SPAN: "telemetry.span",
@@ -488,7 +489,9 @@ var RetryPolicySchema = z.object({
488
489
  /** Maximum delay in milliseconds between retries */
489
490
  maxDelayMs: z.number().nonnegative(),
490
491
  /** Error codes that should trigger a retry */
491
- retryableErrors: z.array(z.string())
492
+ retryableErrors: z.array(z.string()),
493
+ /** Total time budget for all attempts and waits, in milliseconds */
494
+ maxTotalMs: z.number().positive().optional()
492
495
  });
493
496
  var ServiceSessionSchema = z.object({
494
497
  /** The delegation header containing the UCAN */
@@ -625,448 +628,107 @@ function validateTelemetrySpanEvent(data) {
625
628
  return { ok: true, data: result.data };
626
629
  }
627
630
 
628
- // src/context.ts
629
- var ServiceContext = class {
630
- constructor(config) {
631
- this._session = null;
632
- this._services = /* @__PURE__ */ new Map();
633
- this._eventHandlers = /* @__PURE__ */ new Map();
634
- this._abortController = new AbortController();
635
- this._retired = false;
636
- this._invoke = this.wrapInvoke(config.invoke);
637
- this._invokeAny = config.invokeAny ? this.wrapInvokeAny(config.invokeAny) : void 0;
638
- this._fetch = this.wrapFetch(config.fetch ?? globalThis.fetch.bind(globalThis));
639
- this._hosts = config.hosts;
640
- this._session = config.session ?? null;
641
- this._retryPolicy = {
642
- ...defaultRetryPolicy,
643
- ...config.retryPolicy
644
- };
645
- this._telemetryEnabled = typeof config.telemetry === "boolean" ? config.telemetry : config.telemetry?.enabled === true;
646
- this._telemetryHandler = typeof config.telemetry === "object" ? config.telemetry.onEvent : void 0;
647
- }
648
- // ============================================================
649
- // Session Management
650
- // ============================================================
651
- /**
652
- * Get the current session.
653
- */
654
- get session() {
655
- return this._retired ? null : this._session;
631
+ // src/responseErrors.ts
632
+ function parseServiceErrorBody(errorText) {
633
+ try {
634
+ return JSON.parse(errorText);
635
+ } catch {
636
+ return {};
656
637
  }
657
- /**
658
- * Check if the context has an authenticated session.
659
- */
660
- get isAuthenticated() {
661
- return !this._retired && this._session !== null;
638
+ }
639
+ function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
640
+ if (parsed.message) {
641
+ return compactMessage(parsed.message);
662
642
  }
663
- /**
664
- * Update the session and notify all registered services.
665
- *
666
- * @param session - New session or null to clear
667
- */
668
- setSession(session) {
669
- this.assertActive();
670
- this._session = session;
671
- this.emit("session.changed", { authenticated: session !== null });
672
- for (const service of this._services.values()) {
673
- service.onSessionChange(session);
643
+ if (looksLikeHtml(errorText)) {
644
+ if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
645
+ return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
674
646
  }
647
+ return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
675
648
  }
676
- // ============================================================
677
- // Platform Dependencies
678
- // ============================================================
679
- /**
680
- * Get the invoke function for WASM operations.
681
- */
682
- get invoke() {
683
- this.assertActive();
684
- return this._invoke;
685
- }
686
- /**
687
- * Get the multi-resource invoke function when available.
688
- */
689
- get invokeAny() {
690
- this.assertActive();
691
- return this._invokeAny;
649
+ return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
650
+ }
651
+ function responseErrorMeta(status, statusText, errorText) {
652
+ const meta = { status, statusText };
653
+ const snippet = compactMessage(errorText);
654
+ if (snippet) {
655
+ meta.responseSnippet = snippet.slice(0, 300);
692
656
  }
693
- /**
694
- * Get the fetch function for HTTP requests.
695
- */
696
- get fetch() {
697
- this.assertActive();
698
- return this._fetch;
657
+ return meta;
658
+ }
659
+ function looksLikeHtml(text) {
660
+ return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
661
+ }
662
+ function compactMessage(message) {
663
+ const compact = message.replace(/\s+/g, " ").trim();
664
+ return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
665
+ }
666
+
667
+ // src/errors.ts
668
+ import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
669
+
670
+ // src/encryption/networkId.ts
671
+ var URN_PREFIX = "urn:tinycloud:encryption:";
672
+ var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
673
+ var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
674
+ var NetworkIdError = class extends Error {
675
+ constructor(message) {
676
+ super(message);
677
+ this.name = "NetworkIdError";
699
678
  }
700
- /**
701
- * Get the list of TinyCloud host URLs.
702
- */
703
- get hosts() {
704
- this.assertActive();
705
- return this._hosts;
679
+ };
680
+ function parseNetworkId(networkId) {
681
+ if (typeof networkId !== "string" || networkId.length === 0) {
682
+ throw new NetworkIdError("networkId must be a non-empty string");
706
683
  }
707
- // ============================================================
708
- // Service Registry
709
- // ============================================================
710
- /**
711
- * Register a service with the context.
712
- *
713
- * @param name - Service name (e.g., 'kv')
714
- * @param service - Service instance
715
- */
716
- registerService(name, service) {
717
- this._services.set(name, service);
684
+ if (!networkId.startsWith(URN_PREFIX)) {
685
+ throw new NetworkIdError(
686
+ `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
687
+ );
718
688
  }
719
- /**
720
- * Unregister a service from the context.
721
- *
722
- * @param name - Service name to remove
723
- */
724
- unregisterService(name) {
725
- this._services.delete(name);
689
+ const body = networkId.slice(URN_PREFIX.length);
690
+ const lastColon = body.lastIndexOf(":");
691
+ if (lastColon <= 0 || lastColon === body.length - 1) {
692
+ throw new NetworkIdError(
693
+ `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
694
+ );
726
695
  }
727
- /**
728
- * Get a registered service by name.
729
- *
730
- * @param name - Service name
731
- * @returns The service instance or undefined if not registered
732
- */
733
- getService(name) {
734
- return this._services.get(name);
696
+ const ownerDid = body.slice(0, lastColon);
697
+ const name = body.slice(lastColon + 1);
698
+ if (!ownerDid.startsWith("did:")) {
699
+ throw new NetworkIdError(
700
+ `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
701
+ );
735
702
  }
736
- // ============================================================
737
- // Event System (Telemetry)
738
- // ============================================================
739
- /**
740
- * Emit a telemetry event.
741
- *
742
- * @param event - Event name
743
- * @param data - Event data
744
- */
745
- emit(event, data) {
746
- tinyCloudDebugLogger.log(event, data);
747
- if (this._telemetryEnabled && this._telemetryHandler) {
748
- try {
749
- this._telemetryHandler(event, projectDiagnosticData(data));
750
- } catch (error) {
751
- console.error(`Error in telemetry handler for "${event}":`, error);
752
- }
753
- }
754
- const handlers = this._eventHandlers.get(event);
755
- if (handlers) {
756
- for (const handler of handlers) {
757
- try {
758
- handler(data);
759
- } catch (error) {
760
- console.error(`Error in event handler for "${event}":`, error);
761
- }
762
- }
763
- }
703
+ const didParts = ownerDid.split(":");
704
+ if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
705
+ throw new NetworkIdError(
706
+ `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
707
+ );
764
708
  }
765
- /**
766
- * Subscribe to telemetry events.
767
- *
768
- * @param event - Event name to subscribe to
769
- * @param handler - Handler function
770
- * @returns Unsubscribe function
771
- */
772
- on(event, handler) {
773
- if (!this._eventHandlers.has(event)) {
774
- this._eventHandlers.set(event, /* @__PURE__ */ new Set());
775
- }
776
- this._eventHandlers.get(event).add(handler);
777
- return () => {
778
- const handlers = this._eventHandlers.get(event);
779
- if (handlers) {
780
- handlers.delete(handler);
781
- if (handlers.size === 0) {
782
- this._eventHandlers.delete(event);
783
- }
784
- }
785
- };
709
+ if (!NETWORK_NAME_RE.test(name)) {
710
+ throw new NetworkIdError(
711
+ `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
712
+ );
786
713
  }
787
- /**
788
- * Remove all event handlers for an event.
789
- *
790
- * @param event - Event name (if omitted, clears all events)
791
- */
792
- clearEventHandlers(event) {
793
- if (event) {
794
- this._eventHandlers.delete(event);
795
- } else {
796
- this._eventHandlers.clear();
797
- }
714
+ return { networkId, ownerDid, name };
715
+ }
716
+ function buildNetworkId(ownerDid, name) {
717
+ if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
718
+ throw new NetworkIdError("ownerDid must be a DID");
798
719
  }
799
- // ============================================================
800
- // Lifecycle
801
- // ============================================================
802
- /**
803
- * Get the abort signal for cancelling operations.
804
- */
805
- get abortSignal() {
806
- return this._abortController.signal;
720
+ if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
721
+ throw new NetworkIdError(
722
+ `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
723
+ );
807
724
  }
808
- get operationAbortSignal() {
809
- return this._operationAbortSignal;
810
- }
811
- setOperationAbortSignal(signal) {
812
- this._operationAbortSignal = signal;
813
- }
814
- /**
815
- * Permanently retire this graph after its owner installs a replacement.
816
- * Unlike `abort()`, retirement never creates a fresh controller, so captured
817
- * services cannot resume calls under a superseded session.
818
- */
819
- retire() {
820
- if (this._retired) return;
821
- this._retired = true;
822
- this._session = null;
823
- this._abortController.abort();
824
- for (const service of this._services.values()) {
825
- try {
826
- service.onSignOut();
827
- } catch (error) {
828
- console.error("Error retiring service:", error);
829
- }
830
- }
831
- }
832
- /**
833
- * Abort all pending operations and notify services.
834
- * Creates a new AbortController for future operations.
835
- */
836
- abort() {
837
- if (this._retired) return;
838
- this._abortController.abort();
839
- this._abortController = new AbortController();
840
- for (const service of this._services.values()) {
841
- try {
842
- service.onSignOut();
843
- } catch (error) {
844
- console.error("Error signing out service:", error);
845
- }
846
- }
847
- }
848
- /**
849
- * Sign out - abort operations and clear session.
850
- */
851
- signOut() {
852
- if (this._retired) return;
853
- this.abort();
854
- this.setSession(null);
855
- this.emit("session.expired", {});
856
- }
857
- // ============================================================
858
- // Retry Policy
859
- // ============================================================
860
- /**
861
- * Get the retry policy configuration.
862
- */
863
- get retryPolicy() {
864
- return this._retryPolicy;
865
- }
866
- wrapInvoke(invoke) {
867
- return (session, service, path, action, facts) => {
868
- this.assertActive();
869
- if (!tinyCloudDebugLogger.isEnabled()) {
870
- return invoke(session, service, path, action, facts);
871
- }
872
- const timer = tinyCloudDebugLogger.startTimer("sdk.invoke", {
873
- service,
874
- path,
875
- action,
876
- facts
877
- });
878
- try {
879
- const result = invoke(session, service, path, action, facts);
880
- timer.stop({ ok: true, service, path, action });
881
- return result;
882
- } catch (error) {
883
- timer.stop({ ok: false, service, path, action, error });
884
- throw error;
885
- }
886
- };
887
- }
888
- assertActive() {
889
- if (this._retired) {
890
- throw new Error("Service graph has been retired: its session was replaced or signed out.");
891
- }
892
- }
893
- wrapInvokeAny(invokeAny) {
894
- return (session, entries, facts) => {
895
- this.assertActive();
896
- if (!tinyCloudDebugLogger.isEnabled()) {
897
- return invokeAny(session, entries, facts);
898
- }
899
- const timer = tinyCloudDebugLogger.startTimer("sdk.invokeAny", {
900
- entries,
901
- facts
902
- });
903
- try {
904
- const result = invokeAny(session, entries, facts);
905
- timer.stop({ ok: true, entries });
906
- return result;
907
- } catch (error) {
908
- timer.stop({ ok: false, entries, error });
909
- throw error;
910
- }
911
- };
912
- }
913
- wrapFetch(fetchFn) {
914
- return async (url, init) => {
915
- this.assertActive();
916
- const request = {
917
- ...init,
918
- signal: this.combineSignals(init?.signal)
919
- };
920
- if (!tinyCloudDebugLogger.isEnabled()) {
921
- return fetchFn(url, request);
922
- }
923
- const timer = tinyCloudDebugLogger.startTimer("sdk.fetch", {
924
- url,
925
- method: request.method ?? "GET",
926
- init: request
927
- });
928
- try {
929
- const response = await fetchFn(url, request);
930
- timer.stop({
931
- ok: response.ok,
932
- url,
933
- method: request.method ?? "GET",
934
- status: response.status,
935
- statusText: response.statusText
936
- });
937
- return response;
938
- } catch (error) {
939
- timer.stop({
940
- ok: false,
941
- url,
942
- method: request.method ?? "GET",
943
- error
944
- });
945
- throw error;
946
- }
947
- };
948
- }
949
- combineSignals(signal) {
950
- if (!signal) return this._abortController.signal;
951
- const combined = new AbortController();
952
- const abort = (source) => combined.abort(source.reason);
953
- if (signal.aborted) {
954
- abort(signal);
955
- } else if (this._abortController.signal.aborted) {
956
- abort(this._abortController.signal);
957
- } else {
958
- signal.addEventListener("abort", () => abort(signal), { once: true });
959
- this._abortController.signal.addEventListener(
960
- "abort",
961
- () => abort(this._abortController.signal),
962
- { once: true }
963
- );
964
- }
965
- return combined.signal;
966
- }
967
- };
968
-
969
- // src/responseErrors.ts
970
- function parseServiceErrorBody(errorText) {
971
- try {
972
- return JSON.parse(errorText);
973
- } catch {
974
- return {};
975
- }
976
- }
977
- function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
978
- if (parsed.message) {
979
- return compactMessage(parsed.message);
980
- }
981
- if (looksLikeHtml(errorText)) {
982
- if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
983
- return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
984
- }
985
- return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
986
- }
987
- return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
988
- }
989
- function responseErrorMeta(status, statusText, errorText) {
990
- const meta = { status, statusText };
991
- const snippet = compactMessage(errorText);
992
- if (snippet) {
993
- meta.responseSnippet = snippet.slice(0, 300);
994
- }
995
- return meta;
996
- }
997
- function looksLikeHtml(text) {
998
- return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
999
- }
1000
- function compactMessage(message) {
1001
- const compact = message.replace(/\s+/g, " ").trim();
1002
- return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
1003
- }
1004
-
1005
- // src/errors.ts
1006
- import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
1007
-
1008
- // src/encryption/networkId.ts
1009
- var URN_PREFIX = "urn:tinycloud:encryption:";
1010
- var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
1011
- var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
1012
- var NetworkIdError = class extends Error {
1013
- constructor(message) {
1014
- super(message);
1015
- this.name = "NetworkIdError";
1016
- }
1017
- };
1018
- function parseNetworkId(networkId) {
1019
- if (typeof networkId !== "string" || networkId.length === 0) {
1020
- throw new NetworkIdError("networkId must be a non-empty string");
1021
- }
1022
- if (!networkId.startsWith(URN_PREFIX)) {
1023
- throw new NetworkIdError(
1024
- `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
1025
- );
1026
- }
1027
- const body = networkId.slice(URN_PREFIX.length);
1028
- const lastColon = body.lastIndexOf(":");
1029
- if (lastColon <= 0 || lastColon === body.length - 1) {
1030
- throw new NetworkIdError(
1031
- `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
1032
- );
1033
- }
1034
- const ownerDid = body.slice(0, lastColon);
1035
- const name = body.slice(lastColon + 1);
1036
- if (!ownerDid.startsWith("did:")) {
1037
- throw new NetworkIdError(
1038
- `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
1039
- );
1040
- }
1041
- const didParts = ownerDid.split(":");
1042
- if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
1043
- throw new NetworkIdError(
1044
- `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
1045
- );
1046
- }
1047
- if (!NETWORK_NAME_RE.test(name)) {
1048
- throw new NetworkIdError(
1049
- `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1050
- );
1051
- }
1052
- return { networkId, ownerDid, name };
1053
- }
1054
- function buildNetworkId(ownerDid, name) {
1055
- if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
1056
- throw new NetworkIdError("ownerDid must be a DID");
1057
- }
1058
- if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
1059
- throw new NetworkIdError(
1060
- `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1061
- );
1062
- }
1063
- const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
1064
- parseNetworkId(networkId);
1065
- return networkId;
1066
- }
1067
- function isNetworkId(networkId) {
1068
- if (typeof networkId !== "string") {
1069
- return false;
725
+ const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
726
+ parseNetworkId(networkId);
727
+ return networkId;
728
+ }
729
+ function isNetworkId(networkId) {
730
+ if (typeof networkId !== "string") {
731
+ return false;
1070
732
  }
1071
733
  try {
1072
734
  parseNetworkId(networkId);
@@ -1165,7 +827,7 @@ function nodeRetryError(service, message, response, errorText) {
1165
827
  const body = errorText.trim();
1166
828
  const [, token, stage] = /^([a-z_]+)(?::\s*([a-z_]+):)?/.exec(body) ?? [];
1167
829
  let code;
1168
- if (response.status === 503 && (token === "sql_busy" || token === "unavailable_retryable")) {
830
+ if (response.status === 503 && (token === "sql_busy" || token === "unavailable_retryable" || token === "read_audit_busy")) {
1169
831
  code = ErrorCodes.NODE_BUSY;
1170
832
  } else if (response.status === 503 && token === "request_timeout") {
1171
833
  code = ErrorCodes.REQUEST_TIMEOUT;
@@ -1383,56 +1045,442 @@ function parseStorageRejection(responseText) {
1383
1045
  } catch {
1384
1046
  return parseStorageQuotaBytes(responseText);
1385
1047
  }
1386
- if (typeof body !== "object" || body === null) return void 0;
1387
- const record = body;
1388
- const message = typeof record.message === "string" ? record.message : "";
1389
- const space = byteCountsOf(record.space) ?? parseStorageQuotaBytes(message);
1390
- const isStorageBody = typeof record.error === "string" && STORAGE_REJECTION_BODY_ERRORS[record.error] === true;
1391
- if (!space && !isStorageBody) {
1392
- return void 0;
1048
+ if (typeof body !== "object" || body === null) return void 0;
1049
+ const record = body;
1050
+ const message = typeof record.message === "string" ? record.message : "";
1051
+ const space = byteCountsOf(record.space) ?? parseStorageQuotaBytes(message);
1052
+ const isStorageBody = typeof record.error === "string" && STORAGE_REJECTION_BODY_ERRORS[record.error] === true;
1053
+ if (!space && !isStorageBody) {
1054
+ return void 0;
1055
+ }
1056
+ const details = { ...space };
1057
+ const account = byteCountsOf(record.account);
1058
+ if (account) {
1059
+ const plan = record.account.plan;
1060
+ details.account = typeof plan === "string" && plan !== "" ? { ...account, plan } : account;
1061
+ }
1062
+ return details;
1063
+ }
1064
+ function storageRejectionError(service, status, meta, responseText) {
1065
+ const quotaMeta = { ...meta, ...parseStorageRejection(responseText) };
1066
+ return status === 402 ? storageQuotaExceededError(service, STORAGE_FULL_MESSAGE, quotaMeta) : storageLimitReachedError(service, STORAGE_WRITE_TOO_LARGE_MESSAGE, quotaMeta);
1067
+ }
1068
+ function isStorageFullError(error) {
1069
+ return error?.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error?.code === ErrorCodes.STORAGE_LIMIT_REACHED;
1070
+ }
1071
+ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
1072
+ if (error instanceof Error) {
1073
+ if (isRequestTimeoutError(error)) {
1074
+ return timeoutError(service, error.timeoutMs, error);
1075
+ }
1076
+ if (error.name === "AbortError") {
1077
+ return abortedError(service);
1078
+ }
1079
+ if (error.name === "TimeoutError" || error.message.toLowerCase().includes("timeout")) {
1080
+ return timeoutError(service);
1081
+ }
1082
+ return {
1083
+ code: defaultCode,
1084
+ message: error.message,
1085
+ service,
1086
+ cause: error
1087
+ };
1088
+ }
1089
+ return {
1090
+ code: defaultCode,
1091
+ message: String(error),
1092
+ service
1093
+ };
1094
+ }
1095
+ function errorResult(error) {
1096
+ return err(error);
1097
+ }
1098
+
1099
+ // src/sleep.ts
1100
+ var abortableSleep = (ms, signal, unref = false) => new Promise((resolve) => {
1101
+ const done = () => {
1102
+ clearTimeout(timer);
1103
+ signal.removeEventListener("abort", done);
1104
+ resolve();
1105
+ };
1106
+ const timer = setTimeout(done, ms);
1107
+ if (unref) timer.unref?.();
1108
+ signal.aborted ? done() : signal.addEventListener("abort", done);
1109
+ });
1110
+
1111
+ // src/context.ts
1112
+ var ServiceContext = class {
1113
+ constructor(config) {
1114
+ this._session = null;
1115
+ this._services = /* @__PURE__ */ new Map();
1116
+ this._eventHandlers = /* @__PURE__ */ new Map();
1117
+ this._abortController = new AbortController();
1118
+ this._retired = false;
1119
+ this._invoke = this.wrapInvoke(config.invoke);
1120
+ this._invokeAny = config.invokeAny ? this.wrapInvokeAny(config.invokeAny) : void 0;
1121
+ this._fetch = this.wrapFetch(config.fetch ?? globalThis.fetch.bind(globalThis));
1122
+ this._hosts = config.hosts;
1123
+ this._session = config.session ?? null;
1124
+ this._retryPolicy = {
1125
+ ...defaultRetryPolicy,
1126
+ ...config.retryPolicy
1127
+ };
1128
+ this._telemetryEnabled = typeof config.telemetry === "boolean" ? config.telemetry : config.telemetry?.enabled === true;
1129
+ this._telemetryHandler = typeof config.telemetry === "object" ? config.telemetry.onEvent : void 0;
1130
+ }
1131
+ // ============================================================
1132
+ // Session Management
1133
+ // ============================================================
1134
+ /**
1135
+ * Get the current session.
1136
+ */
1137
+ get session() {
1138
+ return this._retired ? null : this._session;
1139
+ }
1140
+ /**
1141
+ * Check if the context has an authenticated session.
1142
+ */
1143
+ get isAuthenticated() {
1144
+ return !this._retired && this._session !== null;
1145
+ }
1146
+ /**
1147
+ * Update the session and notify all registered services.
1148
+ *
1149
+ * @param session - New session or null to clear
1150
+ */
1151
+ setSession(session) {
1152
+ this.assertActive();
1153
+ this._session = session;
1154
+ this.emit("session.changed", { authenticated: session !== null });
1155
+ for (const service of this._services.values()) {
1156
+ service.onSessionChange(session);
1157
+ }
1158
+ }
1159
+ // ============================================================
1160
+ // Platform Dependencies
1161
+ // ============================================================
1162
+ /**
1163
+ * Get the invoke function for WASM operations.
1164
+ */
1165
+ get invoke() {
1166
+ this.assertActive();
1167
+ return this._invoke;
1168
+ }
1169
+ /**
1170
+ * Get the multi-resource invoke function when available.
1171
+ */
1172
+ get invokeAny() {
1173
+ this.assertActive();
1174
+ return this._invokeAny;
1175
+ }
1176
+ /**
1177
+ * Get the fetch function for HTTP requests.
1178
+ */
1179
+ get fetch() {
1180
+ this.assertActive();
1181
+ return this._fetch;
1182
+ }
1183
+ /**
1184
+ * Get the list of TinyCloud host URLs.
1185
+ */
1186
+ get hosts() {
1187
+ this.assertActive();
1188
+ return this._hosts;
1189
+ }
1190
+ // ============================================================
1191
+ // Service Registry
1192
+ // ============================================================
1193
+ /**
1194
+ * Register a service with the context.
1195
+ *
1196
+ * @param name - Service name (e.g., 'kv')
1197
+ * @param service - Service instance
1198
+ */
1199
+ registerService(name, service) {
1200
+ this._services.set(name, service);
1201
+ }
1202
+ /**
1203
+ * Unregister a service from the context.
1204
+ *
1205
+ * @param name - Service name to remove
1206
+ */
1207
+ unregisterService(name) {
1208
+ this._services.delete(name);
1209
+ }
1210
+ /**
1211
+ * Get a registered service by name.
1212
+ *
1213
+ * @param name - Service name
1214
+ * @returns The service instance or undefined if not registered
1215
+ */
1216
+ getService(name) {
1217
+ return this._services.get(name);
1218
+ }
1219
+ // ============================================================
1220
+ // Event System (Telemetry)
1221
+ // ============================================================
1222
+ /**
1223
+ * Emit a telemetry event.
1224
+ *
1225
+ * @param event - Event name
1226
+ * @param data - Event data
1227
+ */
1228
+ emit(event, data) {
1229
+ tinyCloudDebugLogger.log(event, data);
1230
+ if (this._telemetryEnabled && this._telemetryHandler) {
1231
+ try {
1232
+ this._telemetryHandler(event, projectDiagnosticData(data));
1233
+ } catch (error) {
1234
+ console.error(`Error in telemetry handler for "${event}":`, error);
1235
+ }
1236
+ }
1237
+ const handlers = this._eventHandlers.get(event);
1238
+ if (handlers) {
1239
+ for (const handler of handlers) {
1240
+ try {
1241
+ handler(data);
1242
+ } catch (error) {
1243
+ console.error(`Error in event handler for "${event}":`, error);
1244
+ }
1245
+ }
1246
+ }
1247
+ }
1248
+ /**
1249
+ * Subscribe to telemetry events.
1250
+ *
1251
+ * @param event - Event name to subscribe to
1252
+ * @param handler - Handler function
1253
+ * @returns Unsubscribe function
1254
+ */
1255
+ on(event, handler) {
1256
+ if (!this._eventHandlers.has(event)) {
1257
+ this._eventHandlers.set(event, /* @__PURE__ */ new Set());
1258
+ }
1259
+ this._eventHandlers.get(event).add(handler);
1260
+ return () => {
1261
+ const handlers = this._eventHandlers.get(event);
1262
+ if (handlers) {
1263
+ handlers.delete(handler);
1264
+ if (handlers.size === 0) {
1265
+ this._eventHandlers.delete(event);
1266
+ }
1267
+ }
1268
+ };
1269
+ }
1270
+ /**
1271
+ * Remove all event handlers for an event.
1272
+ *
1273
+ * @param event - Event name (if omitted, clears all events)
1274
+ */
1275
+ clearEventHandlers(event) {
1276
+ if (event) {
1277
+ this._eventHandlers.delete(event);
1278
+ } else {
1279
+ this._eventHandlers.clear();
1280
+ }
1281
+ }
1282
+ // ============================================================
1283
+ // Lifecycle
1284
+ // ============================================================
1285
+ /**
1286
+ * Get the abort signal for cancelling operations.
1287
+ */
1288
+ get abortSignal() {
1289
+ return this._abortController.signal;
1290
+ }
1291
+ get operationAbortSignal() {
1292
+ return this._operationAbortSignal;
1293
+ }
1294
+ setOperationAbortSignal(signal) {
1295
+ this._operationAbortSignal = signal;
1296
+ }
1297
+ /**
1298
+ * Permanently retire this graph after its owner installs a replacement.
1299
+ * Unlike `abort()`, retirement never creates a fresh controller, so captured
1300
+ * services cannot resume calls under a superseded session.
1301
+ */
1302
+ retire() {
1303
+ if (this._retired) return;
1304
+ this._retired = true;
1305
+ this._session = null;
1306
+ this._abortController.abort();
1307
+ for (const service of this._services.values()) {
1308
+ try {
1309
+ service.onSignOut();
1310
+ } catch (error) {
1311
+ console.error("Error retiring service:", error);
1312
+ }
1313
+ }
1314
+ }
1315
+ /**
1316
+ * Abort all pending operations and notify services.
1317
+ * Creates a new AbortController for future operations.
1318
+ */
1319
+ abort() {
1320
+ if (this._retired) return;
1321
+ this._abortController.abort();
1322
+ this._abortController = new AbortController();
1323
+ for (const service of this._services.values()) {
1324
+ try {
1325
+ service.onSignOut();
1326
+ } catch (error) {
1327
+ console.error("Error signing out service:", error);
1328
+ }
1329
+ }
1330
+ }
1331
+ /**
1332
+ * Sign out - abort operations and clear session.
1333
+ */
1334
+ signOut() {
1335
+ if (this._retired) return;
1336
+ this.abort();
1337
+ this.setSession(null);
1338
+ this.emit("session.expired", {});
1393
1339
  }
1394
- const details = { ...space };
1395
- const account = byteCountsOf(record.account);
1396
- if (account) {
1397
- const plan = record.account.plan;
1398
- details.account = typeof plan === "string" && plan !== "" ? { ...account, plan } : account;
1340
+ // ============================================================
1341
+ // Retry Policy
1342
+ // ============================================================
1343
+ /**
1344
+ * Get the retry policy configuration.
1345
+ */
1346
+ get retryPolicy() {
1347
+ return this._retryPolicy;
1399
1348
  }
1400
- return details;
1401
- }
1402
- function storageRejectionError(service, status, meta, responseText) {
1403
- const quotaMeta = { ...meta, ...parseStorageRejection(responseText) };
1404
- return status === 402 ? storageQuotaExceededError(service, STORAGE_FULL_MESSAGE, quotaMeta) : storageLimitReachedError(service, STORAGE_WRITE_TOO_LARGE_MESSAGE, quotaMeta);
1405
- }
1406
- function isStorageFullError(error) {
1407
- return error?.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error?.code === ErrorCodes.STORAGE_LIMIT_REACHED;
1408
- }
1409
- function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
1410
- if (error instanceof Error) {
1411
- if (isRequestTimeoutError(error)) {
1412
- return timeoutError(service, error.timeoutMs, error);
1413
- }
1414
- if (error.name === "AbortError") {
1415
- return abortedError(service);
1349
+ wrapInvoke(invoke) {
1350
+ return (session, service, path, action, facts) => {
1351
+ this.assertActive();
1352
+ if (!tinyCloudDebugLogger.isEnabled()) {
1353
+ return invoke(session, service, path, action, facts);
1354
+ }
1355
+ const timer = tinyCloudDebugLogger.startTimer("sdk.invoke", {
1356
+ service,
1357
+ path,
1358
+ action,
1359
+ facts
1360
+ });
1361
+ try {
1362
+ const result = invoke(session, service, path, action, facts);
1363
+ timer.stop({ ok: true, service, path, action });
1364
+ return result;
1365
+ } catch (error) {
1366
+ timer.stop({ ok: false, service, path, action, error });
1367
+ throw error;
1368
+ }
1369
+ };
1370
+ }
1371
+ assertActive() {
1372
+ if (this._retired) {
1373
+ throw new Error("Service graph has been retired: its session was replaced or signed out.");
1416
1374
  }
1417
- if (error.name === "TimeoutError" || error.message.toLowerCase().includes("timeout")) {
1418
- return timeoutError(service);
1375
+ }
1376
+ wrapInvokeAny(invokeAny) {
1377
+ return (session, entries, facts) => {
1378
+ this.assertActive();
1379
+ if (!tinyCloudDebugLogger.isEnabled()) {
1380
+ return invokeAny(session, entries, facts);
1381
+ }
1382
+ const timer = tinyCloudDebugLogger.startTimer("sdk.invokeAny", {
1383
+ entries,
1384
+ facts
1385
+ });
1386
+ try {
1387
+ const result = invokeAny(session, entries, facts);
1388
+ timer.stop({ ok: true, entries });
1389
+ return result;
1390
+ } catch (error) {
1391
+ timer.stop({ ok: false, entries, error });
1392
+ throw error;
1393
+ }
1394
+ };
1395
+ }
1396
+ /** Retry `NODE_BUSY` / `REQUEST_TIMEOUT`; `build` must re-sign per attempt (the node 409s a repeated invocation). */
1397
+ async invokeWithRetry(url, build) {
1398
+ this.assertActive();
1399
+ const policy = this._retryPolicy;
1400
+ const startedAt = Date.now();
1401
+ let backoffMs = 0;
1402
+ let signal;
1403
+ for (let attempt = 1; ; attempt++) {
1404
+ const init = await build();
1405
+ signal ?? (signal = this.combineSignals(init.signal));
1406
+ const response = await this._fetch(url, init);
1407
+ const done = () => {
1408
+ try {
1409
+ response.retry = { attempts: attempt, backoffMs };
1410
+ } catch {
1411
+ }
1412
+ return response;
1413
+ };
1414
+ if (attempt >= policy.maxAttempts || response.status !== 503 || !response.clone) return done();
1415
+ const text = await response.clone().text().catch(() => "");
1416
+ const error = nodeRetryError("", "", response, text);
1417
+ if (!error || error.meta?.retryable !== true || !policy.retryableErrors.includes(error.code)) {
1418
+ return done();
1419
+ }
1420
+ const remaining = (policy.maxTotalMs ?? Infinity) - (Date.now() - startedAt);
1421
+ if (remaining <= 0) return done();
1422
+ const retryAfterMs = error.meta.retryAfterMs;
1423
+ const step = Math.min(
1424
+ policy.maxDelayMs,
1425
+ policy.backoff === "exponential" ? policy.baseDelayMs * 2 ** (attempt - 1) : policy.backoff === "linear" ? policy.baseDelayMs * attempt : 0
1426
+ );
1427
+ const delay = Math.min(
1428
+ typeof retryAfterMs === "number" ? retryAfterMs : Math.random() * step,
1429
+ remaining
1430
+ );
1431
+ response.body?.cancel?.()?.catch?.(() => {
1432
+ });
1433
+ await abortableSleep(delay, signal);
1434
+ if (signal.aborted) throw signal.reason;
1435
+ backoffMs += delay;
1419
1436
  }
1420
- return {
1421
- code: defaultCode,
1422
- message: error.message,
1423
- service,
1424
- cause: error
1437
+ }
1438
+ wrapFetch(fetchFn) {
1439
+ return async (url, init) => {
1440
+ this.assertActive();
1441
+ const request = {
1442
+ ...init,
1443
+ signal: this.combineSignals(init?.signal)
1444
+ };
1445
+ if (!tinyCloudDebugLogger.isEnabled()) {
1446
+ return fetchFn(url, request);
1447
+ }
1448
+ const meta = { url, method: request.method ?? "GET" };
1449
+ const timer = tinyCloudDebugLogger.startTimer("sdk.fetch", { ...meta, init: request });
1450
+ try {
1451
+ const response = await fetchFn(url, request);
1452
+ timer.stop({
1453
+ ...meta,
1454
+ ok: response.ok,
1455
+ status: response.status,
1456
+ statusText: response.statusText
1457
+ });
1458
+ return response;
1459
+ } catch (error) {
1460
+ timer.stop({ ...meta, ok: false, error });
1461
+ throw error;
1462
+ }
1425
1463
  };
1426
1464
  }
1427
- return {
1428
- code: defaultCode,
1429
- message: String(error),
1430
- service
1431
- };
1432
- }
1433
- function errorResult(error) {
1434
- return err(error);
1435
- }
1465
+ combineSignals(signal) {
1466
+ if (!signal) return this._abortController.signal;
1467
+ const combined = new AbortController();
1468
+ const abort = (source) => combined.abort(source.reason);
1469
+ if (signal.aborted) {
1470
+ abort(signal);
1471
+ } else if (this._abortController.signal.aborted) {
1472
+ abort(this._abortController.signal);
1473
+ } else {
1474
+ signal.addEventListener("abort", () => abort(signal), { once: true });
1475
+ this._abortController.signal.addEventListener(
1476
+ "abort",
1477
+ () => abort(this._abortController.signal),
1478
+ { once: true }
1479
+ );
1480
+ }
1481
+ return combined.signal;
1482
+ }
1483
+ };
1436
1484
 
1437
1485
  // src/base/BaseService.ts
1438
1486
  var MAX_TIMER_DELAY_MS = 2147483647;
@@ -1616,6 +1664,18 @@ var BaseService = class {
1616
1664
  }
1617
1665
  return controller.signal;
1618
1666
  }
1667
+ /** POST a signed invocation with retry; `mint` re-signs per attempt, `init.headers` add to the signed ones. */
1668
+ invokeWithRetry(url, mint, { headers: extra, ...init } = {}) {
1669
+ const build = () => {
1670
+ const signed = mint();
1671
+ return {
1672
+ method: "POST",
1673
+ ...init,
1674
+ headers: Array.isArray(signed) ? [...signed, ...Object.entries(extra ?? {})] : { ...signed, ...extra }
1675
+ };
1676
+ };
1677
+ return this.context.invokeWithRetry ? this.context.invokeWithRetry(url, build) : this.context.fetch(url, build());
1678
+ }
1619
1679
  /**
1620
1680
  * Create the abort signal for one request, bounded by the request timeout.
1621
1681
  *
@@ -1983,15 +2043,6 @@ var KVService = class extends BaseService {
1983
2043
  get host() {
1984
2044
  return this.context.hosts[0];
1985
2045
  }
1986
- withJsonContentType(headers) {
1987
- if (Array.isArray(headers)) {
1988
- return [...headers, ["content-type", "application/json"]];
1989
- }
1990
- return {
1991
- ...headers,
1992
- "content-type": "application/json"
1993
- };
1994
- }
1995
2046
  /**
1996
2047
  * Execute an invoke operation.
1997
2048
  *
@@ -2002,20 +2053,11 @@ var KVService = class extends BaseService {
2002
2053
  * @returns Fetch response
2003
2054
  */
2004
2055
  async invokeOperation(path, action, body, signal, extraHeaders) {
2005
- const session = this.context.session;
2006
- const headers = this.context.invoke(
2007
- session,
2008
- "kv",
2009
- path,
2010
- action
2056
+ return this.invokeWithRetry(
2057
+ `${this.host}/invoke`,
2058
+ () => this.context.invoke(this.context.session, "kv", path, action),
2059
+ { headers: extraHeaders, body, signal }
2011
2060
  );
2012
- const requestHeaders = Array.isArray(headers) ? [...headers, ...Object.entries(extraHeaders ?? {})] : { ...headers, ...extraHeaders };
2013
- return this.context.fetch(`${this.host}/invoke`, {
2014
- method: "POST",
2015
- headers: requestHeaders,
2016
- body,
2017
- signal
2018
- });
2019
2061
  }
2020
2062
  /**
2021
2063
  * Serialize a single put value into a fetch body.
@@ -2221,28 +2263,28 @@ var KVService = class extends BaseService {
2221
2263
  }
2222
2264
  const request = this.createRequestSignal(options?.signal, options?.timeout);
2223
2265
  try {
2224
- const session = this.context.session;
2225
- const invocationHeaders = this.context.invokeAny(
2226
- session,
2227
- paths.map((path) => ({
2228
- spaceId: session.spaceId,
2229
- service: "kv",
2230
- path,
2231
- action
2232
- }))
2233
- );
2234
2266
  const limitHeaders = {};
2235
2267
  if (getOptions?.maxResponseBytes !== void 0) {
2236
2268
  limitHeaders["x-tinycloud-max-response-bytes"] = String(
2237
2269
  getOptions.maxResponseBytes
2238
2270
  );
2239
2271
  }
2240
- const headers = Array.isArray(invocationHeaders) ? [...invocationHeaders, ...Object.entries(limitHeaders)] : { ...invocationHeaders, ...limitHeaders };
2241
- const response = await this.context.fetch(`${this.host}/invoke`, {
2242
- method: "POST",
2243
- headers,
2244
- signal: request.signal
2245
- });
2272
+ const response = await this.invokeWithRetry(
2273
+ `${this.host}/invoke`,
2274
+ () => {
2275
+ const session = this.context.session;
2276
+ return this.context.invokeAny(
2277
+ session,
2278
+ paths.map((path) => ({
2279
+ spaceId: session.spaceId,
2280
+ service: "kv",
2281
+ path,
2282
+ action
2283
+ }))
2284
+ );
2285
+ },
2286
+ { headers: limitHeaders, signal: request.signal }
2287
+ );
2246
2288
  if (!response.ok) {
2247
2289
  const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
2248
2290
  if (response.status === 401 || response.status === 403) {
@@ -2288,10 +2330,10 @@ var KVService = class extends BaseService {
2288
2330
  result: err(serviceError(item.error.code, item.error.message, "kv"))
2289
2331
  };
2290
2332
  }
2291
- const headers2 = item.headers;
2333
+ const headers = item.headers;
2292
2334
  const data = action === KVAction.HEAD ? void 0 : this.parseBatchValue(
2293
2335
  item.dataBase64,
2294
- headers2,
2336
+ headers,
2295
2337
  getOptions?.raw,
2296
2338
  getOptions?.binary
2297
2339
  );
@@ -2299,7 +2341,7 @@ var KVService = class extends BaseService {
2299
2341
  key: keys[index],
2300
2342
  result: ok({
2301
2343
  data,
2302
- headers: this.createBatchResponseHeaders(headers2)
2344
+ headers: this.createBatchResponseHeaders(headers)
2303
2345
  })
2304
2346
  };
2305
2347
  });
@@ -2534,7 +2576,7 @@ var KVService = class extends BaseService {
2534
2576
  ));
2535
2577
  }
2536
2578
  const data = await this.parseResponse(response, options?.raw, options?.binary);
2537
- return ok({ data, headers: this.createResponseHeaders(response.headers) });
2579
+ return ok({ data, headers: this.createResponseHeaders(response.headers), ...response.retry && { retry: response.retry } });
2538
2580
  }
2539
2581
  async batchGet(keys, options) {
2540
2582
  return this.withTelemetry(
@@ -2624,7 +2666,8 @@ var KVService = class extends BaseService {
2624
2666
  }
2625
2667
  return ok({
2626
2668
  data: void 0,
2627
- headers: this.createResponseHeaders(response.headers)
2669
+ headers: this.createResponseHeaders(response.headers),
2670
+ ...response.retry && { retry: response.retry }
2628
2671
  });
2629
2672
  }
2630
2673
  /**
@@ -2697,25 +2740,23 @@ var KVService = class extends BaseService {
2697
2740
  entries?.[index]?.body ?? this.serializeBatchPutValue(items[index])
2698
2741
  );
2699
2742
  }
2700
- const headers = this.context.invokeAny(
2701
- session,
2702
- paths.map((path) => ({
2703
- spaceId: session.spaceId,
2704
- service: "kv",
2705
- path,
2706
- action: KVAction.PUT
2707
- }))
2743
+ const response = await this.invokeWithRetry(
2744
+ `${this.host}/invoke`,
2745
+ () => {
2746
+ const headers = this.context.invokeAny(
2747
+ session,
2748
+ paths.map((path) => ({
2749
+ spaceId: session.spaceId,
2750
+ service: "kv",
2751
+ path,
2752
+ action: KVAction.PUT
2753
+ }))
2754
+ );
2755
+ requestMayHaveDispatched = true;
2756
+ return headers;
2757
+ },
2758
+ { body, signal }
2708
2759
  );
2709
- const url = `${this.host}/invoke`;
2710
- const init = {
2711
- method: "POST",
2712
- headers,
2713
- body,
2714
- signal
2715
- };
2716
- const fetchFn = this.context.fetch;
2717
- requestMayHaveDispatched = true;
2718
- const response = await fetchFn(url, init);
2719
2760
  if (!response.ok) {
2720
2761
  let errorText;
2721
2762
  try {
@@ -3185,7 +3226,8 @@ var KVService = class extends BaseService {
3185
3226
  }
3186
3227
  return ok({
3187
3228
  data: void 0,
3188
- headers: this.createResponseHeaders(response.headers)
3229
+ headers: this.createResponseHeaders(response.headers),
3230
+ ...response.retry && { retry: response.retry }
3189
3231
  });
3190
3232
  }
3191
3233
  /**
@@ -3259,12 +3301,6 @@ var KVService = class extends BaseService {
3259
3301
  }
3260
3302
  const path = this.getFullPath(key, options?.prefix);
3261
3303
  const session = this.context.session;
3262
- const headers = this.context.invoke(
3263
- session,
3264
- "kv",
3265
- path,
3266
- KVAction.GET
3267
- );
3268
3304
  const body = {
3269
3305
  space: session.spaceId,
3270
3306
  path,
@@ -3278,12 +3314,15 @@ var KVService = class extends BaseService {
3278
3314
  }
3279
3315
  const request = this.createRequestSignal(options?.signal, options?.timeout);
3280
3316
  try {
3281
- const response = await this.context.fetch(`${this.host}/signed/kv`, {
3282
- method: "POST",
3283
- headers: this.withJsonContentType(headers),
3284
- body: JSON.stringify(body),
3285
- signal: request.signal
3286
- });
3317
+ const response = await this.invokeWithRetry(
3318
+ `${this.host}/signed/kv`,
3319
+ () => this.context.invoke(session, "kv", path, KVAction.GET),
3320
+ {
3321
+ headers: { "content-type": "application/json" },
3322
+ body: JSON.stringify(body),
3323
+ signal: request.signal
3324
+ }
3325
+ );
3287
3326
  if (!response.ok) {
3288
3327
  return await this.createSignedReadUrlError(response, key, path);
3289
3328
  }
@@ -3780,18 +3819,19 @@ var SQLService = class extends BaseService {
3780
3819
  }
3781
3820
  // === Private helpers ===
3782
3821
  async invokeSQL(dbName, actions, body, signal) {
3783
- const session = this.context.session;
3784
3822
  const actionList = Array.isArray(actions) ? actions : [actions];
3785
- const headers = actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
3786
- return this.context.fetch(`${this.host}/invoke`, {
3787
- method: "POST",
3788
- headers: {
3789
- ...headers,
3790
- "Content-Type": "application/json"
3823
+ return this.invokeWithRetry(
3824
+ `${this.host}/invoke`,
3825
+ () => {
3826
+ const session = this.context.session;
3827
+ return actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
3791
3828
  },
3792
- body: JSON.stringify(body),
3793
- signal
3794
- });
3829
+ {
3830
+ headers: { "Content-Type": "application/json" },
3831
+ body: JSON.stringify(body),
3832
+ signal
3833
+ }
3834
+ );
3795
3835
  }
3796
3836
  actionForSql(sql, fallback) {
3797
3837
  const token = firstSqlToken(sql);
@@ -4252,22 +4292,20 @@ var DuckDbService = class extends BaseService {
4252
4292
  }
4253
4293
  const request = this.createRequestSignal(options?.signal);
4254
4294
  try {
4255
- const session = this.context.session;
4256
- const headers = this.context.invoke(
4257
- session,
4258
- "duckdb",
4259
- dbName,
4260
- DuckDbAction.IMPORT
4295
+ const response = await this.invokeWithRetry(
4296
+ `${this.host}/invoke`,
4297
+ () => this.context.invoke(
4298
+ this.context.session,
4299
+ "duckdb",
4300
+ dbName,
4301
+ DuckDbAction.IMPORT
4302
+ ),
4303
+ {
4304
+ headers: { "Content-Type": "application/x-duckdb" },
4305
+ body: new Blob([data]),
4306
+ signal: request.signal
4307
+ }
4261
4308
  );
4262
- const response = await this.context.fetch(`${this.host}/invoke`, {
4263
- method: "POST",
4264
- headers: {
4265
- ...headers,
4266
- "Content-Type": "application/x-duckdb"
4267
- },
4268
- body: new Blob([data]),
4269
- signal: request.signal
4270
- });
4271
4309
  if (!response.ok) {
4272
4310
  return await this.handleErrorResponse(response, "import");
4273
4311
  }
@@ -4281,18 +4319,15 @@ var DuckDbService = class extends BaseService {
4281
4319
  }
4282
4320
  // === Private helpers ===
4283
4321
  async invokeDuckDb(dbName, action, body, signal, extraHeaders) {
4284
- const session = this.context.session;
4285
- const headers = this.context.invoke(session, "duckdb", dbName, action);
4286
- return this.context.fetch(`${this.host}/invoke`, {
4287
- method: "POST",
4288
- headers: {
4289
- ...headers,
4290
- "Content-Type": "application/json",
4291
- ...extraHeaders
4292
- },
4293
- body: JSON.stringify(body),
4294
- signal
4295
- });
4322
+ return this.invokeWithRetry(
4323
+ `${this.host}/invoke`,
4324
+ () => this.context.invoke(this.context.session, "duckdb", dbName, action),
4325
+ {
4326
+ headers: { "Content-Type": "application/json", ...extraHeaders },
4327
+ body: JSON.stringify(body),
4328
+ signal
4329
+ }
4330
+ );
4296
4331
  }
4297
4332
  async handleErrorResponse(response, operation) {
4298
4333
  const errorText = await response.text();
@@ -4481,26 +4516,24 @@ var HooksService = class extends BaseService {
4481
4516
  );
4482
4517
  }
4483
4518
  try {
4484
- const response = await this.context.fetch(`${this.host}/hooks/webhooks`, {
4485
- method: "POST",
4486
- headers: {
4487
- ...serviceHeadersToRecord(
4488
- this.createHookHeaders(
4489
- "tinycloud.hooks/register",
4490
- buildScopePath(webhook.service, webhook.pathPrefix)
4491
- )
4492
- ),
4493
- "content-type": "application/json"
4494
- },
4495
- body: JSON.stringify({
4496
- space: webhook.space,
4497
- service: webhook.service,
4498
- pathPrefix: normalizePathPrefix(webhook.pathPrefix),
4499
- abilities: webhook.abilities ?? [],
4500
- callbackUrl: webhook.callbackUrl,
4501
- secret: webhook.secret
4502
- })
4503
- });
4519
+ const response = await this.invokeWithRetry(
4520
+ `${this.host}/hooks/webhooks`,
4521
+ () => this.createHookHeaders(
4522
+ "tinycloud.hooks/register",
4523
+ buildScopePath(webhook.service, webhook.pathPrefix)
4524
+ ),
4525
+ {
4526
+ headers: { "content-type": "application/json" },
4527
+ body: JSON.stringify({
4528
+ space: webhook.space,
4529
+ service: webhook.service,
4530
+ pathPrefix: normalizePathPrefix(webhook.pathPrefix),
4531
+ abilities: webhook.abilities ?? [],
4532
+ callbackUrl: webhook.callbackUrl,
4533
+ secret: webhook.secret
4534
+ })
4535
+ }
4536
+ );
4504
4537
  if (!response.ok) {
4505
4538
  return err(
4506
4539
  await responseError("hooks", "failed to register webhook", response)
@@ -4538,17 +4571,13 @@ var HooksService = class extends BaseService {
4538
4571
  query.set("prefix", normalizedPrefix);
4539
4572
  }
4540
4573
  }
4541
- const response = await this.context.fetch(
4574
+ const response = await this.invokeWithRetry(
4542
4575
  `${this.host}/hooks/webhooks${query.size > 0 ? `?${query.toString()}` : ""}`,
4543
- {
4544
- method: "GET",
4545
- headers: serviceHeadersToRecord(
4546
- this.createHookHeaders(
4547
- "tinycloud.hooks/list",
4548
- options.service ? buildScopePath(options.service, options.pathPrefix) : "webhooks"
4549
- )
4550
- )
4551
- }
4576
+ () => this.createHookHeaders(
4577
+ "tinycloud.hooks/list",
4578
+ options.service ? buildScopePath(options.service, options.pathPrefix) : "webhooks"
4579
+ ),
4580
+ { method: "GET" }
4552
4581
  );
4553
4582
  if (!response.ok) {
4554
4583
  return err(
@@ -4575,20 +4604,16 @@ var HooksService = class extends BaseService {
4575
4604
  return err(authRequiredError("hooks"));
4576
4605
  }
4577
4606
  try {
4578
- const response = await this.context.fetch(
4607
+ const response = await this.invokeWithRetry(
4579
4608
  `${this.host}/hooks/webhooks/${encodeURIComponent(id)}`,
4580
- {
4581
- method: "DELETE",
4582
- headers: serviceHeadersToRecord(
4583
- this.createHookHeaders(
4584
- "tinycloud.hooks/unregister",
4585
- options.target ? buildScopePath(
4586
- options.target.service,
4587
- options.target.pathPrefix
4588
- ) : `webhooks/${id}`
4589
- )
4590
- )
4591
- }
4609
+ () => this.createHookHeaders(
4610
+ "tinycloud.hooks/unregister",
4611
+ options.target ? buildScopePath(
4612
+ options.target.service,
4613
+ options.target.pathPrefix
4614
+ ) : `webhooks/${id}`
4615
+ ),
4616
+ { method: "DELETE" }
4592
4617
  );
4593
4618
  if (!response.ok) {
4594
4619
  return err(
@@ -5044,23 +5069,7 @@ function readProperty(value, key) {
5044
5069
  return void 0;
5045
5070
  }
5046
5071
  }
5047
- function sleep(delayMs, _attempt, signal) {
5048
- return new Promise((resolve) => {
5049
- const finish = () => {
5050
- clearTimeout(timer);
5051
- signal.removeEventListener("abort", stop);
5052
- resolve();
5053
- };
5054
- const timer = setTimeout(finish, delayMs);
5055
- timer.unref?.();
5056
- const stop = () => finish();
5057
- if (signal.aborted) {
5058
- finish();
5059
- } else {
5060
- signal.addEventListener("abort", stop, { once: true });
5061
- }
5062
- });
5063
- }
5072
+ var sleep = (delayMs, _attempt, signal) => abortableSleep(delayMs, signal, true);
5064
5073
  function abortPromise(signal) {
5065
5074
  let resolve;
5066
5075
  const promise = new Promise((done) => {
@@ -5445,7 +5454,7 @@ var StorageFullMonitor = class {
5445
5454
  var UNKNOWN_OWNER = "\0unknown";
5446
5455
 
5447
5456
  // src/storage/status.ts
5448
- var STORAGE_MANAGE_URL = "https://account.tinycloud.xyz/billing";
5457
+ var STORAGE_MANAGE_URL = "https://account.tinycloud.xyz/storage";
5449
5458
  var STORAGE_NEARLY_FULL_RATIO = 0.9;
5450
5459
  function storageUsageState(usedBytes, limitBytes) {
5451
5460
  if (limitBytes === null) return "ok";