@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
@@ -1,4 +1,4 @@
1
- import { P as PermissionHint } from '../types-gmdUiorz.cjs';
1
+ import { P as PermissionHint } from '../types-eVS1Whuf.cjs';
2
2
 
3
3
  /** A reachable node returned an HTTP failure for a decrypt request. */
4
4
  declare class DecryptTransportResponseError extends Error {
@@ -1,4 +1,4 @@
1
- import { P as PermissionHint } from '../types-gmdUiorz.js';
1
+ import { P as PermissionHint } from '../types-eVS1Whuf.js';
2
2
 
3
3
  /** A reachable node returned an HTTP failure for a decrypt request. */
4
4
  declare class DecryptTransportResponseError extends Error {
package/dist/kv/index.cjs CHANGED
@@ -48,6 +48,14 @@ var ErrorCodes = {
48
48
  ABORTED: "ABORTED",
49
49
  INVALID_INPUT: "INVALID_INPUT",
50
50
  PERMISSION_DENIED: "PERMISSION_DENIED",
51
+ // Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
52
+ // retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
53
+ /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */
54
+ NODE_BUSY: "NODE_BUSY",
55
+ /** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
56
+ REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
57
+ /** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */
58
+ OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN",
51
59
  // KV-specific errors
52
60
  KV_NOT_FOUND: "KV_NOT_FOUND",
53
61
  KV_WRITE_FAILED: "KV_WRITE_FAILED",
@@ -83,11 +91,12 @@ var ErrorCodes = {
83
91
  DUCKDB_READONLY_VIOLATION: "DUCKDB_READONLY_VIOLATION"
84
92
  };
85
93
  var defaultRetryPolicy = {
86
- maxAttempts: 3,
94
+ maxAttempts: 4,
87
95
  backoff: "exponential",
88
- baseDelayMs: 1e3,
89
- maxDelayMs: 1e4,
90
- retryableErrors: [ErrorCodes.NETWORK_ERROR, ErrorCodes.TIMEOUT]
96
+ baseDelayMs: 250,
97
+ maxDelayMs: 4e3,
98
+ retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],
99
+ maxTotalMs: 12e3
91
100
  };
92
101
  var TelemetryEvents = {
93
102
  SPAN: "telemetry.span",
@@ -123,6 +132,20 @@ function serviceError(code, message, service, options) {
123
132
  };
124
133
  }
125
134
 
135
+ // src/responseErrors.ts
136
+ function responseErrorMeta(status, statusText, errorText) {
137
+ const meta = { status, statusText };
138
+ const snippet = compactMessage(errorText);
139
+ if (snippet) {
140
+ meta.responseSnippet = snippet.slice(0, 300);
141
+ }
142
+ return meta;
143
+ }
144
+ function compactMessage(message) {
145
+ const compact = message.replace(/\s+/g, " ").trim();
146
+ return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
147
+ }
148
+
126
149
  // src/errors.ts
127
150
  var import_bootstrap = require("@tinycloud/bootstrap");
128
151
 
@@ -216,6 +239,30 @@ function authRequiredError(service) {
216
239
  service
217
240
  };
218
241
  }
242
+ function nodeRetryError(service, message, response, errorText) {
243
+ const body = errorText.trim();
244
+ const [, token, stage] = /^([a-z_]+)(?::\s*([a-z_]+):)?/.exec(body) ?? [];
245
+ let code;
246
+ if (response.status === 503 && (token === "sql_busy" || token === "unavailable_retryable" || token === "read_audit_busy")) {
247
+ code = ErrorCodes.NODE_BUSY;
248
+ } else if (response.status === 503 && token === "request_timeout") {
249
+ code = ErrorCodes.REQUEST_TIMEOUT;
250
+ } else if (response.status === 500 && token === "outcome_unknown") {
251
+ code = ErrorCodes.OUTCOME_UNKNOWN;
252
+ } else {
253
+ return void 0;
254
+ }
255
+ const retryable = code !== ErrorCodes.OUTCOME_UNKNOWN;
256
+ const retryAfter = response.headers.get("retry-after")?.trim();
257
+ return serviceError(code, message, service, {
258
+ meta: {
259
+ ...responseErrorMeta(response.status, response.statusText, errorText),
260
+ retryable,
261
+ ...token === "sql_busy" && stage ? { stage } : {},
262
+ ...retryable && retryAfter && /^\d{1,9}$/.test(retryAfter) ? { retryAfterMs: Number(retryAfter) * 1e3 } : {}
263
+ }
264
+ });
265
+ }
219
266
  var RequestTimeoutError = class extends Error {
220
267
  constructor(timeoutMs) {
221
268
  super(`Request timed out after ${timeoutMs}ms.`);
@@ -627,6 +674,18 @@ var BaseService = class {
627
674
  }
628
675
  return controller.signal;
629
676
  }
677
+ /** POST a signed invocation with retry; `mint` re-signs per attempt, `init.headers` add to the signed ones. */
678
+ invokeWithRetry(url, mint, { headers: extra, ...init } = {}) {
679
+ const build = () => {
680
+ const signed = mint();
681
+ return {
682
+ method: "POST",
683
+ ...init,
684
+ headers: Array.isArray(signed) ? [...signed, ...Object.entries(extra ?? {})] : { ...signed, ...extra }
685
+ };
686
+ };
687
+ return this.context.invokeWithRetry ? this.context.invokeWithRetry(url, build) : this.context.fetch(url, build());
688
+ }
630
689
  /**
631
690
  * Create the abort signal for one request, bounded by the request timeout.
632
691
  *
@@ -994,15 +1053,6 @@ var KVService = class extends BaseService {
994
1053
  get host() {
995
1054
  return this.context.hosts[0];
996
1055
  }
997
- withJsonContentType(headers) {
998
- if (Array.isArray(headers)) {
999
- return [...headers, ["content-type", "application/json"]];
1000
- }
1001
- return {
1002
- ...headers,
1003
- "content-type": "application/json"
1004
- };
1005
- }
1006
1056
  /**
1007
1057
  * Execute an invoke operation.
1008
1058
  *
@@ -1013,20 +1063,11 @@ var KVService = class extends BaseService {
1013
1063
  * @returns Fetch response
1014
1064
  */
1015
1065
  async invokeOperation(path, action, body, signal, extraHeaders) {
1016
- const session = this.context.session;
1017
- const headers = this.context.invoke(
1018
- session,
1019
- "kv",
1020
- path,
1021
- action
1066
+ return this.invokeWithRetry(
1067
+ `${this.host}/invoke`,
1068
+ () => this.context.invoke(this.context.session, "kv", path, action),
1069
+ { headers: extraHeaders, body, signal }
1022
1070
  );
1023
- const requestHeaders = Array.isArray(headers) ? [...headers, ...Object.entries(extraHeaders ?? {})] : { ...headers, ...extraHeaders };
1024
- return this.context.fetch(`${this.host}/invoke`, {
1025
- method: "POST",
1026
- headers: requestHeaders,
1027
- body,
1028
- signal
1029
- });
1030
1071
  }
1031
1072
  /**
1032
1073
  * Serialize a single put value into a fetch body.
@@ -1232,28 +1273,28 @@ var KVService = class extends BaseService {
1232
1273
  }
1233
1274
  const request = this.createRequestSignal(options?.signal, options?.timeout);
1234
1275
  try {
1235
- const session = this.context.session;
1236
- const invocationHeaders = this.context.invokeAny(
1237
- session,
1238
- paths.map((path) => ({
1239
- spaceId: session.spaceId,
1240
- service: "kv",
1241
- path,
1242
- action
1243
- }))
1244
- );
1245
1276
  const limitHeaders = {};
1246
1277
  if (getOptions?.maxResponseBytes !== void 0) {
1247
1278
  limitHeaders["x-tinycloud-max-response-bytes"] = String(
1248
1279
  getOptions.maxResponseBytes
1249
1280
  );
1250
1281
  }
1251
- const headers = Array.isArray(invocationHeaders) ? [...invocationHeaders, ...Object.entries(limitHeaders)] : { ...invocationHeaders, ...limitHeaders };
1252
- const response = await this.context.fetch(`${this.host}/invoke`, {
1253
- method: "POST",
1254
- headers,
1255
- signal: request.signal
1256
- });
1282
+ const response = await this.invokeWithRetry(
1283
+ `${this.host}/invoke`,
1284
+ () => {
1285
+ const session = this.context.session;
1286
+ return this.context.invokeAny(
1287
+ session,
1288
+ paths.map((path) => ({
1289
+ spaceId: session.spaceId,
1290
+ service: "kv",
1291
+ path,
1292
+ action
1293
+ }))
1294
+ );
1295
+ },
1296
+ { headers: limitHeaders, signal: request.signal }
1297
+ );
1257
1298
  if (!response.ok) {
1258
1299
  const errorText = response.status === 401 || response.status === 403 ? await this.readAuthorizationText(response) : await response.text();
1259
1300
  if (response.status === 401 || response.status === 403) {
@@ -1299,10 +1340,10 @@ var KVService = class extends BaseService {
1299
1340
  result: err(serviceError(item.error.code, item.error.message, "kv"))
1300
1341
  };
1301
1342
  }
1302
- const headers2 = item.headers;
1343
+ const headers = item.headers;
1303
1344
  const data = action === KVAction.HEAD ? void 0 : this.parseBatchValue(
1304
1345
  item.dataBase64,
1305
- headers2,
1346
+ headers,
1306
1347
  getOptions?.raw,
1307
1348
  getOptions?.binary
1308
1349
  );
@@ -1310,7 +1351,7 @@ var KVService = class extends BaseService {
1310
1351
  key: keys[index],
1311
1352
  result: ok({
1312
1353
  data,
1313
- headers: this.createBatchResponseHeaders(headers2)
1354
+ headers: this.createBatchResponseHeaders(headers)
1314
1355
  })
1315
1356
  };
1316
1357
  });
@@ -1534,15 +1575,18 @@ var KVService = class extends BaseService {
1534
1575
  { meta: { status: response.status, statusText: response.statusText } }
1535
1576
  ));
1536
1577
  }
1578
+ const message = `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
1579
+ const retryError = nodeRetryError("kv", message, response, errorText);
1580
+ if (retryError) return err(retryError);
1537
1581
  return err(serviceError(
1538
1582
  ErrorCodes.NETWORK_ERROR,
1539
- `Failed to get key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
1583
+ message,
1540
1584
  "kv",
1541
1585
  { meta: { status: response.status, statusText: response.statusText } }
1542
1586
  ));
1543
1587
  }
1544
1588
  const data = await this.parseResponse(response, options?.raw, options?.binary);
1545
- return ok({ data, headers: this.createResponseHeaders(response.headers) });
1589
+ return ok({ data, headers: this.createResponseHeaders(response.headers), ...response.retry && { retry: response.retry } });
1546
1590
  }
1547
1591
  async batchGet(keys, options) {
1548
1592
  return this.withTelemetry(
@@ -1618,26 +1662,22 @@ var KVService = class extends BaseService {
1618
1662
  { meta: { status: response.status, statusText: response.statusText } }
1619
1663
  ));
1620
1664
  }
1621
- if (response.status === 503 && (options?.ifMatch !== void 0 || options?.ifNoneMatch !== void 0)) {
1622
- return err(serviceError(
1623
- ErrorCodes.KV_CONFLICT,
1624
- `Concurrent KV update conflicted for key ${JSON.stringify(key)}`,
1625
- "kv",
1626
- { meta: { status: response.status, statusText: response.statusText } }
1627
- ));
1628
- }
1665
+ const message = `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
1666
+ const retryError = nodeRetryError("kv", message, response, errorText);
1667
+ if (retryError) return err(retryError);
1629
1668
  const quotaError = this.handleQuotaErrorResponse(response, errorText, key);
1630
1669
  if (quotaError) return quotaError;
1631
1670
  return err(serviceError(
1632
1671
  ErrorCodes.KV_WRITE_FAILED,
1633
- `Failed to put key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
1672
+ message,
1634
1673
  "kv",
1635
1674
  { meta: { status: response.status, statusText: response.statusText } }
1636
1675
  ));
1637
1676
  }
1638
1677
  return ok({
1639
1678
  data: void 0,
1640
- headers: this.createResponseHeaders(response.headers)
1679
+ headers: this.createResponseHeaders(response.headers),
1680
+ ...response.retry && { retry: response.retry }
1641
1681
  });
1642
1682
  }
1643
1683
  /**
@@ -1710,25 +1750,23 @@ var KVService = class extends BaseService {
1710
1750
  entries?.[index]?.body ?? this.serializeBatchPutValue(items[index])
1711
1751
  );
1712
1752
  }
1713
- const headers = this.context.invokeAny(
1714
- session,
1715
- paths.map((path) => ({
1716
- spaceId: session.spaceId,
1717
- service: "kv",
1718
- path,
1719
- action: KVAction.PUT
1720
- }))
1753
+ const response = await this.invokeWithRetry(
1754
+ `${this.host}/invoke`,
1755
+ () => {
1756
+ const headers = this.context.invokeAny(
1757
+ session,
1758
+ paths.map((path) => ({
1759
+ spaceId: session.spaceId,
1760
+ service: "kv",
1761
+ path,
1762
+ action: KVAction.PUT
1763
+ }))
1764
+ );
1765
+ requestMayHaveDispatched = true;
1766
+ return headers;
1767
+ },
1768
+ { body, signal }
1721
1769
  );
1722
- const url = `${this.host}/invoke`;
1723
- const init = {
1724
- method: "POST",
1725
- headers,
1726
- body,
1727
- signal
1728
- };
1729
- const fetchFn = this.context.fetch;
1730
- requestMayHaveDispatched = true;
1731
- const response = await fetchFn(url, init);
1732
1770
  if (!response.ok) {
1733
1771
  let errorText;
1734
1772
  try {
@@ -1930,9 +1968,12 @@ var KVService = class extends BaseService {
1930
1968
  );
1931
1969
  }
1932
1970
  const errorText = await response.text();
1971
+ const message = `Failed to list keys: ${response.status} - ${errorText}`;
1972
+ const retryError = nodeRetryError("kv", message, response, errorText);
1973
+ if (retryError) return err(retryError);
1933
1974
  return err(serviceError(
1934
1975
  ErrorCodes.NETWORK_ERROR,
1935
- `Failed to list keys: ${response.status} - ${errorText}`,
1976
+ message,
1936
1977
  "kv",
1937
1978
  { meta: { status: response.status, statusText: response.statusText } }
1938
1979
  ));
@@ -2183,24 +2224,20 @@ var KVService = class extends BaseService {
2183
2224
  { meta: { status: response.status, statusText: response.statusText } }
2184
2225
  ));
2185
2226
  }
2186
- if (response.status === 503 && options?.ifMatch !== void 0) {
2187
- return err(serviceError(
2188
- ErrorCodes.KV_CONFLICT,
2189
- `Concurrent KV delete conflicted for key ${JSON.stringify(key)}`,
2190
- "kv",
2191
- { meta: { status: response.status, statusText: response.statusText } }
2192
- ));
2193
- }
2227
+ const message = `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
2228
+ const retryError = nodeRetryError("kv", message, response, errorText);
2229
+ if (retryError) return err(retryError);
2194
2230
  return err(serviceError(
2195
2231
  ErrorCodes.NETWORK_ERROR,
2196
- `Failed to delete key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2232
+ message,
2197
2233
  "kv",
2198
2234
  { meta: { status: response.status, statusText: response.statusText } }
2199
2235
  ));
2200
2236
  }
2201
2237
  return ok({
2202
2238
  data: void 0,
2203
- headers: this.createResponseHeaders(response.headers)
2239
+ headers: this.createResponseHeaders(response.headers),
2240
+ ...response.retry && { retry: response.retry }
2204
2241
  });
2205
2242
  }
2206
2243
  /**
@@ -2234,10 +2271,13 @@ var KVService = class extends BaseService {
2234
2271
  return await this.classifyNotFound(response, key);
2235
2272
  }
2236
2273
  const errorText = await response.text();
2274
+ const message = `Failed to get metadata for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`;
2275
+ const retryError = nodeRetryError("kv", message, response, errorText);
2276
+ if (retryError) return err(retryError);
2237
2277
  return err(
2238
2278
  serviceError(
2239
2279
  ErrorCodes.NETWORK_ERROR,
2240
- `Failed to get metadata for key ${JSON.stringify(key)}: ${response.status} - ${errorText}`,
2280
+ message,
2241
2281
  "kv",
2242
2282
  { meta: { status: response.status, statusText: response.statusText } }
2243
2283
  )
@@ -2271,12 +2311,6 @@ var KVService = class extends BaseService {
2271
2311
  }
2272
2312
  const path = this.getFullPath(key, options?.prefix);
2273
2313
  const session = this.context.session;
2274
- const headers = this.context.invoke(
2275
- session,
2276
- "kv",
2277
- path,
2278
- KVAction.GET
2279
- );
2280
2314
  const body = {
2281
2315
  space: session.spaceId,
2282
2316
  path,
@@ -2290,12 +2324,15 @@ var KVService = class extends BaseService {
2290
2324
  }
2291
2325
  const request = this.createRequestSignal(options?.signal, options?.timeout);
2292
2326
  try {
2293
- const response = await this.context.fetch(`${this.host}/signed/kv`, {
2294
- method: "POST",
2295
- headers: this.withJsonContentType(headers),
2296
- body: JSON.stringify(body),
2297
- signal: request.signal
2298
- });
2327
+ const response = await this.invokeWithRetry(
2328
+ `${this.host}/signed/kv`,
2329
+ () => this.context.invoke(session, "kv", path, KVAction.GET),
2330
+ {
2331
+ headers: { "content-type": "application/json" },
2332
+ body: JSON.stringify(body),
2333
+ signal: request.signal
2334
+ }
2335
+ );
2299
2336
  if (!response.ok) {
2300
2337
  return await this.createSignedReadUrlError(response, key, path);
2301
2338
  }