@relyio/sdk 0.2.1 → 0.4.0

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 (39) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/{chunk-RJBIJ2ZK.js → chunk-43VPZT2G.js} +496 -103
  4. package/dist/chunk-BBUB7C2Z.js +81 -0
  5. package/dist/{chunk-FJK7ZWKK.cjs → chunk-HAQELDZN.cjs} +156 -221
  6. package/dist/chunk-LSRPMNPT.cjs +89 -0
  7. package/dist/{chunk-7W4MJXQ4.cjs → chunk-XS32U2UP.cjs} +497 -102
  8. package/dist/{chunk-2QUK2ZGD.js → chunk-XY5KFEFY.js} +157 -219
  9. package/dist/client/index.cjs +4 -3
  10. package/dist/client/index.d.cts +4 -3
  11. package/dist/client/index.d.ts +4 -3
  12. package/dist/client/index.js +2 -1
  13. package/dist/define-content-type-CBfnTWSS.d.cts +716 -0
  14. package/dist/define-content-type-CBfnTWSS.d.ts +716 -0
  15. package/dist/{define-schema-DHvUCKwj.d.cts → define-schema-2xbHgtK-.d.cts} +63 -726
  16. package/dist/{define-schema-DHvUCKwj.d.ts → define-schema-DzYXk4HL.d.ts} +63 -726
  17. package/dist/dev-handshake/index.cjs +37 -0
  18. package/dist/dev-handshake/index.d.cts +144 -0
  19. package/dist/dev-handshake/index.d.ts +144 -0
  20. package/dist/dev-handshake/index.js +4 -0
  21. package/dist/{index-DGWaawHt.d.cts → index-BvO6HDtY.d.cts} +237 -12
  22. package/dist/{index-B-KiFHbx.d.ts → index-BzOFJJKS.d.ts} +237 -12
  23. package/dist/{index-BsUqCAu8.d.cts → index-DFehx5sI.d.cts} +749 -2160
  24. package/dist/{index-BsUqCAu8.d.ts → index-FDak5ofk.d.ts} +749 -2160
  25. package/dist/index.cjs +120 -70
  26. package/dist/index.d.cts +136 -55
  27. package/dist/index.d.ts +136 -55
  28. package/dist/index.js +45 -3
  29. package/dist/schema/index.cjs +35 -47
  30. package/dist/schema/index.d.cts +3 -2
  31. package/dist/schema/index.d.ts +3 -2
  32. package/dist/schema/index.js +1 -1
  33. package/dist/types/index.d.cts +2 -1
  34. package/dist/types/index.d.ts +2 -1
  35. package/package.json +15 -19
  36. package/dist/react/index.cjs +0 -4
  37. package/dist/react/index.d.cts +0 -2
  38. package/dist/react/index.d.ts +0 -2
  39. package/dist/react/index.js +0 -3
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var chunkLSRPMNPT_cjs = require('./chunk-LSRPMNPT.cjs');
3
4
  var chunkMWMSFIWN_cjs = require('./chunk-MWMSFIWN.cjs');
4
5
 
5
6
  /* RelyIO SDK - Built with ❤️ by the RelyIO Team */
@@ -167,14 +168,17 @@ var _UsageQuotaExceededError = class _UsageQuotaExceededError extends SDKError {
167
168
  chunkMWMSFIWN_cjs.__name(_UsageQuotaExceededError, "UsageQuotaExceededError");
168
169
  var UsageQuotaExceededError = _UsageQuotaExceededError;
169
170
  var _RateLimitError = class _RateLimitError extends SDKError {
170
- constructor(message = "Rate limit exceeded", limit, resetAt, context) {
171
+ constructor(message = "Rate limit exceeded", limit, resetAt, context, retryAfterMs) {
171
172
  super(message, 429, void 0, context);
172
173
  chunkMWMSFIWN_cjs.__publicField(this, "code", "RATE_LIMIT_EXCEEDED");
173
174
  chunkMWMSFIWN_cjs.__publicField(this, "retryable", true);
174
175
  chunkMWMSFIWN_cjs.__publicField(this, "limit");
175
176
  chunkMWMSFIWN_cjs.__publicField(this, "resetAt");
177
+ /** `Retry-After`, in milliseconds, where the response carried a readable one. */
178
+ chunkMWMSFIWN_cjs.__publicField(this, "retryAfterMs");
176
179
  this.limit = limit;
177
180
  this.resetAt = resetAt;
181
+ this.retryAfterMs = retryAfterMs;
178
182
  }
179
183
  getUserMessage() {
180
184
  return this.resetAt === void 0 ? "Too many requests. Please retry shortly." : `Too many requests. The window resets at ${this.resetAt}.`;
@@ -223,12 +227,15 @@ var _TransactionRolledBackError = class _TransactionRolledBackError extends SDKE
223
227
  chunkMWMSFIWN_cjs.__name(_TransactionRolledBackError, "TransactionRolledBackError");
224
228
  var TransactionRolledBackError = _TransactionRolledBackError;
225
229
  var _ServiceIndeterminateError = class _ServiceIndeterminateError extends SDKError {
226
- constructor(message = "The service could not decide this request", missing, context) {
230
+ constructor(message = "The service could not decide this request", missing, context, retryAfterMs) {
227
231
  super(message, 503, void 0, context);
228
232
  chunkMWMSFIWN_cjs.__publicField(this, "code", "SERVICE_INDETERMINATE");
229
233
  chunkMWMSFIWN_cjs.__publicField(this, "retryable", true);
230
234
  chunkMWMSFIWN_cjs.__publicField(this, "missing");
235
+ /** `Retry-After`, in milliseconds. The contract requires the header on this code. */
236
+ chunkMWMSFIWN_cjs.__publicField(this, "retryAfterMs");
231
237
  this.missing = missing;
238
+ this.retryAfterMs = retryAfterMs;
232
239
  }
233
240
  getUserMessage() {
234
241
  return "The service is temporarily unable to answer. Please retry.";
@@ -459,6 +466,14 @@ var _InvalidFieldTypeError = class _InvalidFieldTypeError extends BaseValidation
459
466
  chunkMWMSFIWN_cjs.__name(_InvalidFieldTypeError, "InvalidFieldTypeError");
460
467
  var InvalidFieldTypeError = _InvalidFieldTypeError;
461
468
 
469
+ // ../../platform/api-contract/dist/index.js
470
+ var HEADER_RETRY_AFTER = { name: "Retry-After" };
471
+ var HEADER_ENTITY_TAG = { name: "X-Entity-Tag" };
472
+ var HEADER_IF_MATCH = {
473
+ name: "If-Match"};
474
+ var HEADER_IDEMPOTENCY_KEY = {
475
+ name: "Idempotency-Key"};
476
+
462
477
  // ../../platform/api-contract/dist/client/index.js
463
478
  var _a;
464
479
  var ContractBodyError = (_a = class extends Error {
@@ -502,19 +517,47 @@ function readError(body) {
502
517
  }
503
518
  chunkMWMSFIWN_cjs.__name(readError, "readError");
504
519
 
520
+ // src/http/retry-after.ts
521
+ function headerValue(headers, name) {
522
+ if (headers === void 0) return void 0;
523
+ const wanted = name.toLowerCase();
524
+ for (const [key, value] of Object.entries(headers)) {
525
+ if (key.toLowerCase() === wanted) return value;
526
+ }
527
+ return void 0;
528
+ }
529
+ chunkMWMSFIWN_cjs.__name(headerValue, "headerValue");
530
+ function parseRetryAfterMs(value, nowMs) {
531
+ if (value === void 0) return void 0;
532
+ const trimmed = value.trim();
533
+ if (trimmed === "") return void 0;
534
+ if (/^\d+$/.test(trimmed)) {
535
+ return Number(trimmed) * 1e3;
536
+ }
537
+ if (!/[A-Za-z]/.test(trimmed)) return void 0;
538
+ const at = Date.parse(trimmed);
539
+ if (Number.isNaN(at)) return void 0;
540
+ return Math.max(0, at - nowMs);
541
+ }
542
+ chunkMWMSFIWN_cjs.__name(parseRetryAfterMs, "parseRetryAfterMs");
543
+
505
544
  // src/errors/error-mapper.ts
506
545
  var _ErrorMapper = class _ErrorMapper {
507
546
  /** Map a non-2xx response to an SDK error. */
508
547
  static mapHttpError(response, requestContext) {
509
- const { status, statusText, body } = response;
548
+ const { status, statusText, body, headers } = response;
510
549
  const refusal = readError(body);
511
550
  const context = { ...requestContext, response: body };
512
551
  if (refusal === void 0) {
513
552
  return new NetworkError(`HTTP ${status}: ${statusText}`.trim(), status, void 0, context);
514
553
  }
515
- return this.fromContractCode(refusal, status, context);
554
+ const retryAfterMs = parseRetryAfterMs(
555
+ headerValue(headers, HEADER_RETRY_AFTER.name),
556
+ Date.now()
557
+ );
558
+ return this.fromContractCode(refusal, status, context, retryAfterMs);
516
559
  }
517
- static fromContractCode(refusal, status, context) {
560
+ static fromContractCode(refusal, status, context, retryAfterMs) {
518
561
  const message = refusal.messages[0] ?? `RelyIO refused with ${refusal.code}`;
519
562
  switch (refusal.code) {
520
563
  case "MISSING_API_KEY": {
@@ -545,7 +588,8 @@ var _ErrorMapper = class _ErrorMapper {
545
588
  message,
546
589
  detailNumber(refusal, "limit"),
547
590
  detailString(refusal, "resetAt"),
548
- context
591
+ context,
592
+ retryAfterMs
549
593
  );
550
594
  }
551
595
  case "REQUEST_INVALID": {
@@ -588,7 +632,12 @@ var _ErrorMapper = class _ErrorMapper {
588
632
  );
589
633
  }
590
634
  case "SERVICE_INDETERMINATE": {
591
- return new ServiceIndeterminateError(message, detailString(refusal, "missing"), context);
635
+ return new ServiceIndeterminateError(
636
+ message,
637
+ detailString(refusal, "missing"),
638
+ context,
639
+ retryAfterMs
640
+ );
592
641
  }
593
642
  case "INTERNAL_ERROR": {
594
643
  return new ServerError(message, status, context);
@@ -630,12 +679,6 @@ var _ErrorMapper = class _ErrorMapper {
630
679
  }
631
680
  return error.name === "TypeError" && error.message.includes("fetch") || error.message.includes("timeout") || error.message.includes("Network Error") || error.message.includes("ECONNREFUSED");
632
681
  }
633
- /**
634
- * Extract retry delay from error
635
- */
636
- static getRetryDelay(_error) {
637
- return 0;
638
- }
639
682
  };
640
683
  chunkMWMSFIWN_cjs.__name(_ErrorMapper, "ErrorMapper");
641
684
  var ErrorMapper = _ErrorMapper;
@@ -693,6 +736,158 @@ function fillPath(template, params) {
693
736
  return filled;
694
737
  }
695
738
  chunkMWMSFIWN_cjs.__name(fillPath, "fillPath");
739
+ var DECLARED_PATHS = {
740
+ "/health": true,
741
+ "/health/cache": true,
742
+ "/health/live": true,
743
+ "/health/ready": true,
744
+ "/internal/media/claim": true,
745
+ "/internal/media/file-complete": true,
746
+ "/internal/media/progress": true,
747
+ "/internal/media/resolve": true,
748
+ "/internal/usage/cycle": true,
749
+ "/internal/usage/ingest": true,
750
+ "/v1/{contentType}": true,
751
+ "/v1/{contentType}/{id}": true,
752
+ "/v1/{contentType}/{id}/children": true,
753
+ "/v1/{contentType}/{id}/file": true,
754
+ "/v1/{contentType}/{id}/file/confirm": true,
755
+ "/v1/{contentType}/{id}/file/multipart/abort": true,
756
+ "/v1/{contentType}/{id}/file/multipart/complete": true,
757
+ "/v1/{contentType}/{id}/file/multipart/create": true,
758
+ "/v1/{contentType}/{id}/file/multipart/list-parts": true,
759
+ "/v1/{contentType}/{id}/file/multipart/sign-part": true,
760
+ "/v1/{contentType}/{id}/file/presign": true,
761
+ "/v1/{contentType}/{id}/file/url": true,
762
+ "/v1/{contentType}/{id}/versions": true,
763
+ "/v1/{contentType}/{id}/versions/{versionNumber}": true,
764
+ "/v1/{contentType}/{id}/versions/{versionNumber}/restore": true,
765
+ "/v1/batch": true,
766
+ "/v1/changes": true,
767
+ "/v1/meta/content-types": true,
768
+ "/v1/meta/info": true,
769
+ "/v1/meta/locales": true,
770
+ "/v1/meta/schema": true,
771
+ "/v1/meta/schema/{contentType}": true,
772
+ "/v1/meta/schema/{contentType}/fields": true,
773
+ "/v1/query": true,
774
+ "/v1/search": true,
775
+ "/v1/search/autocomplete": true,
776
+ "/v1/search/similar": true,
777
+ "/v1/settings": true,
778
+ "/v1/settings/{groupId}": true,
779
+ "/v1/settings/{groupId}/defaults": true,
780
+ "/v1/settings/{groupId}/reset": true,
781
+ "/v1/settings/{groupId}/versions": true,
782
+ "/v1/settings/{groupId}/versions/{versionNumber}": true,
783
+ "/v1/singletons/{key}": true,
784
+ "/v1/singletons/{key}/exists": true,
785
+ "/v1/singletons/{key}/versions": true,
786
+ "/v1/singletons/{key}/versions/{versionNumber}": true
787
+ };
788
+ function compile(path) {
789
+ const segments = path.split("/").slice(1).map((part) => {
790
+ const param = /^\{(\w+)\}$/.exec(part);
791
+ return param?.[1] === void 0 ? { kind: "static", value: part } : { kind: "param", name: param[1] };
792
+ });
793
+ return { path, segments };
794
+ }
795
+ chunkMWMSFIWN_cjs.__name(compile, "compile");
796
+ function byRouterPrecedence(a, b) {
797
+ const shared = Math.min(a.segments.length, b.segments.length);
798
+ for (let index = 0; index < shared; index += 1) {
799
+ const left = a.segments[index]?.kind;
800
+ const right = b.segments[index]?.kind;
801
+ if (left !== right) return left === "static" ? -1 : 1;
802
+ }
803
+ return 0;
804
+ }
805
+ chunkMWMSFIWN_cjs.__name(byRouterPrecedence, "byRouterPrecedence");
806
+ var COMPILED_ROUTES = Object.keys(DECLARED_PATHS).map(compile).sort(byRouterPrecedence);
807
+ function matchRoute(url, options = {}) {
808
+ const parsed = new URL(url, "http://relative.invalid");
809
+ let pathname = parsed.pathname;
810
+ if (options.baseUrl !== void 0) {
811
+ const prefix = new URL(options.baseUrl, "http://relative.invalid").pathname.replace(/\/+$/, "");
812
+ if (prefix !== "") {
813
+ if (pathname !== prefix && !pathname.startsWith(`${prefix}/`)) return null;
814
+ pathname = pathname.slice(prefix.length);
815
+ }
816
+ }
817
+ const parts = pathname.split("/").slice(1);
818
+ for (const route of COMPILED_ROUTES) {
819
+ if (route.segments.length !== parts.length) continue;
820
+ const params = {};
821
+ const matched = route.segments.every((segment, index) => {
822
+ const part = parts[index];
823
+ if (part === void 0) return false;
824
+ if (segment.kind === "static") return segment.value === part;
825
+ if (part === "") return false;
826
+ params[segment.name] = decodeURIComponent(part);
827
+ return true;
828
+ });
829
+ if (matched) return { path: route.path, params, query: readQuery(parsed.searchParams) };
830
+ }
831
+ return null;
832
+ }
833
+ chunkMWMSFIWN_cjs.__name(matchRoute, "matchRoute");
834
+ function readQuery(searchParams) {
835
+ const query = {};
836
+ for (const key of new Set(searchParams.keys())) {
837
+ const values = searchParams.getAll(key);
838
+ query[key] = values.length === 1 && values[0] !== void 0 ? values[0] : values;
839
+ }
840
+ return query;
841
+ }
842
+ chunkMWMSFIWN_cjs.__name(readQuery, "readQuery");
843
+
844
+ // src/http/retry-policy.ts
845
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
846
+ "GET",
847
+ "HEAD",
848
+ "OPTIONS",
849
+ "PUT",
850
+ "DELETE",
851
+ "TRACE"
852
+ ]);
853
+ function isReplaySafe(request) {
854
+ if (IDEMPOTENT_METHODS.has(request.method.toUpperCase())) return true;
855
+ return headerValue(request.headers, HEADER_IDEMPOTENCY_KEY.name) !== void 0;
856
+ }
857
+ chunkMWMSFIWN_cjs.__name(isReplaySafe, "isReplaySafe");
858
+ function serviceInstructedDelayMs(error, nowMs) {
859
+ if (error instanceof ServiceIndeterminateError) {
860
+ return error.retryAfterMs;
861
+ }
862
+ if (!(error instanceof RateLimitError)) return void 0;
863
+ if (error.retryAfterMs !== void 0) return error.retryAfterMs;
864
+ if (error.resetAt === void 0) return void 0;
865
+ const resetAtMs = Date.parse(error.resetAt);
866
+ if (Number.isNaN(resetAtMs)) return void 0;
867
+ const wait = resetAtMs - nowMs;
868
+ return wait > 0 ? wait : void 0;
869
+ }
870
+ chunkMWMSFIWN_cjs.__name(serviceInstructedDelayMs, "serviceInstructedDelayMs");
871
+ var _DefaultRetryPolicy = class _DefaultRetryPolicy {
872
+ constructor(config) {
873
+ chunkMWMSFIWN_cjs.__publicField(this, "config");
874
+ this.config = config;
875
+ }
876
+ shouldRetry(attempt, error, request) {
877
+ if (attempt >= this.config.maxAttempts) return false;
878
+ if (!ErrorMapper.isRetryable(error)) return false;
879
+ return isReplaySafe(request);
880
+ }
881
+ delayFor(attempt, error) {
882
+ const instructed = serviceInstructedDelayMs(error, Date.now());
883
+ if (instructed !== void 0) return instructed;
884
+ const exponential = this.config.baseDelay * Math.pow(this.config.backoffFactor, attempt - 1);
885
+ const delay = Math.min(exponential, this.config.maxDelay);
886
+ return this.config.jitter ? delay * (0.5 + Math.random() * 0.5) : delay;
887
+ }
888
+ };
889
+ chunkMWMSFIWN_cjs.__name(_DefaultRetryPolicy, "DefaultRetryPolicy");
890
+ var DefaultRetryPolicy = _DefaultRetryPolicy;
696
891
 
697
892
  // src/http/http-client.ts
698
893
  var _HttpClient = class _HttpClient {
@@ -700,7 +895,7 @@ var _HttpClient = class _HttpClient {
700
895
  chunkMWMSFIWN_cjs.__publicField(this, "baseUrl");
701
896
  chunkMWMSFIWN_cjs.__publicField(this, "defaultHeaders");
702
897
  chunkMWMSFIWN_cjs.__publicField(this, "timeout");
703
- chunkMWMSFIWN_cjs.__publicField(this, "retryConfig");
898
+ chunkMWMSFIWN_cjs.__publicField(this, "retryPolicy");
704
899
  chunkMWMSFIWN_cjs.__publicField(this, "debug");
705
900
  chunkMWMSFIWN_cjs.__publicField(this, "logger");
706
901
  chunkMWMSFIWN_cjs.__publicField(this, "config");
@@ -721,16 +916,13 @@ var _HttpClient = class _HttpClient {
721
916
  const globalWithVersion = globalThis;
722
917
  this.defaultHeaders["User-Agent"] = `RelyIO-SDK/${globalWithVersion.__VERSION__ ?? "0.0.0"}`;
723
918
  }
724
- this.retryConfig = {
919
+ this.retryPolicy = new DefaultRetryPolicy({
725
920
  maxAttempts: config.retryAttempts,
726
921
  baseDelay: 1e3,
727
922
  maxDelay: 1e4,
728
923
  backoffFactor: 2,
729
- jitter: true,
730
- retryCondition: /* @__PURE__ */ chunkMWMSFIWN_cjs.__name((error, attempt) => {
731
- return ErrorMapper.isRetryable(error) && attempt < config.retryAttempts;
732
- }, "retryCondition")
733
- };
924
+ jitter: true
925
+ });
734
926
  this.log("debug", "HttpClient initialized", {
735
927
  baseUrl: this.baseUrl,
736
928
  timeout: this.timeout,
@@ -763,7 +955,16 @@ var _HttpClient = class _HttpClient {
763
955
  return readPayload(response.body);
764
956
  }
765
957
  /**
766
- * Make an HTTP request with retry logic
958
+ * Make an HTTP request, retrying only where a retry is safe.
959
+ *
960
+ * The headers are merged **once**, before the first attempt, and every attempt
961
+ * sends that same object. That is what makes a minted `Idempotency-Key` stable
962
+ * across the retries of one logical write: the service sees one key and
963
+ * answers the replay from the original outcome instead of applying it twice.
964
+ *
965
+ * The loop always makes one attempt. The incumbent's `while (attempt <
966
+ * maxAttempts)` made none at `retryAttempts: 0` — a value the config validator
967
+ * accepts — and then dereferenced an unassigned `lastError`.
767
968
  */
768
969
  async request(config) {
769
970
  const mergedConfig = this.mergeRequestConfig(config);
@@ -772,14 +973,11 @@ var _HttpClient = class _HttpClient {
772
973
  url: mergedConfig.url
773
974
  // Show the final URL with query params
774
975
  });
775
- let lastError;
776
- let attempt = 0;
777
- while (attempt < this.retryConfig.maxAttempts) {
778
- attempt++;
976
+ if (config.method === "DELETE" && mergedConfig.headers) {
977
+ delete mergedConfig.headers["Content-Type"];
978
+ }
979
+ for (let attempt = 1; ; attempt++) {
779
980
  try {
780
- if (config.method === "DELETE" && mergedConfig.headers) {
781
- delete mergedConfig.headers["Content-Type"];
782
- }
783
981
  const response = await this.makeRequest(mergedConfig.url, mergedConfig);
784
982
  this.log("debug", "Request successful", {
785
983
  status: response.status,
@@ -788,31 +986,31 @@ var _HttpClient = class _HttpClient {
788
986
  });
789
987
  return response;
790
988
  } catch (error) {
791
- lastError = error;
989
+ const failure = error;
990
+ if (config.signal?.aborted === true) throw failure;
792
991
  this.log("warn", "Request failed", {
793
992
  error: error instanceof Error ? error.message : "Unknown error",
794
993
  attempt,
795
994
  url: mergedConfig.url
796
995
  });
797
- if (attempt < this.retryConfig.maxAttempts && this.retryConfig.retryCondition?.(lastError, attempt) === true) {
798
- const delay = this.calculateRetryDelay(attempt);
996
+ if (this.retryPolicy.shouldRetry(attempt, failure, mergedConfig)) {
997
+ const delay = this.retryPolicy.delayFor(attempt, failure);
799
998
  this.log("debug", "Retrying request", { attempt, delay });
800
- await this.sleep(delay);
999
+ await this.sleep(delay, config.signal);
801
1000
  continue;
802
1001
  }
803
- break;
1002
+ const sdkError = ErrorMapper.mapGenericError(failure, {
1003
+ url: mergedConfig.url,
1004
+ method: mergedConfig.method,
1005
+ attempt
1006
+ });
1007
+ this.log("error", "Request failed after all retries", {
1008
+ error: sdkError.toJSON(),
1009
+ url: mergedConfig.url
1010
+ });
1011
+ throw sdkError;
804
1012
  }
805
1013
  }
806
- const sdkError = ErrorMapper.mapGenericError(lastError, {
807
- url: mergedConfig.url,
808
- method: mergedConfig.method,
809
- attempt
810
- });
811
- this.log("error", "Request failed after all retries", {
812
- error: sdkError.toJSON(),
813
- url: mergedConfig.url
814
- });
815
- throw sdkError;
816
1014
  }
817
1015
  /**
818
1016
  * Make a GET request
@@ -897,20 +1095,41 @@ var _HttpClient = class _HttpClient {
897
1095
  getBaseUrl() {
898
1096
  return this.baseUrl;
899
1097
  }
1098
+ /**
1099
+ * One attempt.
1100
+ *
1101
+ * The caller's `signal` and the timeout both end it, through one controller.
1102
+ * The incumbent handed `fetch` the caller's signal **instead of** its own, so a
1103
+ * request that passed a signal had no timeout at all — and a caller abort then
1104
+ * surfaced as `Request timed out`. `timedOut` is what tells the two apart: a
1105
+ * timeout is a `TimeoutError`, a caller abort is the platform's `AbortError`,
1106
+ * rethrown as it came.
1107
+ */
900
1108
  async makeRequest(url, config) {
1109
+ const caller = config.signal;
1110
+ if (caller?.aborted === true) {
1111
+ throw new DOMException("The request was aborted before it was sent", "AbortError");
1112
+ }
901
1113
  const controller = new AbortController();
902
- const timeoutId = setTimeout(() => controller.abort(), config.timeout ?? this.timeout);
1114
+ let timedOut = false;
1115
+ const timeoutId = setTimeout(() => {
1116
+ timedOut = true;
1117
+ controller.abort();
1118
+ }, config.timeout ?? this.timeout);
1119
+ const onCallerAbort = /* @__PURE__ */ chunkMWMSFIWN_cjs.__name(() => controller.abort(), "onCallerAbort");
1120
+ caller?.addEventListener("abort", onCallerAbort, { once: true });
903
1121
  try {
904
1122
  const filteredHeaders = this.filterHeadersForBrowser(config.headers ?? {});
905
1123
  const requestInit = {
906
1124
  method: config.method,
907
1125
  headers: filteredHeaders,
908
1126
  body: this.prepareRequestBody(config),
909
- signal: config.signal ?? controller.signal,
1127
+ signal: controller.signal,
910
1128
  // Include credentials for CORS requests if in browser and configured
911
1129
  ...typeof window !== "undefined" && this.config.browser.includeCredentials != null && { credentials: "include" }
912
1130
  };
913
- const response = await fetch(url, requestInit);
1131
+ const send = this.config.fetch ?? globalThis.fetch;
1132
+ const response = await send(url, requestInit);
914
1133
  clearTimeout(timeoutId);
915
1134
  const responseBody = await this.parseResponse(response);
916
1135
  if (!response.ok) {
@@ -941,6 +1160,7 @@ var _HttpClient = class _HttpClient {
941
1160
  throw error;
942
1161
  }
943
1162
  if (error instanceof DOMException && error.name === "AbortError") {
1163
+ if (!timedOut && isAborted(caller)) throw error;
944
1164
  throw new TimeoutError(
945
1165
  `Request timed out after ${config.timeout ?? this.timeout}ms`,
946
1166
  config.timeout ?? this.timeout
@@ -972,6 +1192,8 @@ var _HttpClient = class _HttpClient {
972
1192
  throw corsError;
973
1193
  }
974
1194
  throw error;
1195
+ } finally {
1196
+ caller?.removeEventListener("abort", onCallerAbort);
975
1197
  }
976
1198
  }
977
1199
  /**
@@ -1060,16 +1282,21 @@ var _HttpClient = class _HttpClient {
1060
1282
  });
1061
1283
  return obj;
1062
1284
  }
1063
- calculateRetryDelay(attempt) {
1064
- const exponentialDelay = this.retryConfig.baseDelay * Math.pow(this.retryConfig.backoffFactor, attempt - 1);
1065
- const delay = Math.min(exponentialDelay, this.retryConfig.maxDelay);
1066
- if (this.retryConfig.jitter) {
1067
- return delay * (0.5 + Math.random() * 0.5);
1068
- }
1069
- return delay;
1070
- }
1071
- sleep(ms) {
1072
- return new Promise((resolve) => setTimeout(resolve, ms));
1285
+ /** A backoff the caller's abort cuts short; the next attempt then refuses to send. */
1286
+ sleep(ms, signal) {
1287
+ return new Promise((resolve) => {
1288
+ if (isAborted(signal)) {
1289
+ resolve();
1290
+ return;
1291
+ }
1292
+ const done = /* @__PURE__ */ chunkMWMSFIWN_cjs.__name(() => {
1293
+ clearTimeout(timer);
1294
+ signal?.removeEventListener("abort", done);
1295
+ resolve();
1296
+ }, "done");
1297
+ const timer = setTimeout(done, ms);
1298
+ signal?.addEventListener("abort", done, { once: true });
1299
+ });
1073
1300
  }
1074
1301
  log(level, message, data) {
1075
1302
  if (!this.debug) return;
@@ -1082,6 +1309,10 @@ var _HttpClient = class _HttpClient {
1082
1309
  };
1083
1310
  chunkMWMSFIWN_cjs.__name(_HttpClient, "HttpClient");
1084
1311
  var HttpClient = _HttpClient;
1312
+ function isAborted(signal) {
1313
+ return signal?.aborted === true;
1314
+ }
1315
+ chunkMWMSFIWN_cjs.__name(isAborted, "isAborted");
1085
1316
 
1086
1317
  // src/content/query-builder.ts
1087
1318
  var _ContentQueryBuilder = class _ContentQueryBuilder {
@@ -1563,32 +1794,27 @@ var _ContentByIdBuilder = class _ContentByIdBuilder {
1563
1794
  chunkMWMSFIWN_cjs.__name(_ContentByIdBuilder, "ContentByIdBuilder");
1564
1795
  var ContentByIdBuilder = _ContentByIdBuilder;
1565
1796
 
1566
- // ../../platform/api-contract/dist/index.js
1567
- var HEADER_ENTITY_TAG = { name: "X-Entity-Tag" };
1568
- var HEADER_IF_MATCH = {
1569
- name: "If-Match"};
1570
- var HEADER_IDEMPOTENCY_KEY = {
1571
- name: "Idempotency-Key"};
1572
-
1573
1797
  // src/http/write-options.ts
1798
+ function mintIdempotencyKey() {
1799
+ const webCrypto = globalThis.crypto;
1800
+ if (typeof webCrypto?.randomUUID !== "function") return void 0;
1801
+ return webCrypto.randomUUID();
1802
+ }
1803
+ chunkMWMSFIWN_cjs.__name(mintIdempotencyKey, "mintIdempotencyKey");
1574
1804
  function writeHeaders(options) {
1575
- if (options === void 0) return void 0;
1576
1805
  const headers = {};
1577
- if ("ifMatch" in options && options.ifMatch !== void 0) {
1806
+ if (options !== void 0 && "ifMatch" in options && options.ifMatch !== void 0) {
1578
1807
  headers[HEADER_IF_MATCH.name] = options.ifMatch;
1579
1808
  }
1580
- if (options.idempotencyKey !== void 0) {
1581
- headers[HEADER_IDEMPOTENCY_KEY.name] = options.idempotencyKey;
1809
+ const idempotencyKey = options?.idempotencyKey ?? mintIdempotencyKey();
1810
+ if (idempotencyKey !== void 0) {
1811
+ headers[HEADER_IDEMPOTENCY_KEY.name] = idempotencyKey;
1582
1812
  }
1583
1813
  return Object.keys(headers).length > 0 ? headers : void 0;
1584
1814
  }
1585
1815
  chunkMWMSFIWN_cjs.__name(writeHeaders, "writeHeaders");
1586
1816
  function entityTagOf(response) {
1587
- const wanted = HEADER_ENTITY_TAG.name.toLowerCase();
1588
- for (const [name, value] of Object.entries(response.headers)) {
1589
- if (name.toLowerCase() === wanted) return value;
1590
- }
1591
- return void 0;
1817
+ return headerValue(response.headers, HEADER_ENTITY_TAG.name);
1592
1818
  }
1593
1819
  chunkMWMSFIWN_cjs.__name(entityTagOf, "entityTagOf");
1594
1820
 
@@ -3854,6 +4080,103 @@ var _AdhocQueryClient = class _AdhocQueryClient {
3854
4080
  chunkMWMSFIWN_cjs.__name(_AdhocQueryClient, "AdhocQueryClient");
3855
4081
  var AdhocQueryClient = _AdhocQueryClient;
3856
4082
 
4083
+ // src/changes/changes.ts
4084
+ var CHANGES_POLL_TIMEOUT_MS = 25e3 + 5e3;
4085
+ var _ChangesExpiredError = class _ChangesExpiredError extends ResourceExpiredError {
4086
+ /** The base class's signature, so `withContext` can rebuild one. */
4087
+ constructor(message, statusCode = 410, cause, context) {
4088
+ super(message, statusCode, cause, context);
4089
+ chunkMWMSFIWN_cjs.__publicField(this, "reason");
4090
+ this.name = "ChangesExpiredError";
4091
+ this.reason = expiredReasonOf(message);
4092
+ }
4093
+ static of(expired) {
4094
+ return new _ChangesExpiredError(expired.message, expired.statusCode, expired, expired.context);
4095
+ }
4096
+ };
4097
+ chunkMWMSFIWN_cjs.__name(_ChangesExpiredError, "ChangesExpiredError");
4098
+ var ChangesExpiredError = _ChangesExpiredError;
4099
+ function expiredReasonOf(message) {
4100
+ if (message.includes("(retention)")) return "retention";
4101
+ if (message.includes("(dataset-replaced)")) return "dataset-replaced";
4102
+ return "unknown";
4103
+ }
4104
+ chunkMWMSFIWN_cjs.__name(expiredReasonOf, "expiredReasonOf");
4105
+ async function* pollChanges(http, options = {}) {
4106
+ const { signal } = options;
4107
+ let since = options.since;
4108
+ let resynced;
4109
+ for (; ; ) {
4110
+ if (aborted(signal)) return;
4111
+ let response;
4112
+ try {
4113
+ response = await pollOnce(http, since, options);
4114
+ } catch (error) {
4115
+ if (aborted(signal)) return;
4116
+ if (!(error instanceof ResourceExpiredError) || since === void 0) throw error;
4117
+ const expired = ChangesExpiredError.of(error);
4118
+ const onExpired = options.onExpired ?? "throw";
4119
+ if (onExpired === "throw") throw expired;
4120
+ since = void 0;
4121
+ resynced = typeof onExpired === "function" ? expired : void 0;
4122
+ continue;
4123
+ }
4124
+ const { data, page } = response.body;
4125
+ if (page.nextCursor === null) {
4126
+ throw new Error("GET /v1/changes answered without page.nextCursor");
4127
+ }
4128
+ since = page.nextCursor;
4129
+ if (resynced !== void 0 && typeof options.onExpired === "function") {
4130
+ const expired = resynced;
4131
+ resynced = void 0;
4132
+ await options.onExpired(expired);
4133
+ if (aborted(signal)) return;
4134
+ }
4135
+ for (const change of data) {
4136
+ yield change;
4137
+ if (aborted(signal)) return;
4138
+ }
4139
+ if (data.length === 0) {
4140
+ const wait = parseRetryAfterMs(headerValue(response.headers, "Retry-After"), Date.now());
4141
+ if (wait !== void 0 && wait > 0) await sleep(wait, signal);
4142
+ }
4143
+ }
4144
+ }
4145
+ chunkMWMSFIWN_cjs.__name(pollChanges, "pollChanges");
4146
+ async function pollOnce(http, since, options) {
4147
+ const query = {};
4148
+ if (since !== void 0) query.since = since;
4149
+ if (options.types !== void 0) query.types = options.types.join(",");
4150
+ if (options.ids !== void 0) query.ids = options.ids.join(",");
4151
+ if (options.limit !== void 0) query.limit = options.limit;
4152
+ return http.send("/v1/changes", "get", {
4153
+ query,
4154
+ timeout: CHANGES_POLL_TIMEOUT_MS,
4155
+ ...options.signal === void 0 ? {} : { signal: options.signal }
4156
+ });
4157
+ }
4158
+ chunkMWMSFIWN_cjs.__name(pollOnce, "pollOnce");
4159
+ function sleep(ms, signal) {
4160
+ return new Promise((resolve) => {
4161
+ if (aborted(signal)) {
4162
+ resolve();
4163
+ return;
4164
+ }
4165
+ const done = /* @__PURE__ */ chunkMWMSFIWN_cjs.__name(() => {
4166
+ clearTimeout(timer);
4167
+ signal?.removeEventListener("abort", done);
4168
+ resolve();
4169
+ }, "done");
4170
+ const timer = setTimeout(done, ms);
4171
+ signal?.addEventListener("abort", done, { once: true });
4172
+ });
4173
+ }
4174
+ chunkMWMSFIWN_cjs.__name(sleep, "sleep");
4175
+ function aborted(signal) {
4176
+ return signal?.aborted === true;
4177
+ }
4178
+ chunkMWMSFIWN_cjs.__name(aborted, "aborted");
4179
+
3857
4180
  // src/config/dev-detection.ts
3858
4181
  function getProcess() {
3859
4182
  return typeof process !== "undefined" ? process : void 0;
@@ -3872,47 +4195,76 @@ function isProduction(proc) {
3872
4195
  return proc?.env?.NODE_ENV === "production";
3873
4196
  }
3874
4197
  chunkMWMSFIWN_cjs.__name(isProduction, "isProduction");
3875
- function resolveFromEnv(proc) {
3876
- const dataset = proc?.env?.RELYIO_DEV_DATASET;
3877
- const apiKey = proc?.env?.RELYIO_DEV_API_KEY;
3878
- if (dataset != null && dataset !== "" && apiKey != null && apiKey !== "") {
3879
- return { dataset, apiKey, source: "env" };
4198
+ function resolveFromEnv(proc, now) {
4199
+ const verdict = chunkLSRPMNPT_cjs.parseDevEnv(proc?.env, now);
4200
+ switch (verdict.state) {
4201
+ case "active":
4202
+ return {
4203
+ state: "active",
4204
+ override: {
4205
+ dataset: verdict.credentials.dataset,
4206
+ apiKey: verdict.credentials.apiKey,
4207
+ source: "env"
4208
+ }
4209
+ };
4210
+ case "expired":
4211
+ return { state: "expired", expiredAt: verdict.expiredAt };
4212
+ case "unreadable":
4213
+ return { state: "unreadable", reason: verdict.reason };
4214
+ case "absent":
4215
+ return { state: "absent" };
3880
4216
  }
3881
- return null;
3882
4217
  }
3883
4218
  chunkMWMSFIWN_cjs.__name(resolveFromEnv, "resolveFromEnv");
3884
- function resolveFromFile(proc) {
3885
- if (proc == null || typeof proc.cwd !== "function") return null;
4219
+ function resolveFromFile(proc, now) {
4220
+ if (proc == null || typeof proc.cwd !== "function") return { state: "absent" };
3886
4221
  const fs = getBuiltin(proc, "node:fs");
3887
4222
  const path = getBuiltin(proc, "node:path");
3888
- if (fs == null || path == null) return null;
4223
+ if (fs == null || path == null) return { state: "absent" };
3889
4224
  let dir = proc.cwd();
3890
4225
  for (; ; ) {
3891
- const candidate = path.join(dir, ".relyio", "dev.json");
4226
+ const candidate = path.join(dir, chunkLSRPMNPT_cjs.DEV_CONTEXT_DIR, chunkLSRPMNPT_cjs.DEV_CONTEXT_FILENAME);
4227
+ let raw = null;
3892
4228
  try {
3893
- if (fs.existsSync(candidate)) {
3894
- const parsed = JSON.parse(fs.readFileSync(candidate, "utf8"));
3895
- if (parsed.datasetId != null && parsed.datasetId !== "" && parsed.previewApiKey != null && parsed.previewApiKey !== "") {
3896
- if (parsed.expiresAt != null && Number.isFinite(new Date(parsed.expiresAt).getTime()) && new Date(parsed.expiresAt).getTime() < Date.now()) {
3897
- return null;
3898
- }
3899
- return { dataset: parsed.datasetId, apiKey: parsed.previewApiKey, source: "file" };
3900
- }
3901
- }
4229
+ if (fs.existsSync(candidate)) raw = fs.readFileSync(candidate, "utf8");
3902
4230
  } catch {
4231
+ return { state: "absent" };
4232
+ }
4233
+ if (raw !== null) {
4234
+ const verdict = chunkLSRPMNPT_cjs.parseDevContextFile(raw, now);
4235
+ switch (verdict.state) {
4236
+ case "active":
4237
+ return {
4238
+ state: "active",
4239
+ override: {
4240
+ dataset: verdict.context.datasetId,
4241
+ apiKey: verdict.context.previewApiKey,
4242
+ source: "file"
4243
+ }
4244
+ };
4245
+ case "expired":
4246
+ return { state: "expired", expiredAt: verdict.expiredAt };
4247
+ case "unreadable":
4248
+ return { state: "unreadable", reason: verdict.reason };
4249
+ case "absent":
4250
+ return { state: "absent" };
4251
+ }
3903
4252
  }
3904
4253
  const parent = path.dirname(dir);
3905
4254
  if (parent === dir) break;
3906
4255
  dir = parent;
3907
4256
  }
3908
- return null;
4257
+ return { state: "absent" };
3909
4258
  }
3910
4259
  chunkMWMSFIWN_cjs.__name(resolveFromFile, "resolveFromFile");
3911
4260
  function resolveDevOverride(dev) {
3912
- if (dev === false) return null;
4261
+ if (dev === false) return { state: "disabled" };
3913
4262
  const proc = getProcess();
3914
- if (dev !== true && isProduction(proc)) return null;
3915
- return resolveFromEnv(proc) ?? resolveFromFile(proc);
4263
+ if (dev !== true && isProduction(proc)) return { state: "disabled" };
4264
+ const now = Date.now();
4265
+ const fromEnv = resolveFromEnv(proc, now);
4266
+ if (fromEnv.state !== "absent") return fromEnv;
4267
+ return resolveFromFile(proc, now);
3916
4268
  }
3917
4269
  chunkMWMSFIWN_cjs.__name(resolveDevOverride, "resolveDevOverride");
3918
4270
 
@@ -3995,16 +4347,26 @@ function mergeConfig(userConfig) {
3995
4347
  ...userConfig.debug
3996
4348
  }
3997
4349
  };
3998
- const devOverride = resolveDevOverride(userConfig.dev);
3999
- if (devOverride != null) {
4000
- merged.apiKey = devOverride.apiKey;
4001
- merged.dataset = devOverride.dataset;
4350
+ const dev = resolveDevOverride(userConfig.dev);
4351
+ if (dev.state === "active") {
4352
+ merged.apiKey = dev.override.apiKey;
4353
+ merged.dataset = dev.override.dataset;
4002
4354
  if (!devOverrideNotified) {
4003
4355
  devOverrideNotified = true;
4004
4356
  console.info(
4005
- `[RelyIO] Sandbox dev mode active (${devOverride.source}) \u2014 targeting dataset "${devOverride.dataset}" with a scoped preview key.`
4357
+ `[RelyIO] Sandbox dev mode active (${dev.override.source}) \u2014 targeting dataset "${dev.override.dataset}" with a scoped preview key.`
4006
4358
  );
4007
4359
  }
4360
+ } else if (dev.state === "expired" && !devOverrideNotified) {
4361
+ devOverrideNotified = true;
4362
+ console.warn(
4363
+ `[RelyIO] Your sandbox's .relyio/dev.json expired at ${dev.expiredAt} \u2014 ignoring it and using the configured dataset "${merged.dataset}". Run \`relyio dev\` to refresh it.`
4364
+ );
4365
+ } else if (dev.state === "unreadable" && !devOverrideNotified) {
4366
+ devOverrideNotified = true;
4367
+ console.warn(
4368
+ `[RelyIO] Your .relyio/dev.json could not be read (${dev.reason}) \u2014 ignoring it and using the configured dataset "${merged.dataset}". Run \`relyio dev\` to rewrite it.`
4369
+ );
4008
4370
  }
4009
4371
  return merged;
4010
4372
  }
@@ -4185,6 +4547,37 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4185
4547
  return this._batch.create();
4186
4548
  }
4187
4549
  // ============================================================================
4550
+ // CHANGES FEED
4551
+ // ============================================================================
4552
+ /**
4553
+ * Follow the dataset's changes feed: each write this key can see, as ids, in
4554
+ * order, long-polled from `GET /v1/changes`.
4555
+ *
4556
+ * Lazy — nothing is requested until the first `next()`. It runs until
4557
+ * `signal` aborts, which ends the `for await` cleanly rather than throwing.
4558
+ * A change names what changed; refetch it through the ordinary read path
4559
+ * (`INV-SS-CHANGE-IS-IDS`).
4560
+ *
4561
+ * @example
4562
+ * ```typescript
4563
+ * const controller = new AbortController()
4564
+ * for await (const change of relyio.changes({ since: cursor, types: ['post'], signal: controller.signal })) {
4565
+ * // { entryId, contentType, locale, kind, cursor }
4566
+ * }
4567
+ * ```
4568
+ *
4569
+ * @example Resync when the cursor has expired
4570
+ * ```typescript
4571
+ * for await (const change of relyio.changes({
4572
+ * since: cursor,
4573
+ * onExpired: async () => invalidateEverything(),
4574
+ * })) { … }
4575
+ * ```
4576
+ */
4577
+ changes(options = {}) {
4578
+ return pollChanges(this.httpClient, options);
4579
+ }
4580
+ // ============================================================================
4188
4581
  // QUERY OPERATIONS
4189
4582
  // ============================================================================
4190
4583
  /**
@@ -4383,6 +4776,7 @@ exports.AutocompleteBuilder = AutocompleteBuilder;
4383
4776
  exports.BaseValidationError = BaseValidationError;
4384
4777
  exports.BatchBuilder = BatchBuilder;
4385
4778
  exports.BatchClient = BatchClient;
4779
+ exports.ChangesExpiredError = ChangesExpiredError;
4386
4780
  exports.ConnectionError = ConnectionError;
4387
4781
  exports.ContentByIdBuilder = ContentByIdBuilder;
4388
4782
  exports.ContentClient = ContentClient;
@@ -4424,6 +4818,7 @@ exports.createClient = createClient;
4424
4818
  exports.entityTagOf = entityTagOf;
4425
4819
  exports.isAssetEntry = isAssetEntry;
4426
4820
  exports.isResourceNotFound = isResourceNotFound;
4821
+ exports.matchRoute = matchRoute;
4427
4822
  exports.validateFile = validateFile;
4428
- //# sourceMappingURL=chunk-7W4MJXQ4.cjs.map
4429
- //# sourceMappingURL=chunk-7W4MJXQ4.cjs.map
4823
+ //# sourceMappingURL=chunk-XS32U2UP.cjs.map
4824
+ //# sourceMappingURL=chunk-XS32U2UP.cjs.map