@tinycloud/sdk-services 3.2.0-beta.3 → 3.2.0-beta.7

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-BDQXjWMx.d.cts → BaseService-4wwJq1Gn.d.cts} +1 -1
  2. package/dist/{BaseService-Di_hVfUV.d.ts → BaseService-BGr2slUz.d.ts} +1 -1
  3. package/dist/encryption/index.cjs +5 -1
  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 +5 -1
  8. package/dist/encryption/index.js.map +1 -1
  9. package/dist/index.cjs +190 -11
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +157 -6
  12. package/dist/index.d.ts +157 -6
  13. package/dist/index.js +184 -11
  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 +56 -11
  18. package/dist/kv/index.cjs.map +1 -1
  19. package/dist/kv/index.d.cts +5 -4
  20. package/dist/kv/index.d.ts +5 -4
  21. package/dist/kv/index.js +56 -11
  22. package/dist/kv/index.js.map +1 -1
  23. package/dist/kv/replication/index.cjs +44 -19
  24. package/dist/kv/replication/index.cjs.map +1 -1
  25. package/dist/kv/replication/index.d.cts +4 -3
  26. package/dist/kv/replication/index.d.ts +4 -3
  27. package/dist/kv/replication/index.js +44 -19
  28. package/dist/kv/replication/index.js.map +1 -1
  29. package/dist/kv/replication/prefix.cjs +43 -0
  30. package/dist/kv/replication/prefix.cjs.map +1 -0
  31. package/dist/kv/replication/prefix.d.cts +6 -0
  32. package/dist/kv/replication/prefix.d.ts +6 -0
  33. package/dist/kv/replication/prefix.js +17 -0
  34. package/dist/kv/replication/prefix.js.map +1 -0
  35. package/dist/{scope-BmLxwyl-.d.ts → scope-CoKKmMiI.d.ts} +5 -5
  36. package/dist/{scope-D855kRDx.d.cts → scope-D7wmjbMH.d.cts} +5 -5
  37. package/dist/sql/index.cjs +44 -2
  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 +44 -2
  42. package/dist/sql/index.js.map +1 -1
  43. package/dist/{types-7A5rHWRY.d.cts → types-D-9A3Sut.d.cts} +6 -0
  44. package/dist/{types-7A5rHWRY.d.ts → types-D-9A3Sut.d.ts} +6 -0
  45. package/package.json +14 -1
package/dist/index.js CHANGED
@@ -36,6 +36,8 @@ var ErrorCodes = {
36
36
  // Storage quota errors
37
37
  STORAGE_QUOTA_EXCEEDED: "STORAGE_QUOTA_EXCEEDED",
38
38
  STORAGE_LIMIT_REACHED: "STORAGE_LIMIT_REACHED",
39
+ /** The node answered the usage read without usage numbers (it predates the read). */
40
+ STORAGE_STATUS_UNAVAILABLE: "STORAGE_STATUS_UNAVAILABLE",
39
41
  // DuckDB-specific errors
40
42
  DUCKDB_ERROR: "DUCKDB_ERROR",
41
43
  DUCKDB_PERMISSION_DENIED: "DUCKDB_PERMISSION_DENIED",
@@ -65,7 +67,9 @@ var TelemetryEvents = {
65
67
  REPLICATION_WRITE: "replication.write",
66
68
  REPLICATION_SYNC: "replication.sync",
67
69
  REPLICATION_STATE: "replication.state",
68
- REPLICATION_DIVERGENCE: "replication.divergence"
70
+ REPLICATION_DIVERGENCE: "replication.divergence",
71
+ /** Emitted once, on the first write rejected because storage is full. */
72
+ STORAGE_FULL: "storage.full"
69
73
  };
70
74
  function ok(data) {
71
75
  return { ok: true, data };
@@ -1284,8 +1288,43 @@ function parseStorageQuotaBytes(responseText) {
1284
1288
  limitBytes: parseInt(match[2], 10)
1285
1289
  };
1286
1290
  }
1291
+ var STORAGE_REJECTION_BODY_ERRORS = {
1292
+ storage_quota_exceeded: true,
1293
+ storage_limit_reached: true
1294
+ };
1295
+ function isByteCount(value) {
1296
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1297
+ }
1298
+ function byteCountsOf(value) {
1299
+ if (typeof value !== "object" || value === null) return void 0;
1300
+ const { usedBytes, limitBytes } = value;
1301
+ return isByteCount(usedBytes) && isByteCount(limitBytes) ? { usedBytes, limitBytes } : void 0;
1302
+ }
1303
+ function parseStorageRejection(responseText) {
1304
+ let body;
1305
+ try {
1306
+ body = JSON.parse(responseText);
1307
+ } catch {
1308
+ return parseStorageQuotaBytes(responseText);
1309
+ }
1310
+ if (typeof body !== "object" || body === null) return void 0;
1311
+ const record = body;
1312
+ const message = typeof record.message === "string" ? record.message : "";
1313
+ const space = byteCountsOf(record.space) ?? parseStorageQuotaBytes(message);
1314
+ const isStorageBody = typeof record.error === "string" && STORAGE_REJECTION_BODY_ERRORS[record.error] === true;
1315
+ if (!space && !isStorageBody) {
1316
+ return void 0;
1317
+ }
1318
+ const details = { ...space };
1319
+ const account = byteCountsOf(record.account);
1320
+ if (account) {
1321
+ const plan = record.account.plan;
1322
+ details.account = typeof plan === "string" && plan !== "" ? { ...account, plan } : account;
1323
+ }
1324
+ return details;
1325
+ }
1287
1326
  function storageRejectionError(service, status, meta, responseText) {
1288
- const quotaMeta = { ...meta, ...parseStorageQuotaBytes(responseText) };
1327
+ const quotaMeta = { ...meta, ...parseStorageRejection(responseText) };
1289
1328
  return status === 402 ? storageQuotaExceededError(service, STORAGE_FULL_MESSAGE, quotaMeta) : storageLimitReachedError(service, STORAGE_WRITE_TOO_LARGE_MESSAGE, quotaMeta);
1290
1329
  }
1291
1330
  function isStorageFullError(error) {
@@ -1814,7 +1853,7 @@ var KVService = class extends BaseService {
1814
1853
  if (response.status !== 402 && response.status !== 413) {
1815
1854
  return void 0;
1816
1855
  }
1817
- if (response.status === 413 && !parseStorageQuotaBytes(errorText)) {
1856
+ if (response.status === 413 && !parseStorageRejection(errorText)) {
1818
1857
  return void 0;
1819
1858
  }
1820
1859
  return err(
@@ -2854,6 +2893,7 @@ var KVService = class extends BaseService {
2854
2893
  "kv"
2855
2894
  ));
2856
2895
  }
2896
+ const requestPrefix = options.prefix === "/" ? "" : options.prefix;
2857
2897
  const headers = {};
2858
2898
  if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
2859
2899
  if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
@@ -2863,7 +2903,7 @@ var KVService = class extends BaseService {
2863
2903
  const request = this.createRequestSignal(options.signal, options.timeout);
2864
2904
  try {
2865
2905
  const response = await this.invokeOperation(
2866
- options.prefix,
2906
+ requestPrefix,
2867
2907
  KVAction.SYNC,
2868
2908
  void 0,
2869
2909
  request.signal,
@@ -2879,7 +2919,7 @@ var KVService = class extends BaseService {
2879
2919
  } catch {
2880
2920
  body = void 0;
2881
2921
  }
2882
- const page = this.normalizeChangesResponse(body);
2922
+ const page = this.normalizeChangesResponse(body, options.prefix);
2883
2923
  if (!page) {
2884
2924
  return err(serviceError(
2885
2925
  ErrorCodes.NETWORK_ERROR,
@@ -2959,7 +2999,7 @@ var KVService = class extends BaseService {
2959
2999
  { meta }
2960
3000
  ));
2961
3001
  }
2962
- normalizeChangesResponse(data) {
3002
+ normalizeChangesResponse(data, requestedPrefix) {
2963
3003
  if (!isJsonObject(data)) return void 0;
2964
3004
  const { changes, more, cursor, source, authority } = data;
2965
3005
  if (!Array.isArray(changes) || typeof more !== "boolean" || typeof cursor !== "string" || !isJsonObject(source) || typeof source.nodeDid !== "string" || typeof source.space !== "string" || typeof source.prefix !== "string" || !isJsonObject(authority)) {
@@ -2991,7 +3031,11 @@ var KVService = class extends BaseService {
2991
3031
  changes: parsed,
2992
3032
  more,
2993
3033
  cursor,
2994
- source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
3034
+ source: {
3035
+ nodeDid: source.nodeDid,
3036
+ space: source.space,
3037
+ prefix: requestedPrefix === "/" && source.prefix === "" ? "/" : source.prefix
3038
+ },
2995
3039
  authority: { notBefore, expiresAt, retainUntil }
2996
3040
  };
2997
3041
  }
@@ -3286,13 +3330,14 @@ ${canonical.space}
3286
3330
  ${canonical.principal}`;
3287
3331
  }
3288
3332
 
3289
- // src/kv/replication/scope.ts
3333
+ // src/kv/replication/prefix.ts
3290
3334
  function kvPrefixCovers(prefix, key) {
3291
- if (prefix === "") return true;
3292
- if (prefix.endsWith("/")) return key.startsWith(prefix);
3293
- return key === prefix || key.startsWith(`${prefix}/`);
3335
+ return prefix === "/" || key.startsWith(prefix);
3294
3336
  }
3337
+
3338
+ // src/kv/replication/scope.ts
3295
3339
  function requiresSecretsOptIn(space, prefix) {
3340
+ if (prefix === "/") return true;
3296
3341
  if (space === "secrets" || space.endsWith(":secrets")) return true;
3297
3342
  return prefix.split("/", 1)[0] === "vault";
3298
3343
  }
@@ -3731,6 +3776,9 @@ var SQLService = class extends BaseService {
3731
3776
  if (response.status === 402) {
3732
3777
  return err(storageRejectionError("sql", 402, meta, errorText));
3733
3778
  }
3779
+ if (response.status === 413 && parseStorageRejection(errorText)) {
3780
+ return err(storageRejectionError("sql", 413, meta, errorText));
3781
+ }
3734
3782
  const errorBody = parseServiceErrorBody(errorText);
3735
3783
  const errorCode = this.mapHttpStatusToErrorCode(
3736
3784
  response.status,
@@ -4197,6 +4245,9 @@ var DuckDbService = class extends BaseService {
4197
4245
  if (response.status === 402) {
4198
4246
  return err(storageRejectionError("duckdb", 402, meta, errorText));
4199
4247
  }
4248
+ if (response.status === 413 && parseStorageRejection(errorText)) {
4249
+ return err(storageRejectionError("duckdb", 413, meta, errorText));
4250
+ }
4200
4251
  const errorBody = parseServiceErrorBody(errorText);
4201
4252
  const errorCode = this.mapHttpStatusToErrorCode(
4202
4253
  response.status,
@@ -5258,6 +5309,122 @@ function parsedStringArrayField(value, key) {
5258
5309
  }
5259
5310
  }
5260
5311
 
5312
+ // src/storage/StorageFullMonitor.ts
5313
+ var StorageFullMonitor = class {
5314
+ /**
5315
+ * @param ownerOf Owner key of a space id. Spaces with one key share one
5316
+ * storage budget. Defaults to the space id itself.
5317
+ */
5318
+ constructor(ownerOf = (spaceId) => spaceId) {
5319
+ this.ownerOf = ownerOf;
5320
+ /** First rejection per owner key; {@link UNKNOWN_OWNER} when the space was unknown. */
5321
+ this.full = /* @__PURE__ */ new Map();
5322
+ this.handlers = /* @__PURE__ */ new Set();
5323
+ }
5324
+ /** True while any owner's storage is known to be full. */
5325
+ get isFull() {
5326
+ return this.full.size > 0;
5327
+ }
5328
+ /** The rejection `storage.full` announced, until the episode ends. */
5329
+ get rejection() {
5330
+ return this.event;
5331
+ }
5332
+ /**
5333
+ * True when a write to `spaceId`'s owner was rejected, or a write whose
5334
+ * space was unknown was: that one could have been any owner's.
5335
+ */
5336
+ isFullFor(spaceId) {
5337
+ return this.full.has(UNKNOWN_OWNER) || spaceId !== void 0 && this.full.has(this.ownerOf(spaceId));
5338
+ }
5339
+ /** Watch a context's service errors. Returns the unsubscribe function. */
5340
+ observe(context) {
5341
+ return context.on(TelemetryEvents.SERVICE_ERROR, (data) => {
5342
+ const { service, error } = data;
5343
+ if (!error || !isStorageFullError(error)) return;
5344
+ const spaceId = context.session?.spaceId;
5345
+ const owner = spaceId ? this.ownerOf(spaceId) : UNKNOWN_OWNER;
5346
+ if (this.full.has(owner)) return;
5347
+ const meta = error.meta ?? {};
5348
+ const event = { service: service ?? error.service, error };
5349
+ if (typeof meta.usedBytes === "number") event.usedBytes = meta.usedBytes;
5350
+ if (typeof meta.limitBytes === "number") event.limitBytes = meta.limitBytes;
5351
+ if (typeof meta.account === "object" && meta.account !== null) {
5352
+ event.account = meta.account;
5353
+ }
5354
+ if (spaceId) event.spaceId = spaceId;
5355
+ this.full.set(owner, event);
5356
+ if (this.event) return;
5357
+ this.event = event;
5358
+ context.emit(TelemetryEvents.STORAGE_FULL, event);
5359
+ for (const handler of this.handlers) {
5360
+ try {
5361
+ handler(event);
5362
+ } catch (handlerError) {
5363
+ console.error(`Error in "${TelemetryEvents.STORAGE_FULL}" handler:`, handlerError);
5364
+ }
5365
+ }
5366
+ });
5367
+ }
5368
+ /** Subscribe to the `storage.full` event. Returns the unsubscribe function. */
5369
+ on(handler) {
5370
+ this.handlers.add(handler);
5371
+ return () => {
5372
+ this.handlers.delete(handler);
5373
+ };
5374
+ }
5375
+ /** Forget that any owner's storage is full; the next rejection emits `storage.full` again. */
5376
+ clear() {
5377
+ this.full.clear();
5378
+ this.event = void 0;
5379
+ }
5380
+ /**
5381
+ * Forget that `spaceId`'s owner is full (`undefined`: the rejections whose
5382
+ * space was unknown). Once no owner is full, the next rejection emits
5383
+ * `storage.full` again.
5384
+ */
5385
+ clearFor(spaceId) {
5386
+ this.full.delete(spaceId === void 0 ? UNKNOWN_OWNER : this.ownerOf(spaceId));
5387
+ if (this.full.size === 0) this.event = void 0;
5388
+ }
5389
+ };
5390
+ var UNKNOWN_OWNER = "\0unknown";
5391
+
5392
+ // src/storage/status.ts
5393
+ var STORAGE_MANAGE_URL = "https://account.tinycloud.xyz/billing";
5394
+ var STORAGE_NEARLY_FULL_RATIO = 0.9;
5395
+ function storageUsageState(usedBytes, limitBytes) {
5396
+ if (limitBytes === null) return "ok";
5397
+ if (usedBytes >= limitBytes) return "full";
5398
+ return usedBytes >= limitBytes * STORAGE_NEARLY_FULL_RATIO ? "nearly_full" : "ok";
5399
+ }
5400
+ function storageUsageOf(value) {
5401
+ if (typeof value !== "object" || value === null) return void 0;
5402
+ const { usedBytes, limitBytes } = value;
5403
+ const isByteCount2 = (n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
5404
+ if (!isByteCount2(usedBytes)) return void 0;
5405
+ if (limitBytes !== null && !isByteCount2(limitBytes)) return void 0;
5406
+ return { usedBytes, limitBytes };
5407
+ }
5408
+ function parseStorageStatus(body) {
5409
+ if (typeof body !== "object" || body === null) return void 0;
5410
+ const record = body;
5411
+ const space = storageUsageOf(record.space);
5412
+ if (!space) return void 0;
5413
+ const status = {
5414
+ ...space,
5415
+ state: storageUsageState(space.usedBytes, space.limitBytes),
5416
+ manageUrl: typeof record.manageUrl === "string" && record.manageUrl.startsWith("https://") ? record.manageUrl : STORAGE_MANAGE_URL
5417
+ };
5418
+ const account = storageUsageOf(record.account);
5419
+ if (account) {
5420
+ const plan = record.account.plan;
5421
+ status.account = typeof plan === "string" && plan !== "" ? { ...account, plan } : account;
5422
+ if (status.account.plan) status.plan = status.account.plan;
5423
+ status.state = storageUsageState(account.usedBytes, account.limitBytes);
5424
+ }
5425
+ return status;
5426
+ }
5427
+
5261
5428
  // src/quota/TinyCloudQuota.ts
5262
5429
  var TinyCloudQuota = class {
5263
5430
  constructor(config = {}) {
@@ -8060,6 +8227,8 @@ export {
8060
8227
  SQLMigrations,
8061
8228
  SQLService,
8062
8229
  STORAGE_FULL_MESSAGE,
8230
+ STORAGE_MANAGE_URL,
8231
+ STORAGE_NEARLY_FULL_RATIO,
8063
8232
  STORAGE_WRITE_TOO_LARGE_MESSAGE,
8064
8233
  SecretsService,
8065
8234
  ServiceContext,
@@ -8069,6 +8238,7 @@ export {
8069
8238
  ServiceResponseEventSchema,
8070
8239
  ServiceRetryEventSchema,
8071
8240
  ServiceSessionSchema,
8241
+ StorageFullMonitor,
8072
8242
  TC832_RESERVED_SCOPES,
8073
8243
  TelemetryEvents,
8074
8244
  TelemetrySpanEventSchema,
@@ -8130,6 +8300,8 @@ export {
8130
8300
  parsePermissionHintFromErrorText,
8131
8301
  parseSecretCatalogKey,
8132
8302
  parseStorageQuotaBytes,
8303
+ parseStorageRejection,
8304
+ parseStorageStatus,
8133
8305
  permissionDeniedError,
8134
8306
  replicationIdentityKey,
8135
8307
  requiresSecretsOptIn,
@@ -8139,6 +8311,7 @@ export {
8139
8311
  storageLimitReachedError,
8140
8312
  storageQuotaExceededError,
8141
8313
  storageRejectionError,
8314
+ storageUsageState,
8142
8315
  timeoutError,
8143
8316
  tinyCloudDebugLogger,
8144
8317
  utf8Decode,