@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.
package/dist/sql/index.js CHANGED
@@ -71,6 +71,7 @@ function serviceError(code, message, service, options) {
71
71
  }
72
72
 
73
73
  // src/errors.ts
74
+ import { CAPABILITY_REGISTRY } from "@tinycloud/bootstrap";
74
75
  function authRequiredError(service) {
75
76
  return {
76
77
  code: ErrorCodes.AUTH_REQUIRED,
@@ -78,11 +79,31 @@ function authRequiredError(service) {
78
79
  service
79
80
  };
80
81
  }
81
- function timeoutError(service) {
82
+ var RequestTimeoutError = class extends Error {
83
+ constructor(timeoutMs) {
84
+ super(`Request timed out after ${timeoutMs}ms.`);
85
+ this.name = "TimeoutError";
86
+ this.timeoutMs = timeoutMs;
87
+ }
88
+ };
89
+ function isRequestTimeoutError(error) {
90
+ return error instanceof RequestTimeoutError || error instanceof Error && error.name === "TimeoutError" && typeof error.timeoutMs === "number";
91
+ }
92
+ function timeoutError(service, timeoutMs, cause) {
93
+ if (timeoutMs === void 0) {
94
+ return {
95
+ code: ErrorCodes.TIMEOUT,
96
+ message: "Request timed out.",
97
+ service,
98
+ ...cause === void 0 ? {} : { cause }
99
+ };
100
+ }
82
101
  return {
83
102
  code: ErrorCodes.TIMEOUT,
84
- message: "Request timed out.",
85
- service
103
+ message: `Request timed out after ${timeoutMs}ms.`,
104
+ service,
105
+ ...cause === void 0 ? {} : { cause },
106
+ meta: { timeoutMs }
86
107
  };
87
108
  }
88
109
  function abortedError(service) {
@@ -99,6 +120,12 @@ function parseAuthError(responseText) {
99
120
  }
100
121
  return {};
101
122
  }
123
+ var grantableActions = new Set(
124
+ CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
125
+ );
126
+ var capabilityServices = new Set(
127
+ CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
128
+ );
102
129
  function storageQuotaExceededError(service, message, meta) {
103
130
  return {
104
131
  code: ErrorCodes.STORAGE_QUOTA_EXCEEDED,
@@ -131,6 +158,9 @@ function storageRejectionError(service, status, meta, responseText) {
131
158
  }
132
159
  function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
133
160
  if (error instanceof Error) {
161
+ if (isRequestTimeoutError(error)) {
162
+ return timeoutError(service, error.timeoutMs, error);
163
+ }
134
164
  if (error.name === "AbortError") {
135
165
  return abortedError(service);
136
166
  }
@@ -152,6 +182,7 @@ function wrapError(service, error, defaultCode = ErrorCodes.NETWORK_ERROR) {
152
182
  }
153
183
 
154
184
  // src/base/BaseService.ts
185
+ var MAX_TIMER_DELAY_MS = 2147483647;
155
186
  var BaseService = class {
156
187
  constructor() {
157
188
  /**
@@ -332,6 +363,64 @@ var BaseService = class {
332
363
  }
333
364
  return controller.signal;
334
365
  }
366
+ /**
367
+ * Create the abort signal for one request, bounded by the request timeout.
368
+ *
369
+ * The signal aborts when the service signs out, the context aborts, the
370
+ * caller's `signal` aborts, or the timeout elapses, whichever comes first.
371
+ * A timeout aborts with a {@link RequestTimeoutError} (`TimeoutError`), which
372
+ * `wrapError` maps to `ErrorCodes.TIMEOUT`; the other sources keep their own
373
+ * reason (normally `AbortError`, mapped to `ErrorCodes.ABORTED`).
374
+ *
375
+ * The timeout is `timeoutMs` when given, otherwise `config.timeout`. Only a
376
+ * positive, finite value (at most 2^31-1 ms) applies; `undefined`, `0`, and
377
+ * `Infinity` mean no timeout, so a per-call `0` opts out of a configured one.
378
+ *
379
+ * Callers must call `dispose()` once the response body has been consumed,
380
+ * which clears the timer and removes the listeners this adds to the
381
+ * long-lived service and context signals.
382
+ *
383
+ * @param signal - The caller's abort signal for this request
384
+ * @param timeoutMs - Per-request timeout override in milliseconds
385
+ */
386
+ createRequestSignal(signal, timeoutMs) {
387
+ const controller = new AbortController();
388
+ const detachers = [];
389
+ let timer;
390
+ const dispose = () => {
391
+ if (timer !== void 0) {
392
+ clearTimeout(timer);
393
+ timer = void 0;
394
+ }
395
+ for (const detach of detachers.splice(0)) {
396
+ detach();
397
+ }
398
+ };
399
+ const abort = (reason) => {
400
+ dispose();
401
+ controller.abort(reason);
402
+ };
403
+ const requestSignal = { signal: controller.signal, dispose };
404
+ const parents = [
405
+ this.abortController.signal,
406
+ this.context?.abortSignal,
407
+ signal
408
+ ].filter((parent) => parent !== void 0);
409
+ for (const parent of parents) {
410
+ if (parent.aborted) {
411
+ abort(parent.reason);
412
+ return requestSignal;
413
+ }
414
+ const onAbort = () => abort(parent.reason);
415
+ parent.addEventListener("abort", onAbort, { once: true });
416
+ detachers.push(() => parent.removeEventListener("abort", onAbort));
417
+ }
418
+ const timeout = timeoutMs ?? this._config.timeout;
419
+ if (typeof timeout === "number" && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS) {
420
+ timer = setTimeout(() => abort(new RequestTimeoutError(timeout)), timeout);
421
+ }
422
+ return requestSignal;
423
+ }
335
424
  /**
336
425
  * Wrap an operation with error handling and telemetry.
337
426
  *
@@ -520,6 +609,7 @@ var SQLService = class extends BaseService {
520
609
  if (!this.requireAuth()) {
521
610
  return err(authRequiredError("sql"));
522
611
  }
612
+ const request = this.createRequestSignal(options?.signal);
523
613
  try {
524
614
  const body = {
525
615
  action: "query",
@@ -528,19 +618,22 @@ var SQLService = class extends BaseService {
528
618
  };
529
619
  if (options?.maxRows !== void 0) body.maxRows = options.maxRows;
530
620
  if (options?.maxBytes !== void 0) body.maxBytes = options.maxBytes;
621
+ const action = this.actionForSql(sql, SQLAction.READ);
531
622
  const response = await this.invokeSQL(
532
623
  dbName,
533
- this.actionForSql(sql, SQLAction.READ),
624
+ action,
534
625
  body,
535
- options?.signal
626
+ request.signal
536
627
  );
537
628
  if (!response.ok) {
538
- return this.handleErrorResponse(response, "query");
629
+ return this.handleErrorResponse(response, "query", dbName, [action]);
539
630
  }
540
631
  const data = await response.json();
541
632
  return ok(data);
542
633
  } catch (error) {
543
634
  return err(wrapError("sql", error));
635
+ } finally {
636
+ request.dispose();
544
637
  }
545
638
  });
546
639
  }
@@ -549,6 +642,7 @@ var SQLService = class extends BaseService {
549
642
  if (!this.requireAuth()) {
550
643
  return err(authRequiredError("sql"));
551
644
  }
645
+ const request = this.createRequestSignal(options?.signal);
552
646
  try {
553
647
  const body = {
554
648
  action: "execute",
@@ -564,19 +658,22 @@ var SQLService = class extends BaseService {
564
658
  (statement) => this.actionForSql(statement, SQLAction.SCHEMA)
565
659
  )
566
660
  ];
661
+ const requestedActions = this.dedupeActions(actions);
567
662
  const response = await this.invokeSQL(
568
663
  dbName,
569
- this.dedupeActions(actions),
664
+ requestedActions,
570
665
  body,
571
- options?.signal
666
+ request.signal
572
667
  );
573
668
  if (!response.ok) {
574
- return this.handleErrorResponse(response, "execute");
669
+ return this.handleErrorResponse(response, "execute", dbName, requestedActions);
575
670
  }
576
671
  const data = await response.json();
577
672
  return ok(data);
578
673
  } catch (error) {
579
674
  return err(wrapError("sql", error));
675
+ } finally {
676
+ request.dispose();
580
677
  }
581
678
  });
582
679
  }
@@ -585,20 +682,24 @@ var SQLService = class extends BaseService {
585
682
  if (!this.requireAuth()) {
586
683
  return err(authRequiredError("sql"));
587
684
  }
685
+ const request = this.createRequestSignal(options?.signal);
588
686
  try {
687
+ const requestedActions = this.actionsForSqlBatch(statements);
589
688
  const response = await this.invokeSQL(
590
689
  dbName,
591
- this.actionsForSqlBatch(statements),
690
+ requestedActions,
592
691
  { action: "batch", statements },
593
- options?.signal
692
+ request.signal
594
693
  );
595
694
  if (!response.ok) {
596
- return this.handleErrorResponse(response, "batch");
695
+ return this.handleErrorResponse(response, "batch", dbName, requestedActions);
597
696
  }
598
697
  const data = await response.json();
599
698
  return ok(data);
600
699
  } catch (error) {
601
700
  return err(wrapError("sql", error));
701
+ } finally {
702
+ request.dispose();
602
703
  }
603
704
  });
604
705
  }
@@ -607,20 +708,23 @@ var SQLService = class extends BaseService {
607
708
  if (!this.requireAuth()) {
608
709
  return err(authRequiredError("sql"));
609
710
  }
711
+ const request = this.createRequestSignal(options?.signal);
610
712
  try {
611
713
  const response = await this.invokeSQL(
612
714
  dbName,
613
715
  SQLAction.WRITE,
614
716
  { action: "execute_statement", name, params: params ?? [] },
615
- options?.signal
717
+ request.signal
616
718
  );
617
719
  if (!response.ok) {
618
- return this.handleErrorResponse(response, "executeStatement");
720
+ return this.handleErrorResponse(response, "executeStatement", dbName, [SQLAction.WRITE]);
619
721
  }
620
722
  const data = await response.json();
621
723
  return ok(data);
622
724
  } catch (error) {
623
725
  return err(wrapError("sql", error));
726
+ } finally {
727
+ request.dispose();
624
728
  }
625
729
  });
626
730
  }
@@ -629,15 +733,16 @@ var SQLService = class extends BaseService {
629
733
  if (!this.requireAuth()) {
630
734
  return err(authRequiredError("sql"));
631
735
  }
736
+ const request = this.createRequestSignal(options?.signal);
632
737
  try {
633
738
  const response = await this.invokeSQL(
634
739
  dbName,
635
740
  SQLAction.READ,
636
741
  { action: "export" },
637
- options?.signal
742
+ request.signal
638
743
  );
639
744
  if (!response.ok) {
640
- return this.handleErrorResponse(response, "export");
745
+ return this.handleErrorResponse(response, "export", dbName, [SQLAction.READ]);
641
746
  }
642
747
  const resp = response;
643
748
  if (typeof resp.blob === "function") {
@@ -648,6 +753,8 @@ var SQLService = class extends BaseService {
648
753
  return ok(text);
649
754
  } catch (error) {
650
755
  return err(wrapError("sql", error));
756
+ } finally {
757
+ request.dispose();
651
758
  }
652
759
  });
653
760
  }
@@ -738,7 +845,7 @@ var SQLService = class extends BaseService {
738
845
  "Content-Type": "application/json"
739
846
  },
740
847
  body: JSON.stringify(body),
741
- signal: this.combineSignals(signal)
848
+ signal
742
849
  });
743
850
  }
744
851
  actionForSql(sql, fallback) {
@@ -771,7 +878,7 @@ var SQLService = class extends BaseService {
771
878
  }))
772
879
  );
773
880
  }
774
- async handleErrorResponse(response, operation) {
881
+ async handleErrorResponse(response, operation, dbName, requestedActions) {
775
882
  const errorText = await response.text();
776
883
  const meta = responseErrorMeta(response.status, response.statusText, errorText);
777
884
  if (response.status === 402) {
@@ -790,9 +897,11 @@ var SQLService = class extends BaseService {
790
897
  errorBody
791
898
  );
792
899
  if (response.status === 401) {
793
- const { resource, action } = parseAuthError(errorText);
794
- if (action) meta.requiredAction = action;
795
- if (resource) meta.resource = resource;
900
+ const { action } = parseAuthError(errorText);
901
+ if (action && requestedActions.includes(action)) {
902
+ meta.requiredAction = action;
903
+ meta.resource = `${this.context.session.spaceId}/sql/${dbName}`;
904
+ }
796
905
  }
797
906
  return err(
798
907
  serviceError(errorCode, message, "sql", { meta })