@relyio/sdk 0.3.0 → 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.
@@ -734,6 +734,110 @@ function fillPath(template, params) {
734
734
  return filled;
735
735
  }
736
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");
737
841
 
738
842
  // src/http/retry-policy.ts
739
843
  var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
@@ -881,6 +985,7 @@ var _HttpClient = class _HttpClient {
881
985
  return response;
882
986
  } catch (error) {
883
987
  const failure = error;
988
+ if (config.signal?.aborted === true) throw failure;
884
989
  this.log("warn", "Request failed", {
885
990
  error: error instanceof Error ? error.message : "Unknown error",
886
991
  attempt,
@@ -889,7 +994,7 @@ var _HttpClient = class _HttpClient {
889
994
  if (this.retryPolicy.shouldRetry(attempt, failure, mergedConfig)) {
890
995
  const delay = this.retryPolicy.delayFor(attempt, failure);
891
996
  this.log("debug", "Retrying request", { attempt, delay });
892
- await this.sleep(delay);
997
+ await this.sleep(delay, config.signal);
893
998
  continue;
894
999
  }
895
1000
  const sdkError = ErrorMapper.mapGenericError(failure, {
@@ -988,20 +1093,41 @@ var _HttpClient = class _HttpClient {
988
1093
  getBaseUrl() {
989
1094
  return this.baseUrl;
990
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
+ */
991
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
+ }
992
1111
  const controller = new AbortController();
993
- 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 });
994
1119
  try {
995
1120
  const filteredHeaders = this.filterHeadersForBrowser(config.headers ?? {});
996
1121
  const requestInit = {
997
1122
  method: config.method,
998
1123
  headers: filteredHeaders,
999
1124
  body: this.prepareRequestBody(config),
1000
- signal: config.signal ?? controller.signal,
1125
+ signal: controller.signal,
1001
1126
  // Include credentials for CORS requests if in browser and configured
1002
1127
  ...typeof window !== "undefined" && this.config.browser.includeCredentials != null && { credentials: "include" }
1003
1128
  };
1004
- const response = await fetch(url, requestInit);
1129
+ const send = this.config.fetch ?? globalThis.fetch;
1130
+ const response = await send(url, requestInit);
1005
1131
  clearTimeout(timeoutId);
1006
1132
  const responseBody = await this.parseResponse(response);
1007
1133
  if (!response.ok) {
@@ -1032,6 +1158,7 @@ var _HttpClient = class _HttpClient {
1032
1158
  throw error;
1033
1159
  }
1034
1160
  if (error instanceof DOMException && error.name === "AbortError") {
1161
+ if (!timedOut && isAborted(caller)) throw error;
1035
1162
  throw new TimeoutError(
1036
1163
  `Request timed out after ${config.timeout ?? this.timeout}ms`,
1037
1164
  config.timeout ?? this.timeout
@@ -1063,6 +1190,8 @@ var _HttpClient = class _HttpClient {
1063
1190
  throw corsError;
1064
1191
  }
1065
1192
  throw error;
1193
+ } finally {
1194
+ caller?.removeEventListener("abort", onCallerAbort);
1066
1195
  }
1067
1196
  }
1068
1197
  /**
@@ -1151,8 +1280,21 @@ var _HttpClient = class _HttpClient {
1151
1280
  });
1152
1281
  return obj;
1153
1282
  }
1154
- sleep(ms) {
1155
- 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
+ });
1156
1298
  }
1157
1299
  log(level, message, data) {
1158
1300
  if (!this.debug) return;
@@ -1165,6 +1307,10 @@ var _HttpClient = class _HttpClient {
1165
1307
  };
1166
1308
  __name(_HttpClient, "HttpClient");
1167
1309
  var HttpClient = _HttpClient;
1310
+ function isAborted(signal) {
1311
+ return signal?.aborted === true;
1312
+ }
1313
+ __name(isAborted, "isAborted");
1168
1314
 
1169
1315
  // src/content/query-builder.ts
1170
1316
  var _ContentQueryBuilder = class _ContentQueryBuilder {
@@ -3932,6 +4078,103 @@ var _AdhocQueryClient = class _AdhocQueryClient {
3932
4078
  __name(_AdhocQueryClient, "AdhocQueryClient");
3933
4079
  var AdhocQueryClient = _AdhocQueryClient;
3934
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
+
3935
4178
  // src/config/dev-detection.ts
3936
4179
  function getProcess() {
3937
4180
  return typeof process !== "undefined" ? process : void 0;
@@ -4302,6 +4545,37 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4302
4545
  return this._batch.create();
4303
4546
  }
4304
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
+ // ============================================================================
4305
4579
  // QUERY OPERATIONS
4306
4580
  // ============================================================================
4307
4581
  /**
@@ -4489,6 +4763,6 @@ function createClient(config) {
4489
4763
  }
4490
4764
  __name(createClient, "createClient");
4491
4765
 
4492
- 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 };
4493
- //# sourceMappingURL=chunk-QIZOBT5F.js.map
4494
- //# sourceMappingURL=chunk-QIZOBT5F.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
@@ -707,5 +707,5 @@ exports.isLocaleDefinition = isLocaleDefinition;
707
707
  exports.isRoleDefinition = isRoleDefinition;
708
708
  exports.isWorkflowDefinition = isWorkflowDefinition;
709
709
  exports.isWorkflowStageDefinition = isWorkflowStageDefinition;
710
- //# sourceMappingURL=chunk-HMVGXUGR.cjs.map
711
- //# sourceMappingURL=chunk-HMVGXUGR.cjs.map
710
+ //# sourceMappingURL=chunk-HAQELDZN.cjs.map
711
+ //# sourceMappingURL=chunk-HAQELDZN.cjs.map