@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.cjs CHANGED
@@ -59,6 +59,8 @@ __export(src_exports, {
59
59
  SQLMigrations: () => SQLMigrations,
60
60
  SQLService: () => SQLService,
61
61
  STORAGE_FULL_MESSAGE: () => STORAGE_FULL_MESSAGE,
62
+ STORAGE_MANAGE_URL: () => STORAGE_MANAGE_URL,
63
+ STORAGE_NEARLY_FULL_RATIO: () => STORAGE_NEARLY_FULL_RATIO,
62
64
  STORAGE_WRITE_TOO_LARGE_MESSAGE: () => STORAGE_WRITE_TOO_LARGE_MESSAGE,
63
65
  SecretsService: () => SecretsService,
64
66
  ServiceContext: () => ServiceContext,
@@ -68,6 +70,7 @@ __export(src_exports, {
68
70
  ServiceResponseEventSchema: () => ServiceResponseEventSchema,
69
71
  ServiceRetryEventSchema: () => ServiceRetryEventSchema,
70
72
  ServiceSessionSchema: () => ServiceSessionSchema,
73
+ StorageFullMonitor: () => StorageFullMonitor,
71
74
  TC832_RESERVED_SCOPES: () => TC832_RESERVED_SCOPES,
72
75
  TelemetryEvents: () => TelemetryEvents,
73
76
  TelemetrySpanEventSchema: () => TelemetrySpanEventSchema,
@@ -129,6 +132,8 @@ __export(src_exports, {
129
132
  parsePermissionHintFromErrorText: () => parsePermissionHintFromErrorText,
130
133
  parseSecretCatalogKey: () => parseSecretCatalogKey,
131
134
  parseStorageQuotaBytes: () => parseStorageQuotaBytes,
135
+ parseStorageRejection: () => parseStorageRejection,
136
+ parseStorageStatus: () => parseStorageStatus,
132
137
  permissionDeniedError: () => permissionDeniedError,
133
138
  replicationIdentityKey: () => replicationIdentityKey,
134
139
  requiresSecretsOptIn: () => requiresSecretsOptIn,
@@ -138,6 +143,7 @@ __export(src_exports, {
138
143
  storageLimitReachedError: () => storageLimitReachedError,
139
144
  storageQuotaExceededError: () => storageQuotaExceededError,
140
145
  storageRejectionError: () => storageRejectionError,
146
+ storageUsageState: () => storageUsageState,
141
147
  timeoutError: () => timeoutError,
142
148
  tinyCloudDebugLogger: () => tinyCloudDebugLogger,
143
149
  utf8Decode: () => utf8Decode,
@@ -195,6 +201,8 @@ var ErrorCodes = {
195
201
  // Storage quota errors
196
202
  STORAGE_QUOTA_EXCEEDED: "STORAGE_QUOTA_EXCEEDED",
197
203
  STORAGE_LIMIT_REACHED: "STORAGE_LIMIT_REACHED",
204
+ /** The node answered the usage read without usage numbers (it predates the read). */
205
+ STORAGE_STATUS_UNAVAILABLE: "STORAGE_STATUS_UNAVAILABLE",
198
206
  // DuckDB-specific errors
199
207
  DUCKDB_ERROR: "DUCKDB_ERROR",
200
208
  DUCKDB_PERMISSION_DENIED: "DUCKDB_PERMISSION_DENIED",
@@ -224,7 +232,9 @@ var TelemetryEvents = {
224
232
  REPLICATION_WRITE: "replication.write",
225
233
  REPLICATION_SYNC: "replication.sync",
226
234
  REPLICATION_STATE: "replication.state",
227
- REPLICATION_DIVERGENCE: "replication.divergence"
235
+ REPLICATION_DIVERGENCE: "replication.divergence",
236
+ /** Emitted once, on the first write rejected because storage is full. */
237
+ STORAGE_FULL: "storage.full"
228
238
  };
229
239
  function ok(data) {
230
240
  return { ok: true, data };
@@ -1443,8 +1453,43 @@ function parseStorageQuotaBytes(responseText) {
1443
1453
  limitBytes: parseInt(match[2], 10)
1444
1454
  };
1445
1455
  }
1456
+ var STORAGE_REJECTION_BODY_ERRORS = {
1457
+ storage_quota_exceeded: true,
1458
+ storage_limit_reached: true
1459
+ };
1460
+ function isByteCount(value) {
1461
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1462
+ }
1463
+ function byteCountsOf(value) {
1464
+ if (typeof value !== "object" || value === null) return void 0;
1465
+ const { usedBytes, limitBytes } = value;
1466
+ return isByteCount(usedBytes) && isByteCount(limitBytes) ? { usedBytes, limitBytes } : void 0;
1467
+ }
1468
+ function parseStorageRejection(responseText) {
1469
+ let body;
1470
+ try {
1471
+ body = JSON.parse(responseText);
1472
+ } catch {
1473
+ return parseStorageQuotaBytes(responseText);
1474
+ }
1475
+ if (typeof body !== "object" || body === null) return void 0;
1476
+ const record = body;
1477
+ const message = typeof record.message === "string" ? record.message : "";
1478
+ const space = byteCountsOf(record.space) ?? parseStorageQuotaBytes(message);
1479
+ const isStorageBody = typeof record.error === "string" && STORAGE_REJECTION_BODY_ERRORS[record.error] === true;
1480
+ if (!space && !isStorageBody) {
1481
+ return void 0;
1482
+ }
1483
+ const details = { ...space };
1484
+ const account = byteCountsOf(record.account);
1485
+ if (account) {
1486
+ const plan = record.account.plan;
1487
+ details.account = typeof plan === "string" && plan !== "" ? { ...account, plan } : account;
1488
+ }
1489
+ return details;
1490
+ }
1446
1491
  function storageRejectionError(service, status, meta, responseText) {
1447
- const quotaMeta = { ...meta, ...parseStorageQuotaBytes(responseText) };
1492
+ const quotaMeta = { ...meta, ...parseStorageRejection(responseText) };
1448
1493
  return status === 402 ? storageQuotaExceededError(service, STORAGE_FULL_MESSAGE, quotaMeta) : storageLimitReachedError(service, STORAGE_WRITE_TOO_LARGE_MESSAGE, quotaMeta);
1449
1494
  }
1450
1495
  function isStorageFullError(error) {
@@ -1973,7 +2018,7 @@ var KVService = class extends BaseService {
1973
2018
  if (response.status !== 402 && response.status !== 413) {
1974
2019
  return void 0;
1975
2020
  }
1976
- if (response.status === 413 && !parseStorageQuotaBytes(errorText)) {
2021
+ if (response.status === 413 && !parseStorageRejection(errorText)) {
1977
2022
  return void 0;
1978
2023
  }
1979
2024
  return err(
@@ -3013,6 +3058,7 @@ var KVService = class extends BaseService {
3013
3058
  "kv"
3014
3059
  ));
3015
3060
  }
3061
+ const requestPrefix = options.prefix === "/" ? "" : options.prefix;
3016
3062
  const headers = {};
3017
3063
  if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
3018
3064
  if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
@@ -3022,7 +3068,7 @@ var KVService = class extends BaseService {
3022
3068
  const request = this.createRequestSignal(options.signal, options.timeout);
3023
3069
  try {
3024
3070
  const response = await this.invokeOperation(
3025
- options.prefix,
3071
+ requestPrefix,
3026
3072
  KVAction.SYNC,
3027
3073
  void 0,
3028
3074
  request.signal,
@@ -3038,7 +3084,7 @@ var KVService = class extends BaseService {
3038
3084
  } catch {
3039
3085
  body = void 0;
3040
3086
  }
3041
- const page = this.normalizeChangesResponse(body);
3087
+ const page = this.normalizeChangesResponse(body, options.prefix);
3042
3088
  if (!page) {
3043
3089
  return err(serviceError(
3044
3090
  ErrorCodes.NETWORK_ERROR,
@@ -3118,7 +3164,7 @@ var KVService = class extends BaseService {
3118
3164
  { meta }
3119
3165
  ));
3120
3166
  }
3121
- normalizeChangesResponse(data) {
3167
+ normalizeChangesResponse(data, requestedPrefix) {
3122
3168
  if (!isJsonObject(data)) return void 0;
3123
3169
  const { changes, more, cursor, source, authority } = data;
3124
3170
  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)) {
@@ -3150,7 +3196,11 @@ var KVService = class extends BaseService {
3150
3196
  changes: parsed,
3151
3197
  more,
3152
3198
  cursor,
3153
- source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
3199
+ source: {
3200
+ nodeDid: source.nodeDid,
3201
+ space: source.space,
3202
+ prefix: requestedPrefix === "/" && source.prefix === "" ? "/" : source.prefix
3203
+ },
3154
3204
  authority: { notBefore, expiresAt, retainUntil }
3155
3205
  };
3156
3206
  }
@@ -3445,13 +3495,14 @@ ${canonical.space}
3445
3495
  ${canonical.principal}`;
3446
3496
  }
3447
3497
 
3448
- // src/kv/replication/scope.ts
3498
+ // src/kv/replication/prefix.ts
3449
3499
  function kvPrefixCovers(prefix, key) {
3450
- if (prefix === "") return true;
3451
- if (prefix.endsWith("/")) return key.startsWith(prefix);
3452
- return key === prefix || key.startsWith(`${prefix}/`);
3500
+ return prefix === "/" || key.startsWith(prefix);
3453
3501
  }
3502
+
3503
+ // src/kv/replication/scope.ts
3454
3504
  function requiresSecretsOptIn(space, prefix) {
3505
+ if (prefix === "/") return true;
3455
3506
  if (space === "secrets" || space.endsWith(":secrets")) return true;
3456
3507
  return prefix.split("/", 1)[0] === "vault";
3457
3508
  }
@@ -3890,6 +3941,9 @@ var SQLService = class extends BaseService {
3890
3941
  if (response.status === 402) {
3891
3942
  return err(storageRejectionError("sql", 402, meta, errorText));
3892
3943
  }
3944
+ if (response.status === 413 && parseStorageRejection(errorText)) {
3945
+ return err(storageRejectionError("sql", 413, meta, errorText));
3946
+ }
3893
3947
  const errorBody = parseServiceErrorBody(errorText);
3894
3948
  const errorCode = this.mapHttpStatusToErrorCode(
3895
3949
  response.status,
@@ -4356,6 +4410,9 @@ var DuckDbService = class extends BaseService {
4356
4410
  if (response.status === 402) {
4357
4411
  return err(storageRejectionError("duckdb", 402, meta, errorText));
4358
4412
  }
4413
+ if (response.status === 413 && parseStorageRejection(errorText)) {
4414
+ return err(storageRejectionError("duckdb", 413, meta, errorText));
4415
+ }
4359
4416
  const errorBody = parseServiceErrorBody(errorText);
4360
4417
  const errorCode = this.mapHttpStatusToErrorCode(
4361
4418
  response.status,
@@ -5417,6 +5474,122 @@ function parsedStringArrayField(value, key) {
5417
5474
  }
5418
5475
  }
5419
5476
 
5477
+ // src/storage/StorageFullMonitor.ts
5478
+ var StorageFullMonitor = class {
5479
+ /**
5480
+ * @param ownerOf Owner key of a space id. Spaces with one key share one
5481
+ * storage budget. Defaults to the space id itself.
5482
+ */
5483
+ constructor(ownerOf = (spaceId) => spaceId) {
5484
+ this.ownerOf = ownerOf;
5485
+ /** First rejection per owner key; {@link UNKNOWN_OWNER} when the space was unknown. */
5486
+ this.full = /* @__PURE__ */ new Map();
5487
+ this.handlers = /* @__PURE__ */ new Set();
5488
+ }
5489
+ /** True while any owner's storage is known to be full. */
5490
+ get isFull() {
5491
+ return this.full.size > 0;
5492
+ }
5493
+ /** The rejection `storage.full` announced, until the episode ends. */
5494
+ get rejection() {
5495
+ return this.event;
5496
+ }
5497
+ /**
5498
+ * True when a write to `spaceId`'s owner was rejected, or a write whose
5499
+ * space was unknown was: that one could have been any owner's.
5500
+ */
5501
+ isFullFor(spaceId) {
5502
+ return this.full.has(UNKNOWN_OWNER) || spaceId !== void 0 && this.full.has(this.ownerOf(spaceId));
5503
+ }
5504
+ /** Watch a context's service errors. Returns the unsubscribe function. */
5505
+ observe(context) {
5506
+ return context.on(TelemetryEvents.SERVICE_ERROR, (data) => {
5507
+ const { service, error } = data;
5508
+ if (!error || !isStorageFullError(error)) return;
5509
+ const spaceId = context.session?.spaceId;
5510
+ const owner = spaceId ? this.ownerOf(spaceId) : UNKNOWN_OWNER;
5511
+ if (this.full.has(owner)) return;
5512
+ const meta = error.meta ?? {};
5513
+ const event = { service: service ?? error.service, error };
5514
+ if (typeof meta.usedBytes === "number") event.usedBytes = meta.usedBytes;
5515
+ if (typeof meta.limitBytes === "number") event.limitBytes = meta.limitBytes;
5516
+ if (typeof meta.account === "object" && meta.account !== null) {
5517
+ event.account = meta.account;
5518
+ }
5519
+ if (spaceId) event.spaceId = spaceId;
5520
+ this.full.set(owner, event);
5521
+ if (this.event) return;
5522
+ this.event = event;
5523
+ context.emit(TelemetryEvents.STORAGE_FULL, event);
5524
+ for (const handler of this.handlers) {
5525
+ try {
5526
+ handler(event);
5527
+ } catch (handlerError) {
5528
+ console.error(`Error in "${TelemetryEvents.STORAGE_FULL}" handler:`, handlerError);
5529
+ }
5530
+ }
5531
+ });
5532
+ }
5533
+ /** Subscribe to the `storage.full` event. Returns the unsubscribe function. */
5534
+ on(handler) {
5535
+ this.handlers.add(handler);
5536
+ return () => {
5537
+ this.handlers.delete(handler);
5538
+ };
5539
+ }
5540
+ /** Forget that any owner's storage is full; the next rejection emits `storage.full` again. */
5541
+ clear() {
5542
+ this.full.clear();
5543
+ this.event = void 0;
5544
+ }
5545
+ /**
5546
+ * Forget that `spaceId`'s owner is full (`undefined`: the rejections whose
5547
+ * space was unknown). Once no owner is full, the next rejection emits
5548
+ * `storage.full` again.
5549
+ */
5550
+ clearFor(spaceId) {
5551
+ this.full.delete(spaceId === void 0 ? UNKNOWN_OWNER : this.ownerOf(spaceId));
5552
+ if (this.full.size === 0) this.event = void 0;
5553
+ }
5554
+ };
5555
+ var UNKNOWN_OWNER = "\0unknown";
5556
+
5557
+ // src/storage/status.ts
5558
+ var STORAGE_MANAGE_URL = "https://account.tinycloud.xyz/billing";
5559
+ var STORAGE_NEARLY_FULL_RATIO = 0.9;
5560
+ function storageUsageState(usedBytes, limitBytes) {
5561
+ if (limitBytes === null) return "ok";
5562
+ if (usedBytes >= limitBytes) return "full";
5563
+ return usedBytes >= limitBytes * STORAGE_NEARLY_FULL_RATIO ? "nearly_full" : "ok";
5564
+ }
5565
+ function storageUsageOf(value) {
5566
+ if (typeof value !== "object" || value === null) return void 0;
5567
+ const { usedBytes, limitBytes } = value;
5568
+ const isByteCount2 = (n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
5569
+ if (!isByteCount2(usedBytes)) return void 0;
5570
+ if (limitBytes !== null && !isByteCount2(limitBytes)) return void 0;
5571
+ return { usedBytes, limitBytes };
5572
+ }
5573
+ function parseStorageStatus(body) {
5574
+ if (typeof body !== "object" || body === null) return void 0;
5575
+ const record = body;
5576
+ const space = storageUsageOf(record.space);
5577
+ if (!space) return void 0;
5578
+ const status = {
5579
+ ...space,
5580
+ state: storageUsageState(space.usedBytes, space.limitBytes),
5581
+ manageUrl: typeof record.manageUrl === "string" && record.manageUrl.startsWith("https://") ? record.manageUrl : STORAGE_MANAGE_URL
5582
+ };
5583
+ const account = storageUsageOf(record.account);
5584
+ if (account) {
5585
+ const plan = record.account.plan;
5586
+ status.account = typeof plan === "string" && plan !== "" ? { ...account, plan } : account;
5587
+ if (status.account.plan) status.plan = status.account.plan;
5588
+ status.state = storageUsageState(account.usedBytes, account.limitBytes);
5589
+ }
5590
+ return status;
5591
+ }
5592
+
5420
5593
  // src/quota/TinyCloudQuota.ts
5421
5594
  var TinyCloudQuota = class {
5422
5595
  constructor(config = {}) {
@@ -8220,6 +8393,8 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
8220
8393
  SQLMigrations,
8221
8394
  SQLService,
8222
8395
  STORAGE_FULL_MESSAGE,
8396
+ STORAGE_MANAGE_URL,
8397
+ STORAGE_NEARLY_FULL_RATIO,
8223
8398
  STORAGE_WRITE_TOO_LARGE_MESSAGE,
8224
8399
  SecretsService,
8225
8400
  ServiceContext,
@@ -8229,6 +8404,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
8229
8404
  ServiceResponseEventSchema,
8230
8405
  ServiceRetryEventSchema,
8231
8406
  ServiceSessionSchema,
8407
+ StorageFullMonitor,
8232
8408
  TC832_RESERVED_SCOPES,
8233
8409
  TelemetryEvents,
8234
8410
  TelemetrySpanEventSchema,
@@ -8290,6 +8466,8 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
8290
8466
  parsePermissionHintFromErrorText,
8291
8467
  parseSecretCatalogKey,
8292
8468
  parseStorageQuotaBytes,
8469
+ parseStorageRejection,
8470
+ parseStorageStatus,
8293
8471
  permissionDeniedError,
8294
8472
  replicationIdentityKey,
8295
8473
  requiresSecretsOptIn,
@@ -8299,6 +8477,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
8299
8477
  storageLimitReachedError,
8300
8478
  storageQuotaExceededError,
8301
8479
  storageRejectionError,
8480
+ storageUsageState,
8302
8481
  timeoutError,
8303
8482
  tinyCloudDebugLogger,
8304
8483
  utf8Decode,