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