@tinycloud/sdk-services 3.1.0-beta.2 → 3.1.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.
@@ -100,6 +100,7 @@ function serviceError(code, message, service, options) {
100
100
  }
101
101
 
102
102
  // src/errors.ts
103
+ var import_bootstrap = require("@tinycloud/bootstrap");
103
104
  function authRequiredError(service) {
104
105
  return {
105
106
  code: ErrorCodes.AUTH_REQUIRED,
@@ -107,11 +108,31 @@ function authRequiredError(service) {
107
108
  service
108
109
  };
109
110
  }
110
- function timeoutError(service) {
111
+ var RequestTimeoutError = class extends Error {
112
+ constructor(timeoutMs) {
113
+ super(`Request timed out after ${timeoutMs}ms.`);
114
+ this.name = "TimeoutError";
115
+ this.timeoutMs = timeoutMs;
116
+ }
117
+ };
118
+ function isRequestTimeoutError(error) {
119
+ return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
120
+ }
121
+ function timeoutError(service, timeoutMs, cause) {
122
+ if (timeoutMs === void 0) {
123
+ return {
124
+ code: ErrorCodes.TIMEOUT,
125
+ message: "Request timed out.",
126
+ service,
127
+ ...cause === void 0 ? {} : { cause }
128
+ };
129
+ }
111
130
  return {
112
131
  code: ErrorCodes.TIMEOUT,
113
- message: "Request timed out.",
114
- service
132
+ message: `Request timed out after ${timeoutMs}ms.`,
133
+ service,
134
+ ...cause === void 0 ? {} : { cause },
135
+ meta: { timeoutMs }
115
136
  };
116
137
  }
117
138
  function abortedError(service) {
@@ -128,6 +149,12 @@ function parseAuthError(responseText) {
128
149
  }
129
150
  return {};
130
151
  }
152
+ var grantableActions = new Set(
153
+ import_bootstrap.CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
154
+ );
155
+ var capabilityServices = new Set(
156
+ import_bootstrap.CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
157
+ );
131
158
  function storageQuotaExceededError(service, message, meta) {
132
159
  return {
133
160
  code: ErrorCodes.STORAGE_QUOTA_EXCEEDED,
@@ -160,6 +187,9 @@ function storageRejectionError(service, status, meta, responseText) {
160
187
  }
161
188
  function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
162
189
  if (error instanceof Error) {
190
+ if (isRequestTimeoutError(error)) {
191
+ return timeoutError(service, error.timeoutMs, error);
192
+ }
163
193
  if (error.name === "AbortError") {
164
194
  return abortedError(service);
165
195
  }
@@ -181,6 +211,7 @@ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
181
211
  }
182
212
 
183
213
  // src/base/BaseService.ts
214
+ var MAX_TIMER_DELAY_MS = 2147483647;
184
215
  var BaseService = class {
185
216
  constructor() {
186
217
  /**
@@ -361,6 +392,64 @@ var BaseService = class {
361
392
  }
362
393
  return controller.signal;
363
394
  }
395
+ /**
396
+ * Create the abort signal for one request, bounded by the request timeout.
397
+ *
398
+ * The signal aborts when the service signs out, the context aborts, the
399
+ * caller's `signal` aborts, or the timeout elapses, whichever comes first.
400
+ * A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
401
+ * `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
402
+ * reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
403
+ *
404
+ * The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
405
+ * positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
406
+ * `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
407
+ *
408
+ * Callers must call `dispose()` once the response body has been consumed,
409
+ * which clears the timer and removes the listeners this adds to the
410
+ * long-lived service and context signals.
411
+ *
412
+ * @param signal - The caller's abort signal for this request
413
+ * @param timeoutMs - Per-request timeout override in milliseconds
414
+ */
415
+ createRequestSignal(signal, timeoutMs) {
416
+ const controller = new AbortController();
417
+ const detachers = [];
418
+ let timer;
419
+ const dispose = () => {
420
+ if (timer !== void 0) {
421
+ clearTimeout(timer);
422
+ timer = void 0;
423
+ }
424
+ for (const detach of detachers.splice(0)) {
425
+ detach();
426
+ }
427
+ };
428
+ const abort = (reason) => {
429
+ dispose();
430
+ controller.abort(reason);
431
+ };
432
+ const requestSignal = { signal: controller.signal, dispose };
433
+ const parents = [
434
+ this.abortController.signal,
435
+ this.context?.abortSignal,
436
+ signal
437
+ ].filter((parent) => parent !== void 0);
438
+ for (const parent of parents) {
439
+ if (parent.aborted) {
440
+ abort(parent.reason);
441
+ return requestSignal;
442
+ }
443
+ const onAbort = () => abort(parent.reason);
444
+ parent.addEventListener("abort", onAbort, { once: true });
445
+ detachers.push(() => parent.removeEventListener("abort", onAbort));
446
+ }
447
+ const timeout = timeoutMs ?? this._config.timeout;
448
+ if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
449
+ timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
450
+ }
451
+ return requestSignal;
452
+ }
364
453
  /**
365
454
  * Wrap an operation with error handling and telemetry.
366
455
  *
@@ -462,16 +551,16 @@ var DatabaseHandle = class {
462
551
  };
463
552
 
464
553
  // src/sql/types.ts
465
- var import_bootstrap = require("@tinycloud/bootstrap");
554
+ var import_bootstrap2 = require("@tinycloud/bootstrap");
466
555
  var SQLAction = {
467
- READ: import_bootstrap.SQL.READ,
468
- WRITE: import_bootstrap.SQL.WRITE,
469
- SCHEMA: import_bootstrap.SQL.SCHEMA,
470
- ADMIN: import_bootstrap.SQL.ADMIN,
471
- SELECT: import_bootstrap.SQL.SELECT,
472
- EXECUTE: import_bootstrap.SQL.EXECUTE,
473
- EXPORT: import_bootstrap.SQL.EXPORT,
474
- ALL: import_bootstrap.SQL.ALL
556
+ READ: import_bootstrap2.SQL.READ,
557
+ WRITE: import_bootstrap2.SQL.WRITE,
558
+ SCHEMA: import_bootstrap2.SQL.SCHEMA,
559
+ ADMIN: import_bootstrap2.SQL.ADMIN,
560
+ SELECT: import_bootstrap2.SQL.SELECT,
561
+ EXECUTE: import_bootstrap2.SQL.EXECUTE,
562
+ EXPORT: import_bootstrap2.SQL.EXPORT,
563
+ ALL: import_bootstrap2.SQL.ALL
475
564
  };
476
565
 
477
566
  // src/sql/SQLService.ts
@@ -549,6 +638,7 @@ var SQLService = class extends BaseService {
549
638
  if (!this.requireAuth()) {
550
639
  return err(authRequiredError("sql"));
551
640
  }
641
+ const request = this.createRequestSignal(options?.signal);
552
642
  try {
553
643
  const body = {
554
644
  action: "query",
@@ -557,19 +647,22 @@ var SQLService = class extends BaseService {
557
647
  };
558
648
  if (options?.maxRows !== void 0) body.maxRows = options.maxRows;
559
649
  if (options?.maxBytes !== void 0) body.maxBytes = options.maxBytes;
650
+ const action = this.actionForSql(sql, SQLAction.READ);
560
651
  const response = await this.invokeSQL(
561
652
  dbName,
562
- this.actionForSql(sql, SQLAction.READ),
653
+ action,
563
654
  body,
564
- options?.signal
655
+ request.signal
565
656
  );
566
657
  if (!response.ok) {
567
- return this.handleErrorResponse(response, "query");
658
+ return this.handleErrorResponse(response, "query", dbName, [action]);
568
659
  }
569
660
  const data = await response.json();
570
661
  return ok(data);
571
662
  } catch (error) {
572
663
  return err(wrapError("sql", error));
664
+ } finally {
665
+ request.dispose();
573
666
  }
574
667
  });
575
668
  }
@@ -578,6 +671,7 @@ var SQLService = class extends BaseService {
578
671
  if (!this.requireAuth()) {
579
672
  return err(authRequiredError("sql"));
580
673
  }
674
+ const request = this.createRequestSignal(options?.signal);
581
675
  try {
582
676
  const body = {
583
677
  action: "execute",
@@ -593,19 +687,22 @@ var SQLService = class extends BaseService {
593
687
  (statement) => this.actionForSql(statement, SQLAction.SCHEMA)
594
688
  )
595
689
  ];
690
+ const requestedActions = this.dedupeActions(actions);
596
691
  const response = await this.invokeSQL(
597
692
  dbName,
598
- this.dedupeActions(actions),
693
+ requestedActions,
599
694
  body,
600
- options?.signal
695
+ request.signal
601
696
  );
602
697
  if (!response.ok) {
603
- return this.handleErrorResponse(response, "execute");
698
+ return this.handleErrorResponse(response, "execute", dbName, requestedActions);
604
699
  }
605
700
  const data = await response.json();
606
701
  return ok(data);
607
702
  } catch (error) {
608
703
  return err(wrapError("sql", error));
704
+ } finally {
705
+ request.dispose();
609
706
  }
610
707
  });
611
708
  }
@@ -614,20 +711,24 @@ var SQLService = class extends BaseService {
614
711
  if (!this.requireAuth()) {
615
712
  return err(authRequiredError("sql"));
616
713
  }
714
+ const request = this.createRequestSignal(options?.signal);
617
715
  try {
716
+ const requestedActions = this.actionsForSqlBatch(statements);
618
717
  const response = await this.invokeSQL(
619
718
  dbName,
620
- this.actionsForSqlBatch(statements),
719
+ requestedActions,
621
720
  { action: "batch", statements },
622
- options?.signal
721
+ request.signal
623
722
  );
624
723
  if (!response.ok) {
625
- return this.handleErrorResponse(response, "batch");
724
+ return this.handleErrorResponse(response, "batch", dbName, requestedActions);
626
725
  }
627
726
  const data = await response.json();
628
727
  return ok(data);
629
728
  } catch (error) {
630
729
  return err(wrapError("sql", error));
730
+ } finally {
731
+ request.dispose();
631
732
  }
632
733
  });
633
734
  }
@@ -636,20 +737,23 @@ var SQLService = class extends BaseService {
636
737
  if (!this.requireAuth()) {
637
738
  return err(authRequiredError("sql"));
638
739
  }
740
+ const request = this.createRequestSignal(options?.signal);
639
741
  try {
640
742
  const response = await this.invokeSQL(
641
743
  dbName,
642
744
  SQLAction.WRITE,
643
745
  { action: "execute_statement", name, params: params ?? [] },
644
- options?.signal
746
+ request.signal
645
747
  );
646
748
  if (!response.ok) {
647
- return this.handleErrorResponse(response, "executeStatement");
749
+ return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
648
750
  }
649
751
  const data = await response.json();
650
752
  return ok(data);
651
753
  } catch (error) {
652
754
  return err(wrapError("sql", error));
755
+ } finally {
756
+ request.dispose();
653
757
  }
654
758
  });
655
759
  }
@@ -658,15 +762,16 @@ var SQLService = class extends BaseService {
658
762
  if (!this.requireAuth()) {
659
763
  return err(authRequiredError("sql"));
660
764
  }
765
+ const request = this.createRequestSignal(options?.signal);
661
766
  try {
662
767
  const response = await this.invokeSQL(
663
768
  dbName,
664
769
  SQLAction.READ,
665
770
  { action: "export" },
666
- options?.signal
771
+ request.signal
667
772
  );
668
773
  if (!response.ok) {
669
- return this.handleErrorResponse(response, "export");
774
+ return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
670
775
  }
671
776
  const resp = response;
672
777
  if (typeof resp.blob === "function") {
@@ -677,6 +782,8 @@ var SQLService = class extends BaseService {
677
782
  return ok(text);
678
783
  } catch (error) {
679
784
  return err(wrapError("sql", error));
785
+ } finally {
786
+ request.dispose();
680
787
  }
681
788
  });
682
789
  }
@@ -767,7 +874,7 @@ var SQLService = class extends BaseService {
767
874
  "Content-Type": "application/json"
768
875
  },
769
876
  body: JSON.stringify(body),
770
- signal: this.combineSignals(signal)
877
+ signal
771
878
  });
772
879
  }
773
880
  actionForSql(sql, fallback) {
@@ -800,7 +907,7 @@ var SQLService = class extends BaseService {
800
907
  }))
801
908
  );
802
909
  }
803
- async handleErrorResponse(response, operation) {
910
+ async handleErrorResponse(response, operation, dbName, requestedActions) {
804
911
  const errorText = await response.text();
805
912
  const meta = responseErrorMeta(response.status, response.statusText, errorText);
806
913
  if (response.status === 402) {
@@ -819,9 +926,11 @@ var SQLService = class extends BaseService {
819
926
  errorBody
820
927
  );
821
928
  if (response.status === 401) {
822
- const { resource, action } = parseAuthError(errorText);
823
- if (action) meta.requiredAction = action;
824
- if (resource) meta.resource = resource;
929
+ const { action } = parseAuthError(errorText);
930
+ if (action && requestedActions.includes(action)) {
931
+ meta.requiredAction = action;
932
+ meta.resource = `${this.context.session.spaceId}/sql/${dbName}`;
933
+ }
825
934
  }
826
935
  return err(
827
936
  serviceError(errorCode, message, "sql", { meta })