@tinycloud/sdk-services 3.2.6-beta.4 → 3.3.0-beta.25

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 (45) hide show
  1. package/dist/{BaseService-1Ug6y7DC.d.cts → BaseService-BhfKzKke.d.cts} +5 -1
  2. package/dist/{BaseService-Bn07Vudy.d.ts → BaseService-x4AFZiEa.d.ts} +5 -1
  3. package/dist/encryption/index.cjs +25 -4
  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 +25 -4
  8. package/dist/encryption/index.js.map +1 -1
  9. package/dist/index.cjs +959 -808
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +114 -7
  12. package/dist/index.d.ts +114 -7
  13. package/dist/index.js +958 -808
  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 +136 -99
  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 +136 -99
  22. package/dist/kv/index.js.map +1 -1
  23. package/dist/kv/replication/grantPolicy.cjs +82 -0
  24. package/dist/kv/replication/grantPolicy.cjs.map +1 -0
  25. package/dist/kv/replication/grantPolicy.d.cts +19 -0
  26. package/dist/kv/replication/grantPolicy.d.ts +19 -0
  27. package/dist/kv/replication/grantPolicy.js +50 -0
  28. package/dist/kv/replication/grantPolicy.js.map +1 -0
  29. package/dist/kv/replication/index.cjs +576 -132
  30. package/dist/kv/replication/index.cjs.map +1 -1
  31. package/dist/kv/replication/index.d.cts +3 -3
  32. package/dist/kv/replication/index.d.ts +3 -3
  33. package/dist/kv/replication/index.js +576 -132
  34. package/dist/kv/replication/index.js.map +1 -1
  35. package/dist/{scope-BnyVPZ5E.d.ts → scope-9U4gVNFN.d.ts} +312 -7
  36. package/dist/{scope-CQ5kmHlt.d.cts → scope-BzdcrOZG.d.cts} +312 -7
  37. package/dist/sql/index.cjs +98 -50
  38. package/dist/sql/index.cjs.map +1 -1
  39. package/dist/sql/index.d.cts +2 -2
  40. package/dist/sql/index.d.ts +2 -2
  41. package/dist/sql/index.js +98 -50
  42. package/dist/sql/index.js.map +1 -1
  43. package/dist/{types-gmdUiorz.d.cts → types-eVS1Whuf.d.cts} +29 -3
  44. package/dist/{types-gmdUiorz.d.ts → types-eVS1Whuf.d.ts} +29 -3
  45. package/package.json +16 -2
package/dist/sql/index.js CHANGED
@@ -14,6 +14,14 @@ var ErrorCodes = {
14
14
  ABORTED: "ABORTED",
15
15
  INVALID_INPUT: "INVALID_INPUT",
16
16
  PERMISSION_DENIED: "PERMISSION_DENIED",
17
+ // Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
18
+ // retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
19
+ /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */
20
+ NODE_BUSY: "NODE_BUSY",
21
+ /** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
22
+ REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
23
+ /** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */
24
+ OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN",
17
25
  // KV-specific errors
18
26
  KV_NOT_FOUND: "KV_NOT_FOUND",
19
27
  KV_WRITE_FAILED: "KV_WRITE_FAILED",
@@ -49,11 +57,12 @@ var ErrorCodes = {
49
57
  DUCKDB_READONLY_VIOLATION: "DUCKDB_READONLY_VIOLATION"
50
58
  };
51
59
  var defaultRetryPolicy = {
52
- maxAttempts: 3,
60
+ maxAttempts: 4,
53
61
  backoff: "exponential",
54
- baseDelayMs: 1e3,
55
- maxDelayMs: 1e4,
56
- retryableErrors: [ErrorCodes.NETWORK_ERROR, ErrorCodes.TIMEOUT]
62
+ baseDelayMs: 250,
63
+ maxDelayMs: 4e3,
64
+ retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],
65
+ maxTotalMs: 12e3
57
66
  };
58
67
  var TelemetryEvents = {
59
68
  SPAN: "telemetry.span",
@@ -89,6 +98,42 @@ function serviceError(code, message, service, options) {
89
98
  };
90
99
  }
91
100
 
101
+ // src/responseErrors.ts
102
+ function parseServiceErrorBody(errorText) {
103
+ try {
104
+ return JSON.parse(errorText);
105
+ } catch {
106
+ return {};
107
+ }
108
+ }
109
+ function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
110
+ if (parsed.message) {
111
+ return compactMessage(parsed.message);
112
+ }
113
+ if (looksLikeHtml(errorText)) {
114
+ if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
115
+ return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
116
+ }
117
+ return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
118
+ }
119
+ return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
120
+ }
121
+ function responseErrorMeta(status, statusText, errorText) {
122
+ const meta = { status, statusText };
123
+ const snippet = compactMessage(errorText);
124
+ if (snippet) {
125
+ meta.responseSnippet = snippet.slice(0, 300);
126
+ }
127
+ return meta;
128
+ }
129
+ function looksLikeHtml(text) {
130
+ return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
131
+ }
132
+ function compactMessage(message) {
133
+ const compact = message.replace(/\s+/g, " ").trim();
134
+ return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
135
+ }
136
+
92
137
  // src/errors.ts
93
138
  import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
94
139
  function authRequiredError(service) {
@@ -98,6 +143,30 @@ function authRequiredError(service) {
98
143
  service
99
144
  };
100
145
  }
146
+ function nodeRetryError(service, message, response, errorText) {
147
+ const body = errorText.trim();
148
+ const [, token, stage] = /^([a-z_]+)(?::\s*([a-z_]+):)?/.exec(body) ?? [];
149
+ let code;
150
+ if (response.status === 503 && (token === "sql_busy" || token === "unavailable_retryable" || token === "read_audit_busy")) {
151
+ code = ErrorCodes.NODE_BUSY;
152
+ } else if (response.status === 503 && token === "request_timeout") {
153
+ code = ErrorCodes.REQUEST_TIMEOUT;
154
+ } else if (response.status === 500 && token === "outcome_unknown") {
155
+ code = ErrorCodes.OUTCOME_UNKNOWN;
156
+ } else {
157
+ return void 0;
158
+ }
159
+ const retryable = code !== ErrorCodes.OUTCOME_UNKNOWN;
160
+ const retryAfter = response.headers.get("retry-after")?.trim();
161
+ return serviceError(code, message, service, {
162
+ meta: {
163
+ ...responseErrorMeta(response.status, response.statusText, errorText),
164
+ retryable,
165
+ ...token === "sql_busy" && stage ? { stage } : {},
166
+ ...retryable && retryAfter && /^\d{1,9}$/.test(retryAfter) ? { retryAfterMs: Number(retryAfter) * 1e3 } : {}
167
+ }
168
+ });
169
+ }
101
170
  var RequestTimeoutError = class extends Error {
102
171
  constructor(timeoutMs) {
103
172
  super(`Request timed out after ${timeoutMs}ms.`);
@@ -417,6 +486,18 @@ var BaseService = class {
417
486
  }
418
487
  return controller.signal;
419
488
  }
489
+ /** POST a signed invocation with retry; `mint` re-signs per attempt, `init.headers` add to the signed ones. */
490
+ invokeWithRetry(url, mint, { headers: extra, ...init } = {}) {
491
+ const build = () => {
492
+ const signed = mint();
493
+ return {
494
+ method: "POST",
495
+ ...init,
496
+ headers: Array.isArray(signed) ? [...signed, ...Object.entries(extra ?? {})] : { ...signed, ...extra }
497
+ };
498
+ };
499
+ return this.context.invokeWithRetry ? this.context.invokeWithRetry(url, build) : this.context.fetch(url, build());
500
+ }
420
501
  /**
421
502
  * Create the abort signal for one request, bounded by the request timeout.
422
503
  *
@@ -505,42 +586,6 @@ var BaseService = class {
505
586
  }
506
587
  };
507
588
 
508
- // src/responseErrors.ts
509
- function parseServiceErrorBody(errorText) {
510
- try {
511
- return JSON.parse(errorText);
512
- } catch {
513
- return {};
514
- }
515
- }
516
- function formatServiceResponseError(serviceLabel, operation, status, errorText, parsed) {
517
- if (parsed.message) {
518
- return compactMessage(parsed.message);
519
- }
520
- if (looksLikeHtml(errorText)) {
521
- if (status === 524 || /524\s*[:-]/i.test(errorText) || /a timeout occurred/i.test(errorText)) {
522
- return `${serviceLabel} ${operation} failed: upstream request timed out. Please retry.`;
523
- }
524
- return `${serviceLabel} ${operation} failed: upstream service returned an HTML error page (${status}).`;
525
- }
526
- return `${serviceLabel} ${operation} failed: ${status} - ${compactMessage(errorText)}`;
527
- }
528
- function responseErrorMeta(status, statusText, errorText) {
529
- const meta = { status, statusText };
530
- const snippet = compactMessage(errorText);
531
- if (snippet) {
532
- meta.responseSnippet = snippet.slice(0, 300);
533
- }
534
- return meta;
535
- }
536
- function looksLikeHtml(text) {
537
- return /<!doctype html/i.test(text) || /<html[\s>]/i.test(text);
538
- }
539
- function compactMessage(message) {
540
- const compact = message.replace(/\s+/g, " ").trim();
541
- return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
542
- }
543
-
544
589
  // src/sql/SQLMigrations.ts
545
590
  var SQLMigrations = class {
546
591
  constructor(service, dbName) {
@@ -904,18 +949,19 @@ var SQLService = class extends BaseService {
904
949
  }
905
950
  // === Private helpers ===
906
951
  async invokeSQL(dbName, actions, body, signal) {
907
- const session = this.context.session;
908
952
  const actionList = Array.isArray(actions) ? actions : [actions];
909
- const headers = actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
910
- return this.context.fetch(`${this.host}/invoke`, {
911
- method: "POST",
912
- headers: {
913
- ...headers,
914
- "Content-Type": "application/json"
953
+ return this.invokeWithRetry(
954
+ `${this.host}/invoke`,
955
+ () => {
956
+ const session = this.context.session;
957
+ return actionList.length === 1 ? this.context.invoke(session, "sql", dbName, actionList[0]) : this.invokeSQLAny(session, dbName, actionList);
915
958
  },
916
- body: JSON.stringify(body),
917
- signal
918
- });
959
+ {
960
+ headers: { "Content-Type": "application/json" },
961
+ body: JSON.stringify(body),
962
+ signal
963
+ }
964
+ );
919
965
  }
920
966
  actionForSql(sql, fallback) {
921
967
  const token = firstSqlToken(sql);
@@ -956,6 +1002,8 @@ var SQLService = class extends BaseService {
956
1002
  if (response.status === 413 && parseStorageRejection(errorText)) {
957
1003
  return err(storageRejectionError("sql", 413, meta, errorText));
958
1004
  }
1005
+ const retryError = nodeRetryError("sql", `SQL ${operation} failed: ${response.status} - ${errorText}`, response, errorText);
1006
+ if (retryError) return err(retryError);
959
1007
  const errorBody = parseServiceErrorBody(errorText);
960
1008
  const errorCode = this.mapHttpStatusToErrorCode(
961
1009
  response.status,