@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.cjs CHANGED
@@ -182,7 +182,7 @@ var ErrorCodes = {
182
182
  PERMISSION_DENIED: "PERMISSION_DENIED",
183
183
  // Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
184
184
  // retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
185
- /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`: nothing was applied; retry after `meta.retryAfterMs`. */
185
+ /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */
186
186
  NODE_BUSY: "NODE_BUSY",
187
187
  /** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
188
188
  REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
@@ -223,11 +223,12 @@ var ErrorCodes = {
223
223
  DUCKDB_READONLY_VIOLATION: "DUCKDB_READONLY_VIOLATION"
224
224
  };
225
225
  var defaultRetryPolicy = {
226
- maxAttempts: 3,
226
+ maxAttempts: 4,
227
227
  backoff: "exponential",
228
- baseDelayMs: 1e3,
229
- maxDelayMs: 1e4,
230
- retryableErrors: [ErrorCodes.NETWORK_ERROR, ErrorCodes.TIMEOUT]
228
+ baseDelayMs: 250,
229
+ maxDelayMs: 4e3,
230
+ retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],
231
+ maxTotalMs: 12e3
231
232
  };
232
233
  var TelemetryEvents = {
233
234
  SPAN: "telemetry.span",
@@ -654,7 +655,9 @@ var RetryPolicySchema = import_zod.z.object({
654
655
  /** Maximum delay in milliseconds between retries */
655
656
  maxDelayMs: import_zod.z.number().nonnegative(),
656
657
  /** Error codes that should trigger a retry */
657
- retryableErrors: import_zod.z.array(import_zod.z.string())
658
+ retryableErrors: import_zod.z.array(import_zod.z.string()),
659
+ /** Total time budget for all attempts and waits, in milliseconds */
660
+ maxTotalMs: import_zod.z.number().positive().optional()
658
661
  });
659
662
  var ServiceSessionSchema = import_zod.z.object({
660
663
  /** The delegation header containing the UCAN */
@@ -791,448 +794,107 @@ function validateTelemetrySpanEvent(data) {
791
794
  return { ok: true, data: result.data };
792
795
  }
793
796
 
794
- // src/context.ts
795
- var ServiceContext = class {
796
- constructor(config) {
797
- this._session = null;
798
- this._services = /* @__PURE__ */ new Map();
799
- this._eventHandlers = /* @__PURE__ */ new Map();
800
- this._abortController = new AbortController();
801
- this._retired = false;
802
- this._invoke = this.wrapInvoke(config.invoke);
803
- this._invokeAny = config.invokeAny ? this.wrapInvokeAny(config.invokeAny) : void 0;
804
- this._fetch = this.wrapFetch(config.fetch ?? globalThis.fetch.bind(globalThis));
805
- this._hosts = config.hosts;
806
- this._session = config.session ?? null;
807
- this._retryPolicy = {
808
- ...defaultRetryPolicy,
809
- ...config.retryPolicy
810
- };
811
- this._telemetryEnabled = typeof config.telemetry === "boolean" ? config.telemetry : config.telemetry?.enabled === true;
812
- this._telemetryHandler = typeof config.telemetry === "object" ? config.telemetry.onEvent : void 0;
813
- }
814
- // ============================================================
815
- // Session Management
816
- // ============================================================
817
- /**
818
- * Get the current session.
819
- */
820
- get session() {
821
- return this._retired ? null : this._session;
797
+ // src/responseErrors.ts
798
+ function parseServiceErrorBody(errorText) {
799
+ try {
800
+ return JSON.parse(errorText);
801
+ } catch {
802
+ return {};
822
803
  }
823
- /**
824
- * Check if the context has an authenticated session.
825
- */
826
- get isAuthenticated() {
827
- return !this._retired && this._session !== null;
804
+ }
805
+ function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
806
+ if (parsed.message) {
807
+ return compactMessage(parsed.message);
828
808
  }
829
- /**
830
- * Update the session and notify all registered services.
831
- *
832
- * @param session - New session or null to clear
833
- */
834
- setSession(session) {
835
- this.assertActive();
836
- this._session = session;
837
- this.emit("session.changed", { authenticated: session !== null });
838
- for (const service of this._services.values()) {
839
- service.onSessionChange(session);
809
+ if (looksLikeHtml(errorText)) {
810
+ if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
811
+ return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
840
812
  }
813
+ return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
841
814
  }
842
- // ============================================================
843
- // Platform Dependencies
844
- // ============================================================
845
- /**
846
- * Get the invoke function for WASM operations.
847
- */
848
- get invoke() {
849
- this.assertActive();
850
- return this._invoke;
851
- }
852
- /**
853
- * Get the multi-resource invoke function when available.
854
- */
855
- get invokeAny() {
856
- this.assertActive();
857
- return this._invokeAny;
815
+ return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
816
+ }
817
+ function responseErrorMeta(status, statusText, errorText) {
818
+ const meta = { status, statusText };
819
+ const snippet = compactMessage(errorText);
820
+ if (snippet) {
821
+ meta.responseSnippet = snippet.slice(0, 300);
858
822
  }
859
- /**
860
- * Get the fetch function for HTTP requests.
861
- */
862
- get fetch() {
863
- this.assertActive();
864
- return this._fetch;
823
+ return meta;
824
+ }
825
+ function looksLikeHtml(text) {
826
+ return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
827
+ }
828
+ function compactMessage(message) {
829
+ const compact = message.replace(/\s+/g, " ").trim();
830
+ return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
831
+ }
832
+
833
+ // src/errors.ts
834
+ var import_bootstrap = require("@tinycloud/bootstrap");
835
+
836
+ // src/encryption/networkId.ts
837
+ var URN_PREFIX = "urn:tinycloud:encryption:";
838
+ var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
839
+ var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
840
+ var NetworkIdError = class extends Error {
841
+ constructor(message) {
842
+ super(message);
843
+ this.name = "NetworkIdError";
865
844
  }
866
- /**
867
- * Get the list of TinyCloud host URLs.
868
- */
869
- get hosts() {
870
- this.assertActive();
871
- return this._hosts;
845
+ };
846
+ function parseNetworkId(networkId) {
847
+ if (typeof networkId !== "string" || networkId.length === 0) {
848
+ throw new NetworkIdError("networkId must be a non-empty string");
872
849
  }
873
- // ============================================================
874
- // Service Registry
875
- // ============================================================
876
- /**
877
- * Register a service with the context.
878
- *
879
- * @param name - Service name (e.g., 'kv')
880
- * @param service - Service instance
881
- */
882
- registerService(name, service) {
883
- this._services.set(name, service);
850
+ if (!networkId.startsWith(URN_PREFIX)) {
851
+ throw new NetworkIdError(
852
+ `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
853
+ );
884
854
  }
885
- /**
886
- * Unregister a service from the context.
887
- *
888
- * @param name - Service name to remove
889
- */
890
- unregisterService(name) {
891
- this._services.delete(name);
855
+ const body = networkId.slice(URN_PREFIX.length);
856
+ const lastColon = body.lastIndexOf(":");
857
+ if (lastColon <= 0 || lastColon === body.length - 1) {
858
+ throw new NetworkIdError(
859
+ `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
860
+ );
892
861
  }
893
- /**
894
- * Get a registered service by name.
895
- *
896
- * @param name - Service name
897
- * @returns The service instance or undefined if not registered
898
- */
899
- getService(name) {
900
- return this._services.get(name);
862
+ const ownerDid = body.slice(0, lastColon);
863
+ const name = body.slice(lastColon + 1);
864
+ if (!ownerDid.startsWith("did:")) {
865
+ throw new NetworkIdError(
866
+ `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
867
+ );
901
868
  }
902
- // ============================================================
903
- // Event System (Telemetry)
904
- // ============================================================
905
- /**
906
- * Emit a telemetry event.
907
- *
908
- * @param event - Event name
909
- * @param data - Event data
910
- */
911
- emit(event, data) {
912
- tinyCloudDebugLogger.log(event, data);
913
- if (this._telemetryEnabled && this._telemetryHandler) {
914
- try {
915
- this._telemetryHandler(event, projectDiagnosticData(data));
916
- } catch (error) {
917
- console.error(`Error in telemetry handler for "${event}":`, error);
918
- }
919
- }
920
- const handlers = this._eventHandlers.get(event);
921
- if (handlers) {
922
- for (const handler of handlers) {
923
- try {
924
- handler(data);
925
- } catch (error) {
926
- console.error(`Error in event handler for "${event}":`, error);
927
- }
928
- }
929
- }
869
+ const didParts = ownerDid.split(":");
870
+ if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
871
+ throw new NetworkIdError(
872
+ `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
873
+ );
930
874
  }
931
- /**
932
- * Subscribe to telemetry events.
933
- *
934
- * @param event - Event name to subscribe to
935
- * @param handler - Handler function
936
- * @returns Unsubscribe function
937
- */
938
- on(event, handler) {
939
- if (!this._eventHandlers.has(event)) {
940
- this._eventHandlers.set(event, /* @__PURE__ */ new Set());
941
- }
942
- this._eventHandlers.get(event).add(handler);
943
- return () => {
944
- const handlers = this._eventHandlers.get(event);
945
- if (handlers) {
946
- handlers.delete(handler);
947
- if (handlers.size === 0) {
948
- this._eventHandlers.delete(event);
949
- }
950
- }
951
- };
875
+ if (!NETWORK_NAME_RE.test(name)) {
876
+ throw new NetworkIdError(
877
+ `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
878
+ );
952
879
  }
953
- /**
954
- * Remove all event handlers for an event.
955
- *
956
- * @param event - Event name (if omitted, clears all events)
957
- */
958
- clearEventHandlers(event) {
959
- if (event) {
960
- this._eventHandlers.delete(event);
961
- } else {
962
- this._eventHandlers.clear();
963
- }
880
+ return { networkId, ownerDid, name };
881
+ }
882
+ function buildNetworkId(ownerDid, name) {
883
+ if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
884
+ throw new NetworkIdError("ownerDid must be a DID");
964
885
  }
965
- // ============================================================
966
- // Lifecycle
967
- // ============================================================
968
- /**
969
- * Get the abort signal for cancelling operations.
970
- */
971
- get abortSignal() {
972
- return this._abortController.signal;
886
+ if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
887
+ throw new NetworkIdError(
888
+ `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
889
+ );
973
890
  }
974
- get operationAbortSignal() {
975
- return this._operationAbortSignal;
976
- }
977
- setOperationAbortSignal(signal) {
978
- this._operationAbortSignal = signal;
979
- }
980
- /**
981
- * Permanently retire this graph after its owner installs a replacement.
982
- * Unlike `abort()`, retirement never creates a fresh controller, so captured
983
- * services cannot resume calls under a superseded session.
984
- */
985
- retire() {
986
- if (this._retired) return;
987
- this._retired = true;
988
- this._session = null;
989
- this._abortController.abort();
990
- for (const service of this._services.values()) {
991
- try {
992
- service.onSignOut();
993
- } catch (error) {
994
- console.error("Error retiring service:", error);
995
- }
996
- }
997
- }
998
- /**
999
- * Abort all pending operations and notify services.
1000
- * Creates a new AbortController for future operations.
1001
- */
1002
- abort() {
1003
- if (this._retired) return;
1004
- this._abortController.abort();
1005
- this._abortController = new AbortController();
1006
- for (const service of this._services.values()) {
1007
- try {
1008
- service.onSignOut();
1009
- } catch (error) {
1010
- console.error("Error signing out service:", error);
1011
- }
1012
- }
1013
- }
1014
- /**
1015
- * Sign out - abort operations and clear session.
1016
- */
1017
- signOut() {
1018
- if (this._retired) return;
1019
- this.abort();
1020
- this.setSession(null);
1021
- this.emit("session.expired", {});
1022
- }
1023
- // ============================================================
1024
- // Retry Policy
1025
- // ============================================================
1026
- /**
1027
- * Get the retry policy configuration.
1028
- */
1029
- get retryPolicy() {
1030
- return this._retryPolicy;
1031
- }
1032
- wrapInvoke(invoke) {
1033
- return (session, service, path, action, facts) => {
1034
- this.assertActive();
1035
- if (!tinyCloudDebugLogger.isEnabled()) {
1036
- return invoke(session, service, path, action, facts);
1037
- }
1038
- const timer = tinyCloudDebugLogger.startTimer("sdk.invoke", {
1039
- service,
1040
- path,
1041
- action,
1042
- facts
1043
- });
1044
- try {
1045
- const result = invoke(session, service, path, action, facts);
1046
- timer.stop({ ok: true, service, path, action });
1047
- return result;
1048
- } catch (error) {
1049
- timer.stop({ ok: false, service, path, action, error });
1050
- throw error;
1051
- }
1052
- };
1053
- }
1054
- assertActive() {
1055
- if (this._retired) {
1056
- throw new Error("Service graph has been retired: its session was replaced or signed out.");
1057
- }
1058
- }
1059
- wrapInvokeAny(invokeAny) {
1060
- return (session, entries, facts) => {
1061
- this.assertActive();
1062
- if (!tinyCloudDebugLogger.isEnabled()) {
1063
- return invokeAny(session, entries, facts);
1064
- }
1065
- const timer = tinyCloudDebugLogger.startTimer("sdk.invokeAny", {
1066
- entries,
1067
- facts
1068
- });
1069
- try {
1070
- const result = invokeAny(session, entries, facts);
1071
- timer.stop({ ok: true, entries });
1072
- return result;
1073
- } catch (error) {
1074
- timer.stop({ ok: false, entries, error });
1075
- throw error;
1076
- }
1077
- };
1078
- }
1079
- wrapFetch(fetchFn) {
1080
- return async (url, init) => {
1081
- this.assertActive();
1082
- const request = {
1083
- ...init,
1084
- signal: this.combineSignals(init?.signal)
1085
- };
1086
- if (!tinyCloudDebugLogger.isEnabled()) {
1087
- return fetchFn(url, request);
1088
- }
1089
- const timer = tinyCloudDebugLogger.startTimer("sdk.fetch", {
1090
- url,
1091
- method: request.method ?? "GET",
1092
- init: request
1093
- });
1094
- try {
1095
- const response = await fetchFn(url, request);
1096
- timer.stop({
1097
- ok: response.ok,
1098
- url,
1099
- method: request.method ?? "GET",
1100
- status: response.status,
1101
- statusText: response.statusText
1102
- });
1103
- return response;
1104
- } catch (error) {
1105
- timer.stop({
1106
- ok: false,
1107
- url,
1108
- method: request.method ?? "GET",
1109
- error
1110
- });
1111
- throw error;
1112
- }
1113
- };
1114
- }
1115
- combineSignals(signal) {
1116
- if (!signal) return this._abortController.signal;
1117
- const combined = new AbortController();
1118
- const abort = (source) => combined.abort(source.reason);
1119
- if (signal.aborted) {
1120
- abort(signal);
1121
- } else if (this._abortController.signal.aborted) {
1122
- abort(this._abortController.signal);
1123
- } else {
1124
- signal.addEventListener("abort", () => abort(signal), { once: true });
1125
- this._abortController.signal.addEventListener(
1126
- "abort",
1127
- () => abort(this._abortController.signal),
1128
- { once: true }
1129
- );
1130
- }
1131
- return combined.signal;
1132
- }
1133
- };
1134
-
1135
- // src/responseErrors.ts
1136
- function parseServiceErrorBody(errorText) {
1137
- try {
1138
- return JSON.parse(errorText);
1139
- } catch {
1140
- return {};
1141
- }
1142
- }
1143
- function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
1144
- if (parsed.message) {
1145
- return compactMessage(parsed.message);
1146
- }
1147
- if (looksLikeHtml(errorText)) {
1148
- if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
1149
- return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
1150
- }
1151
- return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
1152
- }
1153
- return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
1154
- }
1155
- function responseErrorMeta(status, statusText, errorText) {
1156
- const meta = { status, statusText };
1157
- const snippet = compactMessage(errorText);
1158
- if (snippet) {
1159
- meta.responseSnippet = snippet.slice(0, 300);
1160
- }
1161
- return meta;
1162
- }
1163
- function looksLikeHtml(text) {
1164
- return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
1165
- }
1166
- function compactMessage(message) {
1167
- const compact = message.replace(/\s+/g, " ").trim();
1168
- return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
1169
- }
1170
-
1171
- // src/errors.ts
1172
- var import_bootstrap = require("@tinycloud/bootstrap");
1173
-
1174
- // src/encryption/networkId.ts
1175
- var URN_PREFIX = "urn:tinycloud:encryption:";
1176
- var NETWORK_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
1177
- var PKH_EIP155_DID_RE = /^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/;
1178
- var NetworkIdError = class extends Error {
1179
- constructor(message) {
1180
- super(message);
1181
- this.name = "NetworkIdError";
1182
- }
1183
- };
1184
- function parseNetworkId(networkId) {
1185
- if (typeof networkId !== "string" || networkId.length === 0) {
1186
- throw new NetworkIdError("networkId must be a non-empty string");
1187
- }
1188
- if (!networkId.startsWith(URN_PREFIX)) {
1189
- throw new NetworkIdError(
1190
- `networkId must start with ${URN_PREFIX} (got ${JSON.stringify(networkId)})`
1191
- );
1192
- }
1193
- const body = networkId.slice(URN_PREFIX.length);
1194
- const lastColon = body.lastIndexOf(":");
1195
- if (lastColon <= 0 || lastColon === body.length - 1) {
1196
- throw new NetworkIdError(
1197
- `networkId missing ownerDid or name segment (got ${JSON.stringify(networkId)})`
1198
- );
1199
- }
1200
- const ownerDid = body.slice(0, lastColon);
1201
- const name = body.slice(lastColon + 1);
1202
- if (!ownerDid.startsWith("did:")) {
1203
- throw new NetworkIdError(
1204
- `networkId ownerDid must be a DID (got ${JSON.stringify(ownerDid)})`
1205
- );
1206
- }
1207
- const didParts = ownerDid.split(":");
1208
- if (didParts.length < 3 || didParts.some((p) => p.length === 0)) {
1209
- throw new NetworkIdError(
1210
- `networkId ownerDid is not a well-formed DID (got ${JSON.stringify(ownerDid)})`
1211
- );
1212
- }
1213
- if (!NETWORK_NAME_RE.test(name)) {
1214
- throw new NetworkIdError(
1215
- `networkId name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1216
- );
1217
- }
1218
- return { networkId, ownerDid, name };
1219
- }
1220
- function buildNetworkId(ownerDid, name) {
1221
- if (typeof ownerDid !== "string" || !ownerDid.startsWith("did:")) {
1222
- throw new NetworkIdError("ownerDid must be a DID");
1223
- }
1224
- if (typeof name !== "string" || !NETWORK_NAME_RE.test(name)) {
1225
- throw new NetworkIdError(
1226
- `network name ${JSON.stringify(name)} must match ${NETWORK_NAME_RE.source}`
1227
- );
1228
- }
1229
- const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
1230
- parseNetworkId(networkId);
1231
- return networkId;
1232
- }
1233
- function isNetworkId(networkId) {
1234
- if (typeof networkId !== "string") {
1235
- return false;
891
+ const networkId = `${URN_PREFIX}${ownerDid}:${name}`;
892
+ parseNetworkId(networkId);
893
+ return networkId;
894
+ }
895
+ function isNetworkId(networkId) {
896
+ if (typeof networkId !== "string") {
897
+ return false;
1236
898
  }
1237
899
  try {
1238
900
  parseNetworkId(networkId);
@@ -1331,7 +993,7 @@ function nodeRetryError(service, message, response, errorText) {
1331
993
  const body = errorText.trim();
1332
994
  const [, token, stage] = /^([a-z_]+)(?::\s*([a-z_]+):)?/.exec(body) ?? [];
1333
995
  let code;
1334
- if (response.status === 503 && (token === "sql_busy" || token === "unavailable_retryable")) {
996
+ if (response.status === 503 && (token === "sql_busy" || token === "unavailable_retryable" || token === "read_audit_busy")) {
1335
997
  code = ErrorCodes.NODE_BUSY;
1336
998
  } else if (response.status === 503 && token === "request_timeout") {
1337
999
  code = ErrorCodes.REQUEST_TIMEOUT;
@@ -1549,56 +1211,442 @@ function parseStorageRejection(responseText) {
1549
1211
  } catch {
1550
1212
  return parseStorageQuotaBytes(responseText);
1551
1213
  }
1552
- if (typeof body !== "object" || body === null) return void 0;
1553
- const record = body;
1554
- const message = typeof record.message === "string" ? record.message : "";
1555
- const space = byteCountsOf(record.space) ?? parseStorageQuotaBytes(message);
1556
- const isStorageBody = typeof record.error === "string" && STORAGE_REJECTION_BODY_ERRORS[record.error] === true;
1557
- if (!space && !isStorageBody) {
1558
- return void 0;
1214
+ if (typeof body !== "object" || body === null) return void 0;
1215
+ const record = body;
1216
+ const message = typeof record.message === "string" ? record.message : "";
1217
+ const space = byteCountsOf(record.space) ?? parseStorageQuotaBytes(message);
1218
+ const isStorageBody = typeof record.error === "string" && STORAGE_REJECTION_BODY_ERRORS[record.error] === true;
1219
+ if (!space && !isStorageBody) {
1220
+ return void 0;
1221
+ }
1222
+ const details = { ...space };
1223
+ const account = byteCountsOf(record.account);
1224
+ if (account) {
1225
+ const plan = record.account.plan;
1226
+ details.account = typeof plan === "string" && plan !== "" ? { ...account, plan } : account;
1227
+ }
1228
+ return details;
1229
+ }
1230
+ function storageRejectionError(service, status, meta, responseText) {
1231
+ const quotaMeta = { ...meta, ...parseStorageRejection(responseText) };
1232
+ return status === 402 ? storageQuotaExceededError(service, STORAGE_FULL_MESSAGE, quotaMeta) : storageLimitReachedError(service, STORAGE_WRITE_TOO_LARGE_MESSAGE, quotaMeta);
1233
+ }
1234
+ function isStorageFullError(error) {
1235
+ return error?.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error?.code === ErrorCodes.STORAGE_LIMIT_REACHED;
1236
+ }
1237
+ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
1238
+ if (error instanceof Error) {
1239
+ if (isRequestTimeoutError(error)) {
1240
+ return timeoutError(service, error.timeoutMs, error);
1241
+ }
1242
+ if (error.name === "AbortError") {
1243
+ return abortedError(service);
1244
+ }
1245
+ if (error.name === "TimeoutError" || error.message.toLowerCase().includes("timeout")) {
1246
+ return timeoutError(service);
1247
+ }
1248
+ return {
1249
+ code: defaultCode,
1250
+ message: error.message,
1251
+ service,
1252
+ cause: error
1253
+ };
1254
+ }
1255
+ return {
1256
+ code: defaultCode,
1257
+ message: String(error),
1258
+ service
1259
+ };
1260
+ }
1261
+ function errorResult(error) {
1262
+ return err(error);
1263
+ }
1264
+
1265
+ // src/sleep.ts
1266
+ var abortableSleep = (ms, signal, unref = false) => new Promise((resolve) => {
1267
+ const done = () => {
1268
+ clearTimeout(timer);
1269
+ signal.removeEventListener("abort", done);
1270
+ resolve();
1271
+ };
1272
+ const timer = setTimeout(done, ms);
1273
+ if (unref) timer.unref?.();
1274
+ signal.aborted ? done() : signal.addEventListener("abort", done);
1275
+ });
1276
+
1277
+ // src/context.ts
1278
+ var ServiceContext = class {
1279
+ constructor(config) {
1280
+ this._session = null;
1281
+ this._services = /* @__PURE__ */ new Map();
1282
+ this._eventHandlers = /* @__PURE__ */ new Map();
1283
+ this._abortController = new AbortController();
1284
+ this._retired = false;
1285
+ this._invoke = this.wrapInvoke(config.invoke);
1286
+ this._invokeAny = config.invokeAny ? this.wrapInvokeAny(config.invokeAny) : void 0;
1287
+ this._fetch = this.wrapFetch(config.fetch ?? globalThis.fetch.bind(globalThis));
1288
+ this._hosts = config.hosts;
1289
+ this._session = config.session ?? null;
1290
+ this._retryPolicy = {
1291
+ ...defaultRetryPolicy,
1292
+ ...config.retryPolicy
1293
+ };
1294
+ this._telemetryEnabled = typeof config.telemetry === "boolean" ? config.telemetry : config.telemetry?.enabled === true;
1295
+ this._telemetryHandler = typeof config.telemetry === "object" ? config.telemetry.onEvent : void 0;
1296
+ }
1297
+ // ============================================================
1298
+ // Session Management
1299
+ // ============================================================
1300
+ /**
1301
+ * Get the current session.
1302
+ */
1303
+ get session() {
1304
+ return this._retired ? null : this._session;
1305
+ }
1306
+ /**
1307
+ * Check if the context has an authenticated session.
1308
+ */
1309
+ get isAuthenticated() {
1310
+ return !this._retired && this._session !== null;
1311
+ }
1312
+ /**
1313
+ * Update the session and notify all registered services.
1314
+ *
1315
+ * @param session - New session or null to clear
1316
+ */
1317
+ setSession(session) {
1318
+ this.assertActive();
1319
+ this._session = session;
1320
+ this.emit("session.changed", { authenticated: session !== null });
1321
+ for (const service of this._services.values()) {
1322
+ service.onSessionChange(session);
1323
+ }
1324
+ }
1325
+ // ============================================================
1326
+ // Platform Dependencies
1327
+ // ============================================================
1328
+ /**
1329
+ * Get the invoke function for WASM operations.
1330
+ */
1331
+ get invoke() {
1332
+ this.assertActive();
1333
+ return this._invoke;
1334
+ }
1335
+ /**
1336
+ * Get the multi-resource invoke function when available.
1337
+ */
1338
+ get invokeAny() {
1339
+ this.assertActive();
1340
+ return this._invokeAny;
1341
+ }
1342
+ /**
1343
+ * Get the fetch function for HTTP requests.
1344
+ */
1345
+ get fetch() {
1346
+ this.assertActive();
1347
+ return this._fetch;
1348
+ }
1349
+ /**
1350
+ * Get the list of TinyCloud host URLs.
1351
+ */
1352
+ get hosts() {
1353
+ this.assertActive();
1354
+ return this._hosts;
1355
+ }
1356
+ // ============================================================
1357
+ // Service Registry
1358
+ // ============================================================
1359
+ /**
1360
+ * Register a service with the context.
1361
+ *
1362
+ * @param name - Service name (e.g., 'kv')
1363
+ * @param service - Service instance
1364
+ */
1365
+ registerService(name, service) {
1366
+ this._services.set(name, service);
1367
+ }
1368
+ /**
1369
+ * Unregister a service from the context.
1370
+ *
1371
+ * @param name - Service name to remove
1372
+ */
1373
+ unregisterService(name) {
1374
+ this._services.delete(name);
1375
+ }
1376
+ /**
1377
+ * Get a registered service by name.
1378
+ *
1379
+ * @param name - Service name
1380
+ * @returns The service instance or undefined if not registered
1381
+ */
1382
+ getService(name) {
1383
+ return this._services.get(name);
1384
+ }
1385
+ // ============================================================
1386
+ // Event System (Telemetry)
1387
+ // ============================================================
1388
+ /**
1389
+ * Emit a telemetry event.
1390
+ *
1391
+ * @param event - Event name
1392
+ * @param data - Event data
1393
+ */
1394
+ emit(event, data) {
1395
+ tinyCloudDebugLogger.log(event, data);
1396
+ if (this._telemetryEnabled && this._telemetryHandler) {
1397
+ try {
1398
+ this._telemetryHandler(event, projectDiagnosticData(data));
1399
+ } catch (error) {
1400
+ console.error(`Error in telemetry handler for "${event}":`, error);
1401
+ }
1402
+ }
1403
+ const handlers = this._eventHandlers.get(event);
1404
+ if (handlers) {
1405
+ for (const handler of handlers) {
1406
+ try {
1407
+ handler(data);
1408
+ } catch (error) {
1409
+ console.error(`Error in event handler for "${event}":`, error);
1410
+ }
1411
+ }
1412
+ }
1413
+ }
1414
+ /**
1415
+ * Subscribe to telemetry events.
1416
+ *
1417
+ * @param event - Event name to subscribe to
1418
+ * @param handler - Handler function
1419
+ * @returns Unsubscribe function
1420
+ */
1421
+ on(event, handler) {
1422
+ if (!this._eventHandlers.has(event)) {
1423
+ this._eventHandlers.set(event, /* @__PURE__ */ new Set());
1424
+ }
1425
+ this._eventHandlers.get(event).add(handler);
1426
+ return () => {
1427
+ const handlers = this._eventHandlers.get(event);
1428
+ if (handlers) {
1429
+ handlers.delete(handler);
1430
+ if (handlers.size === 0) {
1431
+ this._eventHandlers.delete(event);
1432
+ }
1433
+ }
1434
+ };
1435
+ }
1436
+ /**
1437
+ * Remove all event handlers for an event.
1438
+ *
1439
+ * @param event - Event name (if omitted, clears all events)
1440
+ */
1441
+ clearEventHandlers(event) {
1442
+ if (event) {
1443
+ this._eventHandlers.delete(event);
1444
+ } else {
1445
+ this._eventHandlers.clear();
1446
+ }
1447
+ }
1448
+ // ============================================================
1449
+ // Lifecycle
1450
+ // ============================================================
1451
+ /**
1452
+ * Get the abort signal for cancelling operations.
1453
+ */
1454
+ get abortSignal() {
1455
+ return this._abortController.signal;
1456
+ }
1457
+ get operationAbortSignal() {
1458
+ return this._operationAbortSignal;
1459
+ }
1460
+ setOperationAbortSignal(signal) {
1461
+ this._operationAbortSignal = signal;
1462
+ }
1463
+ /**
1464
+ * Permanently retire this graph after its owner installs a replacement.
1465
+ * Unlike `abort()`, retirement never creates a fresh controller, so captured
1466
+ * services cannot resume calls under a superseded session.
1467
+ */
1468
+ retire() {
1469
+ if (this._retired) return;
1470
+ this._retired = true;
1471
+ this._session = null;
1472
+ this._abortController.abort();
1473
+ for (const service of this._services.values()) {
1474
+ try {
1475
+ service.onSignOut();
1476
+ } catch (error) {
1477
+ console.error("Error retiring service:", error);
1478
+ }
1479
+ }
1480
+ }
1481
+ /**
1482
+ * Abort all pending operations and notify services.
1483
+ * Creates a new AbortController for future operations.
1484
+ */
1485
+ abort() {
1486
+ if (this._retired) return;
1487
+ this._abortController.abort();
1488
+ this._abortController = new AbortController();
1489
+ for (const service of this._services.values()) {
1490
+ try {
1491
+ service.onSignOut();
1492
+ } catch (error) {
1493
+ console.error("Error signing out service:", error);
1494
+ }
1495
+ }
1496
+ }
1497
+ /**
1498
+ * Sign out - abort operations and clear session.
1499
+ */
1500
+ signOut() {
1501
+ if (this._retired) return;
1502
+ this.abort();
1503
+ this.setSession(null);
1504
+ this.emit("session.expired", {});
1559
1505
  }
1560
- const details = { ...space };
1561
- const account = byteCountsOf(record.account);
1562
- if (account) {
1563
- const plan = record.account.plan;
1564
- details.account = typeof plan === "string" && plan !== "" ? { ...account, plan } : account;
1506
+ // ============================================================
1507
+ // Retry Policy
1508
+ // ============================================================
1509
+ /**
1510
+ * Get the retry policy configuration.
1511
+ */
1512
+ get retryPolicy() {
1513
+ return this._retryPolicy;
1565
1514
  }
1566
- return details;
1567
- }
1568
- function storageRejectionError(service, status, meta, responseText) {
1569
- const quotaMeta = { ...meta, ...parseStorageRejection(responseText) };
1570
- return status === 402 ? storageQuotaExceededError(service, STORAGE_FULL_MESSAGE, quotaMeta) : storageLimitReachedError(service, STORAGE_WRITE_TOO_LARGE_MESSAGE, quotaMeta);
1571
- }
1572
- function isStorageFullError(error) {
1573
- return error?.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error?.code === ErrorCodes.STORAGE_LIMIT_REACHED;
1574
- }
1575
- function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
1576
- if (error instanceof Error) {
1577
- if (isRequestTimeoutError(error)) {
1578
- return timeoutError(service, error.timeoutMs, error);
1579
- }
1580
- if (error.name === "AbortError") {
1581
- return abortedError(service);
1515
+ wrapInvoke(invoke) {
1516
+ return (session, service, path, action, facts) => {
1517
+ this.assertActive();
1518
+ if (!tinyCloudDebugLogger.isEnabled()) {
1519
+ return invoke(session, service, path, action, facts);
1520
+ }
1521
+ const timer = tinyCloudDebugLogger.startTimer("sdk.invoke", {
1522
+ service,
1523
+ path,
1524
+ action,
1525
+ facts
1526
+ });
1527
+ try {
1528
+ const result = invoke(session, service, path, action, facts);
1529
+ timer.stop({ ok: true, service, path, action });
1530
+ return result;
1531
+ } catch (error) {
1532
+ timer.stop({ ok: false, service, path, action, error });
1533
+ throw error;
1534
+ }
1535
+ };
1536
+ }
1537
+ assertActive() {
1538
+ if (this._retired) {
1539
+ throw new Error("Service graph has been retired: its session was replaced or signed out.");
1582
1540
  }
1583
- if (error.name === "TimeoutError" || error.message.toLowerCase().includes("timeout")) {
1584
- return timeoutError(service);
1541
+ }
1542
+ wrapInvokeAny(invokeAny) {
1543
+ return (session, entries, facts) => {
1544
+ this.assertActive();
1545
+ if (!tinyCloudDebugLogger.isEnabled()) {
1546
+ return invokeAny(session, entries, facts);
1547
+ }
1548
+ const timer = tinyCloudDebugLogger.startTimer("sdk.invokeAny", {
1549
+ entries,
1550
+ facts
1551
+ });
1552
+ try {
1553
+ const result = invokeAny(session, entries, facts);
1554
+ timer.stop({ ok: true, entries });
1555
+ return result;
1556
+ } catch (error) {
1557
+ timer.stop({ ok: false, entries, error });
1558
+ throw error;
1559
+ }
1560
+ };
1561
+ }
1562
+ /** Retry `NODE_BUSY` / `REQUEST_TIMEOUT`; `build` must re-sign per attempt (the node 409s a repeated invocation). */
1563
+ async invokeWithRetry(url, build) {
1564
+ this.assertActive();
1565
+ const policy = this._retryPolicy;
1566
+ const startedAt = Date.now();
1567
+ let backoffMs = 0;
1568
+ let signal;
1569
+ for (let attempt = 1; ; attempt++) {
1570
+ const init = await build();
1571
+ signal ?? (signal = this.combineSignals(init.signal));
1572
+ const response = await this._fetch(url, init);
1573
+ const done = () => {
1574
+ try {
1575
+ response.retry = { attempts: attempt, backoffMs };
1576
+ } catch {
1577
+ }
1578
+ return response;
1579
+ };
1580
+ if (attempt >= policy.maxAttempts || response.status !== 503 || !response.clone) return done();
1581
+ const text = await response.clone().text().catch(() => "");
1582
+ const error = nodeRetryError("", "", response, text);
1583
+ if (!error || error.meta?.retryable !== true || !policy.retryableErrors.includes(error.code)) {
1584
+ return done();
1585
+ }
1586
+ const remaining = (policy.maxTotalMs ?? Infinity) - (Date.now() - startedAt);
1587
+ if (remaining <= 0) return done();
1588
+ const retryAfterMs = error.meta.retryAfterMs;
1589
+ const step = Math.min(
1590
+ policy.maxDelayMs,
1591
+ policy.backoff === "exponential" ? policy.baseDelayMs * 2 ** (attempt - 1) : policy.backoff === "linear" ? policy.baseDelayMs * attempt : 0
1592
+ );
1593
+ const delay = Math.min(
1594
+ typeof retryAfterMs === "number" ? retryAfterMs : Math.random() * step,
1595
+ remaining
1596
+ );
1597
+ response.body?.cancel?.()?.catch?.(() => {
1598
+ });
1599
+ await abortableSleep(delay, signal);
1600
+ if (signal.aborted) throw signal.reason;
1601
+ backoffMs += delay;
1585
1602
  }
1586
- return {
1587
- code: defaultCode,
1588
- message: error.message,
1589
- service,
1590
- cause: error
1603
+ }
1604
+ wrapFetch(fetchFn) {
1605
+ return async (url, init) => {
1606
+ this.assertActive();
1607
+ const request = {
1608
+ ...init,
1609
+ signal: this.combineSignals(init?.signal)
1610
+ };
1611
+ if (!tinyCloudDebugLogger.isEnabled()) {
1612
+ return fetchFn(url, request);
1613
+ }
1614
+ const meta = { url, method: request.method ?? "GET" };
1615
+ const timer = tinyCloudDebugLogger.startTimer("sdk.fetch", { ...meta, init: request });
1616
+ try {
1617
+ const response = await fetchFn(url, request);
1618
+ timer.stop({
1619
+ ...meta,
1620
+ ok: response.ok,
1621
+ status: response.status,
1622
+ statusText: response.statusText
1623
+ });
1624
+ return response;
1625
+ } catch (error) {
1626
+ timer.stop({ ...meta, ok: false, error });
1627
+ throw error;
1628
+ }
1591
1629
  };
1592
1630
  }
1593
- return {
1594
- code: defaultCode,
1595
- message: String(error),
1596
- service
1597
- };
1598
- }
1599
- function errorResult(error) {
1600
- return err(error);
1601
- }
1631
+ combineSignals(signal) {
1632
+ if (!signal) return this._abortController.signal;
1633
+ const combined = new AbortController();
1634
+ const abort = (source) => combined.abort(source.reason);
1635
+ if (signal.aborted) {
1636
+ abort(signal);
1637
+ } else if (this._abortController.signal.aborted) {
1638
+ abort(this._abortController.signal);
1639
+ } else {
1640
+ signal.addEventListener("abort", () => abort(signal), { once: true });
1641
+ this._abortController.signal.addEventListener(
1642
+ "abort",
1643
+ () => abort(this._abortController.signal),
1644
+ { once: true }
1645
+ );
1646
+ }
1647
+ return combined.signal;
1648
+ }
1649
+ };
1602
1650
 
1603
1651
  // src/base/BaseService.ts
1604
1652
  var MAX_TIMER_DELAY_MS = 2147483647;
@@ -1782,6 +1830,18 @@ var BaseService = class {
1782
1830
  }
1783
1831
  return controller.signal;
1784
1832
  }
1833
+ /** POST a signed invocation with retry; `mint` re-signs per attempt, `init.headers` add to the signed ones. */
1834
+ invokeWithRetry(url, mint, { headers: extra, ...init } = {}) {
1835
+ const build = () => {
1836
+ const signed = mint();
1837
+ return {
1838
+ method: "POST",
1839
+ ...init,
1840
+ headers: Array.isArray(signed) ? [...signed, ...Object.entries(extra ?? {})] : { ...signed, ...extra }
1841
+ };
1842
+ };
1843
+ return this.context.invokeWithRetry ? this.context.invokeWithRetry(url, build) : this.context.fetch(url, build());
1844
+ }
1785
1845
  /**
1786
1846
  * Create the abort signal for one request, bounded by the request timeout.
1787
1847
  *
@@ -2149,15 +2209,6 @@ var KVService = class extends BaseService {
2149
2209
  get host() {
2150
2210
  return this.context.hosts[0];
2151
2211
  }
2152
- withJsonContentType(headers) {
2153
- if (Array.isArray(headers)) {
2154
- return [...headers, ["content-type", "application/json"]];
2155
- }
2156
- return {
2157
- ...headers,
2158
- "content-type": "application/json"
2159
- };
2160
- }
2161
2212
  /**
2162
2213
  * Execute an invoke operation.
2163
2214
  *
@@ -2168,20 +2219,11 @@ var KVService = class extends BaseService {
2168
2219
  * @returns Fetch response
2169
2220
  */
2170
2221
  async invokeOperation(path, action, body, signal, extraHeaders) {
2171
- const session = this.context.session;
2172
- const headers = this.context.invoke(
2173
- session,
2174
- "kv",
2175
- path,
2176
- action
2222
+ return this.invokeWithRetry(
2223
+ `${this.host}/invoke`,
2224
+ () => this.context.invoke(this.context.session, "kv", path, action),
2225
+ { headers: extraHeaders, body, signal }
2177
2226
  );
2178
- const requestHeaders = Array.isArray(headers) ? [...headers, ...Object.entries(extraHeaders ?? {})] : { ...headers, ...extraHeaders };
2179
- return this.context.fetch(`${this.host}/invoke`, {
2180
- method: "POST",
2181
- headers: requestHeaders,
2182
- body,
2183
- signal
2184
- });
2185
2227
  }
2186
2228
  /**
2187
2229
  * Serialize a single put value into a fetch body.
@@ -2387,28 +2429,28 @@ var KVService = class extends BaseService {
2387
2429
  }
2388
2430
  const request = this.createRequestSignal(options?.signal, options?.timeout);
2389
2431
  try {
2390
- const session = this.context.session;
2391
- const invocationHeaders = this.context.invokeAny(
2392
- session,
2393
- paths.map((path) => ({
2394
- spaceId: session.spaceId,
2395
- service: "kv",
2396
- path,
2397
- action
2398
- }))
2399
- );
2400
2432
  const limitHeaders = {};
2401
2433
  if (getOptions?.maxResponseBytes !== void 0) {
2402
2434
  limitHeaders["x-tinycloud-max-response-bytes"] = String(
2403
2435
  getOptions.maxResponseBytes
2404
2436
  );
2405
2437
  }
2406
- const headers = Array.isArray(invocationHeaders) ? [...invocationHeaders, ...Object.entries(limitHeaders)] : { ...invocationHeaders, ...limitHeaders };
2407
- const response = await this.context.fetch(`${this.host}/invoke`, {
2408
- method: "POST",
2409
- headers,
2410
- signal: request.signal
2411
- });
2438
+ const response = await this.invokeWithRetry(
2439
+ `${this.host}/invoke`,
2440
+ () => {
2441
+ const session = this.context.session;
2442
+ return this.context.invokeAny(
2443
+ session,
2444
+ paths.map((path) => ({
2445
+ spaceId: session.spaceId,
2446
+ service: "kv",
2447
+ path,
2448
+ action
2449
+ }))
2450
+ );
2451
+ },
2452
+ { headers: limitHeaders, signal: request.signal }
2453
+ );
2412
2454
  if (!response.ok) {
2413
2455
  const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
2414
2456
  if (response.status === 401 || response.status === 403) {
@@ -2454,10 +2496,10 @@ var KVService = class extends BaseService {
2454
2496
  result: err(serviceError(item.error.code, item.error.message, "kv"))
2455
2497
  };
2456
2498
  }
2457
- const headers2 = item.headers;
2499
+ const headers = item.headers;
2458
2500
  const data = action === KVAction.HEAD ? void 0 : this.parseBatchValue(
2459
2501
  item.dataBase64,
2460
- headers2,
2502
+ headers,
2461
2503
  getOptions?.raw,
2462
2504
  getOptions?.binary
2463
2505
  );
@@ -2465,7 +2507,7 @@ var KVService = class extends BaseService {
2465
2507
  key: keys[index],
2466
2508
  result: ok({
2467
2509
  data,
2468
- headers: this.createBatchResponseHeaders(headers2)
2510
+ headers: this.createBatchResponseHeaders(headers)
2469
2511
  })
2470
2512
  };
2471
2513
  });
@@ -2700,7 +2742,7 @@ var KVService = class extends BaseService {
2700
2742
  ));
2701
2743
  }
2702
2744
  const data = await this.parseResponse(response, options?.raw, options?.binary);
2703
- return ok({ data, headers: this.createResponseHeaders(response.headers) });
2745
+ return ok({ data, headers: this.createResponseHeaders(response.headers), ...response.retry && { retry: response.retry } });
2704
2746
  }
2705
2747
  async batchGet(keys, options) {
2706
2748
  return this.withTelemetry(
@@ -2790,7 +2832,8 @@ var KVService = class extends BaseService {
2790
2832
  }
2791
2833
  return ok({
2792
2834
  data: void 0,
2793
- headers: this.createResponseHeaders(response.headers)
2835
+ headers: this.createResponseHeaders(response.headers),
2836
+ ...response.retry && { retry: response.retry }
2794
2837
  });
2795
2838
  }
2796
2839
  /**
@@ -2863,25 +2906,23 @@ var KVService = class extends BaseService {
2863
2906
  entries?.[index]?.body ?? this.serializeBatchPutValue(items[index])
2864
2907
  );
2865
2908
  }
2866
- const headers = this.context.invokeAny(
2867
- session,
2868
- paths.map((path) => ({
2869
- spaceId: session.spaceId,
2870
- service: "kv",
2871
- path,
2872
- action: KVAction.PUT
2873
- }))
2909
+ const response = await this.invokeWithRetry(
2910
+ `${this.host}/invoke`,
2911
+ () => {
2912
+ const headers = this.context.invokeAny(
2913
+ session,
2914
+ paths.map((path) => ({
2915
+ spaceId: session.spaceId,
2916
+ service: "kv",
2917
+ path,
2918
+ action: KVAction.PUT
2919
+ }))
2920
+ );
2921
+ requestMayHaveDispatched = true;
2922
+ return headers;
2923
+ },
2924
+ { body, signal }
2874
2925
  );
2875
- const url = `${this.host}/invoke`;
2876
- const init = {
2877
- method: "POST",
2878
- headers,
2879
- body,
2880
- signal
2881
- };
2882
- const fetchFn = this.context.fetch;
2883
- requestMayHaveDispatched = true;
2884
- const response = await fetchFn(url, init);
2885
2926
  if (!response.ok) {
2886
2927
  let errorText;
2887
2928
  try {
@@ -3351,7 +3392,8 @@ var KVService = class extends BaseService {
3351
3392
  }
3352
3393
  return ok({
3353
3394
  data: void 0,
3354
- headers: this.createResponseHeaders(response.headers)
3395
+ headers: this.createResponseHeaders(response.headers),
3396
+ ...response.retry && { retry: response.retry }
3355
3397
  });
3356
3398
  }
3357
3399
  /**
@@ -3425,12 +3467,6 @@ var KVService = class extends BaseService {
3425
3467
  }
3426
3468
  const path = this.getFullPath(key, options?.prefix);
3427
3469
  const session = this.context.session;
3428
- const headers = this.context.invoke(
3429
- session,
3430
- "kv",
3431
- path,
3432
- KVAction.GET
3433
- );
3434
3470
  const body = {
3435
3471
  space: session.spaceId,
3436
3472
  path,
@@ -3444,12 +3480,15 @@ var KVService = class extends BaseService {
3444
3480
  }
3445
3481
  const request = this.createRequestSignal(options?.signal, options?.timeout);
3446
3482
  try {
3447
- const response = await this.context.fetch(`${this.host}/signed/kv`, {
3448
- method: "POST",
3449
- headers: this.withJsonContentType(headers),
3450
- body: JSON.stringify(body),
3451
- signal: request.signal
3452
- });
3483
+ const response = await this.invokeWithRetry(
3484
+ `${this.host}/signed/kv`,
3485
+ () => this.context.invoke(session, "kv", path, KVAction.GET),
3486
+ {
3487
+ headers: { "content-type": "application/json" },
3488
+ body: JSON.stringify(body),
3489
+ signal: request.signal
3490
+ }
3491
+ );
3453
3492
  if (!response.ok) {
3454
3493
  return await this.createSignedReadUrlError(response, key, path);
3455
3494
  }
@@ -3946,18 +3985,19 @@ var SQLService = class extends BaseService {
3946
3985
  }
3947
3986
  // === Private helpers ===
3948
3987
  async invokeSQL(dbName, actions, body, signal) {
3949
- const session = this.context.session;
3950
3988
  const actionList = Array.isArray(actions) ? actions : [actions];
3951
- const headers = actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
3952
- return this.context.fetch(`${this.host}/invoke`, {
3953
- method: "POST",
3954
- headers: {
3955
- ...headers,
3956
- "Content-Type": "application/json"
3989
+ return this.invokeWithRetry(
3990
+ `${this.host}/invoke`,
3991
+ () => {
3992
+ const session = this.context.session;
3993
+ return actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
3957
3994
  },
3958
- body: JSON.stringify(body),
3959
- signal
3960
- });
3995
+ {
3996
+ headers: { "Content-Type": "application/json" },
3997
+ body: JSON.stringify(body),
3998
+ signal
3999
+ }
4000
+ );
3961
4001
  }
3962
4002
  actionForSql(sql, fallback) {
3963
4003
  const token = firstSqlToken(sql);
@@ -4418,22 +4458,20 @@ var DuckDbService = class extends BaseService {
4418
4458
  }
4419
4459
  const request = this.createRequestSignal(options?.signal);
4420
4460
  try {
4421
- const session = this.context.session;
4422
- const headers = this.context.invoke(
4423
- session,
4424
- "duckdb",
4425
- dbName,
4426
- DuckDbAction.IMPORT
4461
+ const response = await this.invokeWithRetry(
4462
+ `${this.host}/invoke`,
4463
+ () => this.context.invoke(
4464
+ this.context.session,
4465
+ "duckdb",
4466
+ dbName,
4467
+ DuckDbAction.IMPORT
4468
+ ),
4469
+ {
4470
+ headers: { "Content-Type": "application/x-duckdb" },
4471
+ body: new Blob([data]),
4472
+ signal: request.signal
4473
+ }
4427
4474
  );
4428
- const response = await this.context.fetch(`${this.host}/invoke`, {
4429
- method: "POST",
4430
- headers: {
4431
- ...headers,
4432
- "Content-Type": "application/x-duckdb"
4433
- },
4434
- body: new Blob([data]),
4435
- signal: request.signal
4436
- });
4437
4475
  if (!response.ok) {
4438
4476
  return await this.handleErrorResponse(response, "import");
4439
4477
  }
@@ -4447,18 +4485,15 @@ var DuckDbService = class extends BaseService {
4447
4485
  }
4448
4486
  // === Private helpers ===
4449
4487
  async invokeDuckDb(dbName, action, body, signal, extraHeaders) {
4450
- const session = this.context.session;
4451
- const headers = this.context.invoke(session, "duckdb", dbName, action);
4452
- return this.context.fetch(`${this.host}/invoke`, {
4453
- method: "POST",
4454
- headers: {
4455
- ...headers,
4456
- "Content-Type": "application/json",
4457
- ...extraHeaders
4458
- },
4459
- body: JSON.stringify(body),
4460
- signal
4461
- });
4488
+ return this.invokeWithRetry(
4489
+ `${this.host}/invoke`,
4490
+ () => this.context.invoke(this.context.session, "duckdb", dbName, action),
4491
+ {
4492
+ headers: { "Content-Type": "application/json", ...extraHeaders },
4493
+ body: JSON.stringify(body),
4494
+ signal
4495
+ }
4496
+ );
4462
4497
  }
4463
4498
  async handleErrorResponse(response, operation) {
4464
4499
  const errorText = await response.text();
@@ -4647,26 +4682,24 @@ var HooksService = class extends BaseService {
4647
4682
  );
4648
4683
  }
4649
4684
  try {
4650
- const response = await this.context.fetch(`${this.host}/hooks/webhooks`, {
4651
- method: "POST",
4652
- headers: {
4653
- ...serviceHeadersToRecord(
4654
- this.createHookHeaders(
4655
- "tinycloud.hooks/register",
4656
- buildScopePath(webhook.service, webhook.pathPrefix)
4657
- )
4658
- ),
4659
- "content-type": "application/json"
4660
- },
4661
- body: JSON.stringify({
4662
- space: webhook.space,
4663
- service: webhook.service,
4664
- pathPrefix: normalizePathPrefix(webhook.pathPrefix),
4665
- abilities: webhook.abilities ?? [],
4666
- callbackUrl: webhook.callbackUrl,
4667
- secret: webhook.secret
4668
- })
4669
- });
4685
+ const response = await this.invokeWithRetry(
4686
+ `${this.host}/hooks/webhooks`,
4687
+ () => this.createHookHeaders(
4688
+ "tinycloud.hooks/register",
4689
+ buildScopePath(webhook.service, webhook.pathPrefix)
4690
+ ),
4691
+ {
4692
+ headers: { "content-type": "application/json" },
4693
+ body: JSON.stringify({
4694
+ space: webhook.space,
4695
+ service: webhook.service,
4696
+ pathPrefix: normalizePathPrefix(webhook.pathPrefix),
4697
+ abilities: webhook.abilities ?? [],
4698
+ callbackUrl: webhook.callbackUrl,
4699
+ secret: webhook.secret
4700
+ })
4701
+ }
4702
+ );
4670
4703
  if (!response.ok) {
4671
4704
  return err(
4672
4705
  await responseError("hooks", "failed to register webhook", response)
@@ -4704,17 +4737,13 @@ var HooksService = class extends BaseService {
4704
4737
  query.set("prefix", normalizedPrefix);
4705
4738
  }
4706
4739
  }
4707
- const response = await this.context.fetch(
4740
+ const response = await this.invokeWithRetry(
4708
4741
  `${this.host}/hooks/webhooks${query.size > 0 ? `?${query.toString()}` : ""}`,
4709
- {
4710
- method: "GET",
4711
- headers: serviceHeadersToRecord(
4712
- this.createHookHeaders(
4713
- "tinycloud.hooks/list",
4714
- options.service ? buildScopePath(options.service, options.pathPrefix) : "webhooks"
4715
- )
4716
- )
4717
- }
4742
+ () => this.createHookHeaders(
4743
+ "tinycloud.hooks/list",
4744
+ options.service ? buildScopePath(options.service, options.pathPrefix) : "webhooks"
4745
+ ),
4746
+ { method: "GET" }
4718
4747
  );
4719
4748
  if (!response.ok) {
4720
4749
  return err(
@@ -4741,20 +4770,16 @@ var HooksService = class extends BaseService {
4741
4770
  return err(authRequiredError("hooks"));
4742
4771
  }
4743
4772
  try {
4744
- const response = await this.context.fetch(
4773
+ const response = await this.invokeWithRetry(
4745
4774
  `${this.host}/hooks/webhooks/${encodeURIComponent(id)}`,
4746
- {
4747
- method: "DELETE",
4748
- headers: serviceHeadersToRecord(
4749
- this.createHookHeaders(
4750
- "tinycloud.hooks/unregister",
4751
- options.target ? buildScopePath(
4752
- options.target.service,
4753
- options.target.pathPrefix
4754
- ) : `webhooks/${id}`
4755
- )
4756
- )
4757
- }
4775
+ () => this.createHookHeaders(
4776
+ "tinycloud.hooks/unregister",
4777
+ options.target ? buildScopePath(
4778
+ options.target.service,
4779
+ options.target.pathPrefix
4780
+ ) : `webhooks/${id}`
4781
+ ),
4782
+ { method: "DELETE" }
4758
4783
  );
4759
4784
  if (!response.ok) {
4760
4785
  return err(
@@ -5210,23 +5235,7 @@ function readProperty(value, key) {
5210
5235
  return void 0;
5211
5236
  }
5212
5237
  }
5213
- function sleep(delayMs, _attempt, signal) {
5214
- return new Promise((resolve) => {
5215
- const finish = () => {
5216
- clearTimeout(timer);
5217
- signal.removeEventListener("abort", stop);
5218
- resolve();
5219
- };
5220
- const timer = setTimeout(finish, delayMs);
5221
- timer.unref?.();
5222
- const stop = () => finish();
5223
- if (signal.aborted) {
5224
- finish();
5225
- } else {
5226
- signal.addEventListener("abort", stop, { once: true });
5227
- }
5228
- });
5229
- }
5238
+ var sleep = (delayMs, _attempt, signal) => abortableSleep(delayMs, signal, true);
5230
5239
  function abortPromise(signal) {
5231
5240
  let resolve;
5232
5241
  const promise = new Promise((done) => {
@@ -5611,7 +5620,7 @@ var StorageFullMonitor = class {
5611
5620
  var UNKNOWN_OWNER = "\0unknown";
5612
5621
 
5613
5622
  // src/storage/status.ts
5614
- var STORAGE_MANAGE_URL = "https://account.tinycloud.xyz/billing";
5623
+ var STORAGE_MANAGE_URL = "https://account.tinycloud.xyz/storage";
5615
5624
  var STORAGE_NEARLY_FULL_RATIO = 0.9;
5616
5625
  function storageUsageState(usedBytes, limitBytes) {
5617
5626
  if (limitBytes === null) return "ok";