@tinycloud/sdk-services 3.0.0 → 3.1.0-beta.10

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.
package/dist/sql/index.js CHANGED
@@ -5,6 +5,10 @@ var ErrorCodes = {
5
5
  AUTH_EXPIRED: "AUTH_EXPIRED",
6
6
  AUTH_REQUIRED: "AUTH_REQUIRED",
7
7
  AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
8
+ /** HTTP 401: the delegation the invocation cites was revoked. */
9
+ AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
10
+ /** HTTP 401: a delegation the cited one rests on was revoked. */
11
+ AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
8
12
  NETWORK_ERROR: "NETWORK_ERROR",
9
13
  TIMEOUT: "TIMEOUT",
10
14
  ABORTED: "ABORTED",
@@ -16,6 +20,10 @@ var ErrorCodes = {
16
20
  KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
17
21
  KV_CONFLICT: "KV_CONFLICT",
18
22
  KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
23
+ /** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
24
+ KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
25
+ /** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
26
+ KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
19
27
  // SQL-specific errors
20
28
  SQL_ERROR: "SQL_ERROR",
21
29
  SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
@@ -71,6 +79,7 @@ function serviceError(code, message, service, options) {
71
79
  }
72
80
 
73
81
  // src/errors.ts
82
+ import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
74
83
  function authRequiredError(service) {
75
84
  return {
76
85
  code: ErrorCodes.AUTH_REQUIRED,
@@ -78,11 +87,31 @@ function authRequiredError(service) {
78
87
  service
79
88
  };
80
89
  }
81
- function timeoutError(service) {
90
+ var RequestTimeoutError = class extends Error {
91
+ constructor(timeoutMs) {
92
+ super(`Request timed out after ${timeoutMs}ms.`);
93
+ this.name = "TimeoutError";
94
+ this.timeoutMs = timeoutMs;
95
+ }
96
+ };
97
+ function isRequestTimeoutError(error) {
98
+ return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
99
+ }
100
+ function timeoutError(service, timeoutMs, cause) {
101
+ if (timeoutMs === void 0) {
102
+ return {
103
+ code: ErrorCodes.TIMEOUT,
104
+ message: "Request timed out.",
105
+ service,
106
+ ...cause === void 0 ? {} : { cause }
107
+ };
108
+ }
82
109
  return {
83
110
  code: ErrorCodes.TIMEOUT,
84
- message: "Request timed out.",
85
- service
111
+ message: `Request timed out after ${timeoutMs}ms.`,
112
+ service,
113
+ ...cause === void 0 ? {} : { cause },
114
+ meta: { timeoutMs }
86
115
  };
87
116
  }
88
117
  function abortedError(service) {
@@ -99,8 +128,47 @@ function parseAuthError(responseText) {
99
128
  }
100
129
  return {};
101
130
  }
131
+ var grantableActions = new Set(
132
+ CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
133
+ );
134
+ var capabilityServices = new Set(
135
+ CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
136
+ );
137
+ function storageQuotaExceededError(service, message, meta) {
138
+ return {
139
+ code: ErrorCodes.STORAGE_QUOTA_EXCEEDED,
140
+ message,
141
+ service,
142
+ meta
143
+ };
144
+ }
145
+ function storageLimitReachedError(service, message, meta) {
146
+ return {
147
+ code: ErrorCodes.STORAGE_LIMIT_REACHED,
148
+ message,
149
+ service,
150
+ meta
151
+ };
152
+ }
153
+ var STORAGE_FULL_MESSAGE = "TinyCloud storage is full, so this change was not saved. Reading still works. Free up space or upgrade your plan to save again.";
154
+ var STORAGE_WRITE_TOO_LARGE_MESSAGE = "This change is larger than the TinyCloud storage you have left, so it was not saved. Reading still works. Free up space or upgrade your plan to save it.";
155
+ function parseStorageQuotaBytes(responseText) {
156
+ const match = responseText.match(/Used:\s*(\d+)\s*bytes,\s*Limit:\s*(\d+)\s*bytes/i);
157
+ if (!match) return void 0;
158
+ return {
159
+ usedBytes: parseInt(match[1], 10),
160
+ limitBytes: parseInt(match[2], 10)
161
+ };
162
+ }
163
+ function storageRejectionError(service, status, meta, responseText) {
164
+ const quotaMeta = { ...meta, ...parseStorageQuotaBytes(responseText) };
165
+ return status === 402 ? storageQuotaExceededError(service, STORAGE_FULL_MESSAGE, quotaMeta) : storageLimitReachedError(service, STORAGE_WRITE_TOO_LARGE_MESSAGE, quotaMeta);
166
+ }
102
167
  function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
103
168
  if (error instanceof Error) {
169
+ if (isRequestTimeoutError(error)) {
170
+ return timeoutError(service, error.timeoutMs, error);
171
+ }
104
172
  if (error.name === "AbortError") {
105
173
  return abortedError(service);
106
174
  }
@@ -122,6 +190,7 @@ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
122
190
  }
123
191
 
124
192
  // src/base/BaseService.ts
193
+ var MAX_TIMER_DELAY_MS = 2147483647;
125
194
  var BaseService = class {
126
195
  constructor() {
127
196
  /**
@@ -302,6 +371,64 @@ var BaseService = class {
302
371
  }
303
372
  return controller.signal;
304
373
  }
374
+ /**
375
+ * Create the abort signal for one request, bounded by the request timeout.
376
+ *
377
+ * The signal aborts when the service signs out, the context aborts, the
378
+ * caller's `signal` aborts, or the timeout elapses, whichever comes first.
379
+ * A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
380
+ * `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
381
+ * reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
382
+ *
383
+ * The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
384
+ * positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
385
+ * `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
386
+ *
387
+ * Callers must call `dispose()` once the response body has been consumed,
388
+ * which clears the timer and removes the listeners this adds to the
389
+ * long-lived service and context signals.
390
+ *
391
+ * @param signal - The caller's abort signal for this request
392
+ * @param timeoutMs - Per-request timeout override in milliseconds
393
+ */
394
+ createRequestSignal(signal, timeoutMs) {
395
+ const controller = new AbortController();
396
+ const detachers = [];
397
+ let timer;
398
+ const dispose = () => {
399
+ if (timer !== void 0) {
400
+ clearTimeout(timer);
401
+ timer = void 0;
402
+ }
403
+ for (const detach of detachers.splice(0)) {
404
+ detach();
405
+ }
406
+ };
407
+ const abort = (reason) => {
408
+ dispose();
409
+ controller.abort(reason);
410
+ };
411
+ const requestSignal = { signal: controller.signal, dispose };
412
+ const parents = [
413
+ this.abortController.signal,
414
+ this.context?.abortSignal,
415
+ signal
416
+ ].filter((parent) => parent !== void 0);
417
+ for (const parent of parents) {
418
+ if (parent.aborted) {
419
+ abort(parent.reason);
420
+ return requestSignal;
421
+ }
422
+ const onAbort = () => abort(parent.reason);
423
+ parent.addEventListener("abort", onAbort, { once: true });
424
+ detachers.push(() => parent.removeEventListener("abort", onAbort));
425
+ }
426
+ const timeout = timeoutMs ?? this._config.timeout;
427
+ if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
428
+ timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
429
+ }
430
+ return requestSignal;
431
+ }
305
432
  /**
306
433
  * Wrap an operation with error handling and telemetry.
307
434
  *
@@ -490,6 +617,7 @@ var SQLService = class extends BaseService {
490
617
  if (!this.requireAuth()) {
491
618
  return err(authRequiredError("sql"));
492
619
  }
620
+ const request = this.createRequestSignal(options?.signal);
493
621
  try {
494
622
  const body = {
495
623
  action: "query",
@@ -498,19 +626,22 @@ var SQLService = class extends BaseService {
498
626
  };
499
627
  if (options?.maxRows !== void 0) body.maxRows = options.maxRows;
500
628
  if (options?.maxBytes !== void 0) body.maxBytes = options.maxBytes;
629
+ const action = this.actionForSql(sql, SQLAction.READ);
501
630
  const response = await this.invokeSQL(
502
631
  dbName,
503
- this.actionForSql(sql, SQLAction.READ),
632
+ action,
504
633
  body,
505
- options?.signal
634
+ request.signal
506
635
  );
507
636
  if (!response.ok) {
508
- return this.handleErrorResponse(response, "query");
637
+ return this.handleErrorResponse(response, "query", dbName, [action]);
509
638
  }
510
639
  const data = await response.json();
511
640
  return ok(data);
512
641
  } catch (error) {
513
642
  return err(wrapError("sql", error));
643
+ } finally {
644
+ request.dispose();
514
645
  }
515
646
  });
516
647
  }
@@ -519,6 +650,7 @@ var SQLService = class extends BaseService {
519
650
  if (!this.requireAuth()) {
520
651
  return err(authRequiredError("sql"));
521
652
  }
653
+ const request = this.createRequestSignal(options?.signal);
522
654
  try {
523
655
  const body = {
524
656
  action: "execute",
@@ -534,19 +666,22 @@ var SQLService = class extends BaseService {
534
666
  (statement) => this.actionForSql(statement, SQLAction.SCHEMA)
535
667
  )
536
668
  ];
669
+ const requestedActions = this.dedupeActions(actions);
537
670
  const response = await this.invokeSQL(
538
671
  dbName,
539
- this.dedupeActions(actions),
672
+ requestedActions,
540
673
  body,
541
- options?.signal
674
+ request.signal
542
675
  );
543
676
  if (!response.ok) {
544
- return this.handleErrorResponse(response, "execute");
677
+ return this.handleErrorResponse(response, "execute", dbName, requestedActions);
545
678
  }
546
679
  const data = await response.json();
547
680
  return ok(data);
548
681
  } catch (error) {
549
682
  return err(wrapError("sql", error));
683
+ } finally {
684
+ request.dispose();
550
685
  }
551
686
  });
552
687
  }
@@ -555,20 +690,24 @@ var SQLService = class extends BaseService {
555
690
  if (!this.requireAuth()) {
556
691
  return err(authRequiredError("sql"));
557
692
  }
693
+ const request = this.createRequestSignal(options?.signal);
558
694
  try {
695
+ const requestedActions = this.actionsForSqlBatch(statements);
559
696
  const response = await this.invokeSQL(
560
697
  dbName,
561
- this.actionsForSqlBatch(statements),
698
+ requestedActions,
562
699
  { action: "batch", statements },
563
- options?.signal
700
+ request.signal
564
701
  );
565
702
  if (!response.ok) {
566
- return this.handleErrorResponse(response, "batch");
703
+ return this.handleErrorResponse(response, "batch", dbName, requestedActions);
567
704
  }
568
705
  const data = await response.json();
569
706
  return ok(data);
570
707
  } catch (error) {
571
708
  return err(wrapError("sql", error));
709
+ } finally {
710
+ request.dispose();
572
711
  }
573
712
  });
574
713
  }
@@ -577,20 +716,23 @@ var SQLService = class extends BaseService {
577
716
  if (!this.requireAuth()) {
578
717
  return err(authRequiredError("sql"));
579
718
  }
719
+ const request = this.createRequestSignal(options?.signal);
580
720
  try {
581
721
  const response = await this.invokeSQL(
582
722
  dbName,
583
723
  SQLAction.WRITE,
584
724
  { action: "execute_statement", name, params: params ?? [] },
585
- options?.signal
725
+ request.signal
586
726
  );
587
727
  if (!response.ok) {
588
- return this.handleErrorResponse(response, "executeStatement");
728
+ return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
589
729
  }
590
730
  const data = await response.json();
591
731
  return ok(data);
592
732
  } catch (error) {
593
733
  return err(wrapError("sql", error));
734
+ } finally {
735
+ request.dispose();
594
736
  }
595
737
  });
596
738
  }
@@ -599,15 +741,16 @@ var SQLService = class extends BaseService {
599
741
  if (!this.requireAuth()) {
600
742
  return err(authRequiredError("sql"));
601
743
  }
744
+ const request = this.createRequestSignal(options?.signal);
602
745
  try {
603
746
  const response = await this.invokeSQL(
604
747
  dbName,
605
748
  SQLAction.READ,
606
749
  { action: "export" },
607
- options?.signal
750
+ request.signal
608
751
  );
609
752
  if (!response.ok) {
610
- return this.handleErrorResponse(response, "export");
753
+ return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
611
754
  }
612
755
  const resp = response;
613
756
  if (typeof resp.blob === "function") {
@@ -618,26 +761,31 @@ var SQLService = class extends BaseService {
618
761
  return ok(text);
619
762
  } catch (error) {
620
763
  return err(wrapError("sql", error));
764
+ } finally {
765
+ request.dispose();
621
766
  }
622
767
  });
623
768
  }
624
769
  async applyMigrationsOnDbUnlocked(dbName, options) {
625
770
  return this.withTelemetry("migrations.apply", dbName, async () => {
626
- const created = await this.ensureMigrationsTable(dbName, options.signal);
627
- if (!created.ok) return created;
628
771
  const listed = await this.queryOnDb(
629
772
  dbName,
630
773
  `SELECT id FROM ${MIGRATIONS_TABLE} WHERE namespace = ? ORDER BY applied_at, id`,
631
774
  [options.namespace],
632
775
  { signal: options.signal }
633
776
  );
634
- if (!listed.ok) return listed;
777
+ const tableMissing = !listed.ok && (listed.error.code === ErrorCodes.SQL_DATABASE_NOT_FOUND || isMissingTableError(listed.error.message));
778
+ if (!listed.ok && !tableMissing) return listed;
635
779
  const appliedIds = new Set(
636
- listed.data.rows.map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
780
+ (listed.ok ? listed.data.rows : []).map((row) => rowValue(row, 0)).filter((id) => typeof id === "string")
637
781
  );
638
782
  const skipped = options.migrations.filter((migration) => appliedIds.has(migration.id)).map((migration) => migration.id);
639
783
  const pending = options.migrations.filter((migration) => !appliedIds.has(migration.id));
640
784
  const applied = [];
785
+ if (pending.length > 0 && tableMissing) {
786
+ const created = await this.ensureMigrationsTable(dbName, options.signal);
787
+ if (!created.ok) return created;
788
+ }
641
789
  for (const migration of pending) {
642
790
  const result = await this.applyOneMigration(dbName, options.namespace, migration, options.signal);
643
791
  if (!result.ok) return result;
@@ -705,7 +853,7 @@ var SQLService = class extends BaseService {
705
853
  "Content-Type": "application/json"
706
854
  },
707
855
  body: JSON.stringify(body),
708
- signal: this.combineSignals(signal)
856
+ signal
709
857
  });
710
858
  }
711
859
  actionForSql(sql, fallback) {
@@ -738,8 +886,12 @@ var SQLService = class extends BaseService {
738
886
  }))
739
887
  );
740
888
  }
741
- async handleErrorResponse(response, operation) {
889
+ async handleErrorResponse(response, operation, dbName, requestedActions) {
742
890
  const errorText = await response.text();
891
+ const meta = responseErrorMeta(response.status, response.statusText, errorText);
892
+ if (response.status === 402) {
893
+ return err(storageRejectionError("sql", 402, meta, errorText));
894
+ }
743
895
  const errorBody = parseServiceErrorBody(errorText);
744
896
  const errorCode = this.mapHttpStatusToErrorCode(
745
897
  response.status,
@@ -752,11 +904,12 @@ var SQLService = class extends BaseService {
752
904
  errorText,
753
905
  errorBody
754
906
  );
755
- const meta = responseErrorMeta(response.status, response.statusText, errorText);
756
907
  if (response.status === 401) {
757
- const { resource, action } = parseAuthError(errorText);
758
- if (action) meta.requiredAction = action;
759
- if (resource) meta.resource = resource;
908
+ const { action } = parseAuthError(errorText);
909
+ if (action && requestedActions.includes(action)) {
910
+ meta.requiredAction = action;
911
+ meta.resource = `${this.context.session.spaceId}/sql/${dbName}`;
912
+ }
760
913
  }
761
914
  return err(
762
915
  serviceError(errorCode, message, "sql", { meta })
@@ -853,6 +1006,9 @@ function serializeSqlValues(values) {
853
1006
  (value) => value instanceof Uint8Array ? Array.from(value) : value
854
1007
  );
855
1008
  }
1009
+ function isMissingTableError(message) {
1010
+ return /no such table/i.test(message);
1011
+ }
856
1012
  function migrationKey(namespace, id) {
857
1013
  return `${encodeURIComponent(namespace)}:${encodeURIComponent(id)}`;
858
1014
  }