@relyio/sdk 0.6.0 → 0.8.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 (37) hide show
  1. package/CHANGELOG.md +400 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/{chunk-DY2XT7OF.cjs → chunk-72UK5U7T.cjs} +70 -60
  4. package/dist/{chunk-I3ESNANB.js → chunk-H6IBQSMM.js} +2 -2
  5. package/dist/{chunk-7ABNXVWL.js → chunk-QFHVF3YF.js} +1617 -404
  6. package/dist/{chunk-HT6NFRM2.cjs → chunk-TZL374LS.cjs} +2 -2
  7. package/dist/{chunk-GCMQUM6N.cjs → chunk-XIE7WJV3.cjs} +1621 -407
  8. package/dist/{chunk-XJVY2CLY.js → chunk-YEJUMW4X.js} +70 -60
  9. package/dist/client/index.cjs +4 -3
  10. package/dist/client/index.d.cts +5 -4
  11. package/dist/client/index.d.ts +5 -4
  12. package/dist/client/index.js +2 -1
  13. package/dist/{index-d1Kz1e19.d.cts → define-content-type-cEGjIwT5.d.cts} +1859 -733
  14. package/dist/{index-yPbhZc6q.d.ts → define-content-type-cEGjIwT5.d.ts} +1859 -733
  15. package/dist/{define-schema-1kr2HIzN.d.cts → define-schema-BPj2srgh.d.ts} +189 -85
  16. package/dist/{define-schema-ChvAzY8H.d.ts → define-schema-DgbJ5Kwn.d.cts} +189 -85
  17. package/dist/index-B_rIWO_l.d.cts +860 -0
  18. package/dist/index-Cv4tmcrw.d.ts +860 -0
  19. package/dist/{index-Df1VsfJR.d.cts → index-DBoPPlT1.d.cts} +684 -277
  20. package/dist/{index-XqoGUaAY.d.ts → index-yRJ2IqM_.d.ts} +684 -277
  21. package/dist/index.cjs +148 -124
  22. package/dist/index.d.cts +174 -64
  23. package/dist/index.d.ts +174 -64
  24. package/dist/index.js +43 -18
  25. package/dist/schema/index.cjs +35 -35
  26. package/dist/schema/index.d.cts +13 -11
  27. package/dist/schema/index.d.ts +13 -11
  28. package/dist/schema/index.js +1 -1
  29. package/dist/types/index.d.cts +2 -2
  30. package/dist/types/index.d.ts +2 -2
  31. package/dist/utils/index.cjs +19 -19
  32. package/dist/utils/index.d.cts +10 -1
  33. package/dist/utils/index.d.ts +10 -1
  34. package/dist/utils/index.js +1 -1
  35. package/package.json +2 -1
  36. package/dist/define-content-type-CDB3kyeA.d.cts +0 -1173
  37. package/dist/define-content-type-CDB3kyeA.d.ts +0 -1173
@@ -1,4 +1,5 @@
1
1
  import { parseDevEnv, DEV_CONTEXT_DIR, DEV_CONTEXT_FILENAME, parseDevContextFile } from './chunk-BBUB7C2Z.js';
2
+ import { isBrowser } from './chunk-H6IBQSMM.js';
2
3
  import { __name, __publicField } from './chunk-XQ4QHWZE.js';
3
4
 
4
5
  /* RelyIO SDK - Built with ❤️ by the RelyIO Team */
@@ -230,18 +231,6 @@ __name(_UnknownContractError, "UnknownContractError");
230
231
  var UnknownContractError = _UnknownContractError;
231
232
 
232
233
  // src/errors/auth-errors.ts
233
- var _AuthenticationError = class _AuthenticationError extends SDKError {
234
- constructor(message = "Authentication failed", statusCode = 401, cause, context) {
235
- super(message, statusCode, cause, context);
236
- __publicField(this, "code", "AUTHENTICATION_ERROR");
237
- __publicField(this, "retryable", false);
238
- }
239
- getUserMessage() {
240
- return "Authentication failed. Please check your API key and try again.";
241
- }
242
- };
243
- __name(_AuthenticationError, "AuthenticationError");
244
- var AuthenticationError = _AuthenticationError;
245
234
  var _AuthorizationError = class _AuthorizationError extends SDKError {
246
235
  constructor(message = "Authorization failed", statusCode = 403, cause, context) {
247
236
  super(message, statusCode, cause, context);
@@ -420,58 +409,45 @@ var _InvalidPayloadError = class _InvalidPayloadError extends BaseValidationErro
420
409
  };
421
410
  __name(_InvalidPayloadError, "InvalidPayloadError");
422
411
  var InvalidPayloadError = _InvalidPayloadError;
423
- var _SchemaValidationError = class _SchemaValidationError extends BaseValidationError {
424
- constructor(message = "Schema validation failed", violations = [], cause) {
425
- super(message, violations, 400, cause);
426
- __publicField(this, "code", "SCHEMA_VALIDATION_ERROR");
412
+
413
+ // ../../platform/api-contract/dist/chunk-PYZCLRXR.js
414
+ function defineRoute(route) {
415
+ if (route.kind === "raw" && route.standardShape === void 0) {
416
+ throw new Error("kind 'raw' requires a named StandardShape");
427
417
  }
428
- getUserMessage() {
429
- return "The data does not match the expected schema. Please check your content structure.";
418
+ if (route.kind === "raw" && route.success === 302 && !route.headers.some((header) => header.name === "Location")) {
419
+ throw new Error("a raw route declaring success 302 must declare its 'Location' header");
430
420
  }
431
- };
432
- __name(_SchemaValidationError, "SchemaValidationError");
433
- var SchemaValidationError = _SchemaValidationError;
434
- var _RequiredFieldError = class _RequiredFieldError extends BaseValidationError {
435
- constructor(fieldName, message, cause) {
436
- const violation = {
437
- field: fieldName,
438
- message: message ?? `Field "${fieldName}" is required`,
439
- code: "REQUIRED"
440
- };
441
- super(`Required field "${fieldName}" is missing`, [violation], 400, cause);
442
- __publicField(this, "code", "REQUIRED_FIELD_ERROR");
421
+ if (route.kind === "stream" && route.stream === void 0) {
422
+ throw new Error("kind 'stream' requires a StreamDeclaration");
443
423
  }
444
- getUserMessage() {
445
- const violation = this.violations[0];
446
- const fieldName = violation?.field;
447
- return `The field "${fieldName}" is required and cannot be empty.`;
424
+ if (route.kind === "collection" && route.page === void 0) {
425
+ throw new Error("kind 'collection' requires a page dialect");
448
426
  }
449
- };
450
- __name(_RequiredFieldError, "RequiredFieldError");
451
- var RequiredFieldError = _RequiredFieldError;
452
- var _InvalidFieldTypeError = class _InvalidFieldTypeError extends BaseValidationError {
453
- constructor(fieldName, expectedType, actualType, value, cause) {
454
- const violation = {
455
- field: fieldName,
456
- message: `Expected ${expectedType}, got ${actualType}`,
457
- code: "INVALID_TYPE",
458
- value
459
- };
460
- super(
461
- `Invalid type for field "${fieldName}": expected ${expectedType}, got ${actualType}`,
462
- [violation],
463
- 400,
464
- cause
427
+ if ((route.kind === "object" || route.kind === "collection") && route.negotiated !== void 0) {
428
+ refuseMismatchedAlternate(route, route.negotiated["text/event-stream"]);
429
+ }
430
+ return route;
431
+ }
432
+ __name(defineRoute, "defineRoute");
433
+ function refuseMismatchedAlternate(route, stream) {
434
+ if (stream.kind !== "stream") {
435
+ throw new Error("a negotiated 'text/event-stream' alternate must be kind 'stream'");
436
+ }
437
+ if (stream.stream.mediaType !== "text/event-stream") {
438
+ throw new Error(
439
+ `a negotiated 'text/event-stream' alternate declares media type "${stream.stream.mediaType}"`
465
440
  );
466
- __publicField(this, "code", "INVALID_FIELD_TYPE");
467
441
  }
468
- getUserMessage() {
469
- const violation = this.violations[0];
470
- return `The field "${violation?.field}" has an invalid type. ${violation?.message}.`;
442
+ const own = [...new Set(route.codes)].sort();
443
+ const alternate = [...new Set(stream.codes)].sort();
444
+ if (own.length !== alternate.length || own.some((code, index) => code !== alternate[index])) {
445
+ throw new Error(
446
+ `a negotiated alternate refuses with [${alternate.join(", ")}] while its route refuses with [${own.join(", ")}]; one route refuses with one set of codes on every transport`
447
+ );
471
448
  }
472
- };
473
- __name(_InvalidFieldTypeError, "InvalidFieldTypeError");
474
- var InvalidFieldTypeError = _InvalidFieldTypeError;
449
+ }
450
+ __name(refuseMismatchedAlternate, "refuseMismatchedAlternate");
475
451
 
476
452
  // ../../platform/api-contract/dist/index.js
477
453
  var HEADER_RETRY_AFTER = { name: "Retry-After" };
@@ -480,6 +456,169 @@ var HEADER_IF_MATCH = {
480
456
  name: "If-Match"};
481
457
  var HEADER_IDEMPOTENCY_KEY = {
482
458
  name: "Idempotency-Key"};
459
+ var FRESH_PARAMETER = "_fresh";
460
+ var VISITOR_LIVE_PATH = "/visitor/changes";
461
+ var VISITOR_QUERY = {
462
+ /** The publishable key. */
463
+ key: "key",
464
+ /** A dataset id or name the key reaches; absent, the key's default dataset. */
465
+ dataset: "dataset",
466
+ /** The cursor to continue from; absent, the socket starts now. */
467
+ since: "since",
468
+ /** Comma-separated content types to keep; absent keeps every type. */
469
+ types: "types",
470
+ /** Comma-separated entry ids to keep; absent keeps every entry. */
471
+ ids: "ids",
472
+ /** The poll's client id. Required on a poll, and not read on a socket. */
473
+ client: "client"
474
+ };
475
+ var VISITOR_CURSOR = /^[\w.~-]{1,256}$/;
476
+ var VISITOR_POLL_INTERVAL_MS = 15e3;
477
+ var VISITOR_KEEPALIVE = { request: "ping", response: "pong" };
478
+ var VISITOR_FRAMES = {
479
+ opened: "feed.opened",
480
+ changes: "feed.changes",
481
+ resync: "feed.resync"
482
+ };
483
+ defineRoute({
484
+ kind: "object",
485
+ headers: [HEADER_RETRY_AFTER],
486
+ codes: [
487
+ "REQUEST_INVALID",
488
+ "MISSING_API_KEY",
489
+ "INVALID_API_KEY",
490
+ "ACCESS_DENIED",
491
+ "METHOD_NOT_ALLOWED",
492
+ "SERVICE_INDETERMINATE",
493
+ "USAGE_QUOTA_EXCEEDED",
494
+ "RATE_LIMIT_EXCEEDED"
495
+ ]
496
+ });
497
+ var VISITOR_CLOSE_CODES = {
498
+ /** A query the door cannot read: a malformed `since`, filter, `dataset` or `client`. */
499
+ requestInvalid: 4400,
500
+ /** No publishable key, or one that is unknown, revoked, expired, a secret key or a preview key. */
501
+ keyRefused: 4401,
502
+ /** The page's `Origin` is absent, or not on the key's allowed origins. */
503
+ originRefused: 4403,
504
+ /** `dataset` names nothing the key reaches. */
505
+ datasetRefused: 4404,
506
+ /** The key's scope, the project's counter or the feed could not be reached just now. */
507
+ unavailable: 1013,
508
+ /**
509
+ * The project holds as many visitor connections as its plan allows — Starter's 100 — or its
510
+ * Organization is suspended. As HTTP's 402: more needs a bigger plan, or visitors leaving.
511
+ */
512
+ allowanceReached: 4402,
513
+ /** This client's address holds as many connections to the project as one address may. */
514
+ addressLimitReached: 4429,
515
+ /** The project is at its per-project safety ceiling — Pro's 50,000 — whatever it would pay for. */
516
+ ceilingReached: 4503
517
+ };
518
+ var VISITOR_CLOSE_RETRY = {
519
+ 4400: "stop",
520
+ 4401: "stop",
521
+ 4403: "stop",
522
+ 4404: "stop",
523
+ 1013: "retry",
524
+ 4402: "back-off",
525
+ 4429: "back-off",
526
+ 4503: "back-off"
527
+ };
528
+ var VISITOR_POLL_RETRY = {
529
+ REQUEST_INVALID: "stop",
530
+ MISSING_API_KEY: "stop",
531
+ INVALID_API_KEY: "stop",
532
+ ACCESS_DENIED: "stop",
533
+ METHOD_NOT_ALLOWED: "stop",
534
+ SERVICE_INDETERMINATE: "retry",
535
+ USAGE_QUOTA_EXCEEDED: "back-off",
536
+ RATE_LIMIT_EXCEEDED: "back-off"
537
+ };
538
+ var VISITOR_ADMISSION_RETRY_AFTER_SECONDS = 60;
539
+ function parseVisitorFrame(text) {
540
+ let value;
541
+ try {
542
+ value = JSON.parse(text);
543
+ } catch {
544
+ return null;
545
+ }
546
+ if (!isRecord2(value) || !isCursor(value.cursor)) return null;
547
+ switch (value.type) {
548
+ case VISITOR_FRAMES.opened: {
549
+ return hasOnly(value, ["type", "cursor"]) ? { type: VISITOR_FRAMES.opened, cursor: value.cursor } : null;
550
+ }
551
+ case VISITOR_FRAMES.changes: {
552
+ const changes = hasOnly(value, ["type", "changes", "cursor"]) ? readVisitorChanges(value.changes) : null;
553
+ return changes === null ? null : { type: VISITOR_FRAMES.changes, changes, cursor: value.cursor };
554
+ }
555
+ case VISITOR_FRAMES.resync: {
556
+ return hasOnly(value, ["type", "reason", "cursor"]) && isResyncReason(value.reason) ? { type: VISITOR_FRAMES.resync, reason: value.reason, cursor: value.cursor } : null;
557
+ }
558
+ default: {
559
+ return null;
560
+ }
561
+ }
562
+ }
563
+ __name(parseVisitorFrame, "parseVisitorFrame");
564
+ function readVisitorPollAnswer(value) {
565
+ if (!isRecord2(value) || !isCursor(value.cursor)) return null;
566
+ if (value.kind === "resync") {
567
+ return hasOnly(value, ["kind", "reason", "cursor"]) && isResyncReason(value.reason) ? { kind: "resync", reason: value.reason, cursor: value.cursor } : null;
568
+ }
569
+ if (value.kind !== "changes" || !hasOnly(value, ["kind", "changes", "cursor"])) return null;
570
+ const changes = readVisitorChanges(value.changes);
571
+ return changes === null ? null : { kind: "changes", changes, cursor: value.cursor };
572
+ }
573
+ __name(readVisitorPollAnswer, "readVisitorPollAnswer");
574
+ var CHANGE_KINDS = /* @__PURE__ */ new Set([
575
+ "published",
576
+ "updated",
577
+ "unpublished",
578
+ "deleted"
579
+ ]);
580
+ var RESYNC_REASONS = /* @__PURE__ */ new Set([
581
+ "retention",
582
+ "dataset-replaced",
583
+ "ahead-of-head",
584
+ "foreign-cursor",
585
+ "too-far-behind"
586
+ ]);
587
+ function isResyncReason(value) {
588
+ return typeof value === "string" && RESYNC_REASONS.has(value);
589
+ }
590
+ __name(isResyncReason, "isResyncReason");
591
+ function readVisitorChanges(value) {
592
+ if (!Array.isArray(value)) return null;
593
+ const changes = [];
594
+ for (const item of value) {
595
+ if (!isRecord2(item) || !hasOnly(item, ["entryId", "contentType", "locale", "kind"])) return null;
596
+ const { entryId, contentType, locale, kind } = item;
597
+ if (!isNonEmptyString2(entryId) || !isNonEmptyString2(contentType)) return null;
598
+ if (locale !== null && typeof locale !== "string") return null;
599
+ if (typeof kind !== "string" || !CHANGE_KINDS.has(kind)) return null;
600
+ changes.push({ entryId, contentType, locale, kind });
601
+ }
602
+ return changes;
603
+ }
604
+ __name(readVisitorChanges, "readVisitorChanges");
605
+ function isCursor(value) {
606
+ return typeof value === "string" && VISITOR_CURSOR.test(value);
607
+ }
608
+ __name(isCursor, "isCursor");
609
+ function isRecord2(value) {
610
+ return typeof value === "object" && value !== null && !Array.isArray(value);
611
+ }
612
+ __name(isRecord2, "isRecord2");
613
+ function isNonEmptyString2(value) {
614
+ return typeof value === "string" && value.length > 0;
615
+ }
616
+ __name(isNonEmptyString2, "isNonEmptyString2");
617
+ function hasOnly(value, keys) {
618
+ const present = Object.keys(value);
619
+ return present.length === keys.length && present.every((key) => keys.includes(key));
620
+ }
621
+ __name(hasOnly, "hasOnly");
483
622
 
484
623
  // ../../platform/api-contract/dist/client/index.js
485
624
  var _a;
@@ -748,6 +887,8 @@ var DECLARED_PATHS = {
748
887
  "/health/cache": true,
749
888
  "/health/live": true,
750
889
  "/health/ready": true,
890
+ "/internal/live/visitor-feed/{projectId}/{datasetId}/changes": true,
891
+ "/internal/live/visitor-key": true,
751
892
  "/internal/media/claim": true,
752
893
  "/internal/media/file-complete": true,
753
894
  "/internal/media/progress": true,
@@ -871,6 +1012,20 @@ function isAborted(signal) {
871
1012
  }
872
1013
  __name(isAborted, "isAborted");
873
1014
 
1015
+ // src/http/request-timeout.ts
1016
+ var MAX_TIMER_DELAY_MS = 2147483647;
1017
+ function isTimerDelay(value) {
1018
+ return typeof value === "number" && value >= 1 && value <= MAX_TIMER_DELAY_MS;
1019
+ }
1020
+ __name(isTimerDelay, "isTimerDelay");
1021
+ function assertRequestTimeout(timeout) {
1022
+ if (isTimerDelay(timeout)) return;
1023
+ throw new Error(
1024
+ `timeout must be a number of milliseconds from 1 to ${MAX_TIMER_DELAY_MS} \u2014 a timer given 0, NaN, Infinity or more fires at once and ends every request \u2014 and got ${String(timeout)}`
1025
+ );
1026
+ }
1027
+ __name(assertRequestTimeout, "assertRequestTimeout");
1028
+
874
1029
  // src/errors/instructed-delay.ts
875
1030
  function serviceInstructedDelayMs(error, nowMs) {
876
1031
  if (error instanceof ServiceIndeterminateError) {
@@ -924,43 +1079,31 @@ var DefaultRetryPolicy = _DefaultRetryPolicy;
924
1079
  // src/http/http-client.ts
925
1080
  var _HttpClient = class _HttpClient {
926
1081
  constructor(config) {
927
- __publicField(this, "baseUrl");
928
- __publicField(this, "defaultHeaders");
929
- __publicField(this, "timeout");
930
- __publicField(this, "retryPolicy");
931
- __publicField(this, "debug");
932
- __publicField(this, "logger");
933
- __publicField(this, "config");
934
- this.config = config;
935
- this.baseUrl = config.baseUrl;
936
- this.timeout = config.timeout;
937
- this.debug = config.debug.enabled ?? false;
938
- this.logger = config.debug.logger;
939
- const apiKey = presentKey(config.apiKey);
940
- this.defaultHeaders = {
941
- "Content-Type": "application/json",
942
- ...apiKey === void 0 ? {} : { [API_KEY_HEADER]: apiKey },
943
- ...config.headers
944
- };
945
- if (config.dataset != null) {
946
- this.defaultHeaders["X-RelyIO-Dataset"] = config.dataset;
947
- }
948
- if (typeof window === "undefined") {
949
- const globalWithVersion = globalThis;
950
- this.defaultHeaders["User-Agent"] = `RelyIO-SDK/${globalWithVersion.__VERSION__ ?? "0.0.0"}`;
951
- }
952
- this.retryPolicy = new DefaultRetryPolicy({
953
- maxAttempts: config.retryAttempts,
954
- baseDelay: 1e3,
955
- maxDelay: 1e4,
956
- backoffFactor: 2,
957
- jitter: true
958
- });
1082
+ /** What every request reads off the configuration, derived by {@link configure} and nowhere else. */
1083
+ __publicField(this, "connection");
1084
+ this.connection = connectionFor(config);
1085
+ this.announce();
1086
+ }
1087
+ /**
1088
+ * Run on `config` from now on: every later request — whoever holds this client — is sent to its
1089
+ * `baseUrl`, with its headers, key and dataset, its timeout and retries, through its `fetch`, and
1090
+ * logged by its `debug` options.
1091
+ *
1092
+ * The client's own `updateConfig()` calls this rather than building a second client, so a
1093
+ * `content()` or other sub-client taken before the update sends through the update too. Anything
1094
+ * set on this client since it was last configured — `setDefaultHeaders()`, `setBaseUrl()` — is
1095
+ * replaced by what `config` says.
1096
+ */
1097
+ configure(config) {
1098
+ this.connection = connectionFor(config);
1099
+ this.announce();
1100
+ }
1101
+ announce() {
959
1102
  this.log("debug", "HttpClient initialized", {
960
- baseUrl: this.baseUrl,
961
- timeout: this.timeout,
962
- retryAttempts: config.retryAttempts,
963
- environment: typeof window === "undefined" ? "node" : "browser"
1103
+ baseUrl: this.connection.baseUrl,
1104
+ timeout: this.connection.timeout,
1105
+ retryAttempts: this.connection.config.retryAttempts,
1106
+ environment: isBrowser() ? "browser" : "node"
964
1107
  });
965
1108
  }
966
1109
  /**
@@ -997,19 +1140,24 @@ var _HttpClient = class _HttpClient {
997
1140
  *
998
1141
  * The loop always makes one attempt. The incumbent's `while (attempt <
999
1142
  * maxAttempts)` made none at `retryAttempts: 0` — a value the config validator
1000
- * accepts — and then dereferenced an unassigned `lastError`.
1143
+ * then accepted, and now refuses — and then dereferenced an unassigned
1144
+ * `lastError`. `retryAttempts` is the most attempts, the first included; an
1145
+ * `HttpClient` constructed directly is not validated, so the first attempt
1146
+ * stays unconditional.
1001
1147
  *
1002
1148
  * A request that would carry no `X-API-Key` makes none: it rejects with
1003
1149
  * `MissingApiKeyError` before `fetch` is called, is not retried, and names
1004
1150
  * `apiKey`. This is where a client built without a key learns it — the
1005
1151
  * constructor accepts one so that a module-scope client can be evaluated by a
1006
- * build that holds no key (REL-1120).
1152
+ * build that holds no key (REL-1120) — and so does one whose `setApiKey` was
1153
+ * given none.
1007
1154
  */
1008
1155
  async request(config) {
1156
+ if (config.timeout !== void 0) assertRequestTimeout(config.timeout);
1009
1157
  const mergedConfig = this.mergeRequestConfig(config);
1010
1158
  if (presentKey(headerValue(mergedConfig.headers, API_KEY_HEADER)) === void 0) {
1011
1159
  throw new MissingApiKeyError(
1012
- "`apiKey` is missing: this client was created without an API key, so the request was not sent. Pass `apiKey` to createClient \u2014 the documented setup reads it from RELYIO_API_KEY.",
1160
+ "`apiKey` is missing: this client holds no API key, so the request was not sent. Pass `apiKey` to createClient \u2014 the documented setup reads it from RELYIO_API_KEY.",
1013
1161
  { url: mergedConfig.url, method: mergedConfig.method, sent: false }
1014
1162
  );
1015
1163
  }
@@ -1019,7 +1167,7 @@ var _HttpClient = class _HttpClient {
1019
1167
  // Show the final URL with query params
1020
1168
  });
1021
1169
  if (config.method === "DELETE" && mergedConfig.headers) {
1022
- delete mergedConfig.headers["Content-Type"];
1170
+ mergedConfig.headers = layerHeaders(mergedConfig.headers, { "Content-Type": null });
1023
1171
  }
1024
1172
  for (let attempt = 1; ; attempt++) {
1025
1173
  try {
@@ -1038,8 +1186,8 @@ var _HttpClient = class _HttpClient {
1038
1186
  attempt,
1039
1187
  url: mergedConfig.url
1040
1188
  });
1041
- if (this.retryPolicy.shouldRetry(attempt, failure, mergedConfig)) {
1042
- const delay = this.retryPolicy.delayFor(attempt, failure);
1189
+ if (this.connection.retryPolicy.shouldRetry(attempt, failure, mergedConfig)) {
1190
+ const delay = this.connection.retryPolicy.delayFor(attempt, failure);
1043
1191
  this.log("debug", "Retrying request", { attempt, delay });
1044
1192
  await sleep(delay, config.signal);
1045
1193
  continue;
@@ -1111,34 +1259,60 @@ var _HttpClient = class _HttpClient {
1111
1259
  });
1112
1260
  }
1113
1261
  /**
1114
- * Set default headers for all requests
1262
+ * Add headers to every later request, each replacing any header of the same name — the name
1263
+ * matched without regard to case — until the client is next configured.
1115
1264
  */
1116
1265
  setDefaultHeaders(headers) {
1117
- this.defaultHeaders = { ...this.defaultHeaders, ...headers };
1266
+ this.connection = {
1267
+ ...this.connection,
1268
+ defaultHeaders: layerHeaders(this.connection.defaultHeaders, headers)
1269
+ };
1118
1270
  }
1119
1271
  /**
1120
- * Set authentication token
1272
+ * Replace the key every later request sends as `X-API-Key`, and the `apiKey` the configuration
1273
+ * reports.
1274
+ *
1275
+ * The key is read the way construction reads it (`presentKey`): an absent, empty or blank key is
1276
+ * no key, so the header is dropped and the next request is refused with `MissingApiKeyError`
1277
+ * before anything is sent. A key header of any other spelling — one given in `headers` — goes
1278
+ * too, from the headers sent and from the headers the configuration reports, so the old key is
1279
+ * never sent beside the new one.
1280
+ *
1281
+ * The configuration is the one the owning `TypedRelyIOClient` holds (it hands its own to the
1282
+ * constructor), so its `getConfig().apiKey` — what a surface reads to decide a key's kind — is the
1283
+ * key sent from here on.
1284
+ *
1285
+ * This replaced `setAuthToken`, which added `Authorization: Bearer` beside an `X-API-Key` it
1286
+ * never touched: the API reads `X-API-Key` first, so the old key was the one used (REL-1385).
1121
1287
  */
1122
- setAuthToken(token) {
1123
- this.defaultHeaders.Authorization = `Bearer ${token}`;
1288
+ setApiKey(apiKey) {
1289
+ const { config } = this.connection;
1290
+ config.apiKey = apiKey;
1291
+ config.headers = layerHeaders(config.headers, { [API_KEY_HEADER]: null });
1292
+ this.connection = {
1293
+ ...this.connection,
1294
+ defaultHeaders: layerHeaders(this.connection.defaultHeaders, {
1295
+ [API_KEY_HEADER]: presentKey(apiKey) ?? null
1296
+ })
1297
+ };
1124
1298
  }
1125
1299
  /**
1126
1300
  * Get current default headers
1127
1301
  */
1128
1302
  getDefaultHeaders() {
1129
- return { ...this.defaultHeaders };
1303
+ return { ...this.connection.defaultHeaders };
1130
1304
  }
1131
1305
  /**
1132
1306
  * Update base URL
1133
1307
  */
1134
1308
  setBaseUrl(baseUrl) {
1135
- this.baseUrl = baseUrl;
1309
+ this.connection = { ...this.connection, baseUrl };
1136
1310
  }
1137
1311
  /**
1138
1312
  * Get current base URL
1139
1313
  */
1140
1314
  getBaseUrl() {
1141
- return this.baseUrl;
1315
+ return this.connection.baseUrl;
1142
1316
  }
1143
1317
  /**
1144
1318
  * One attempt.
@@ -1160,7 +1334,7 @@ var _HttpClient = class _HttpClient {
1160
1334
  const timeoutId = setTimeout(() => {
1161
1335
  timedOut = true;
1162
1336
  controller.abort();
1163
- }, config.timeout ?? this.timeout);
1337
+ }, config.timeout ?? this.connection.timeout);
1164
1338
  const onCallerAbort = /* @__PURE__ */ __name(() => controller.abort(), "onCallerAbort");
1165
1339
  caller?.addEventListener("abort", onCallerAbort, { once: true });
1166
1340
  try {
@@ -1170,14 +1344,15 @@ var _HttpClient = class _HttpClient {
1170
1344
  headers: filteredHeaders,
1171
1345
  body: this.prepareRequestBody(config),
1172
1346
  signal: controller.signal,
1173
- // Only where `window` exists. A server or edge runtime has no cookie jar for the mode to
1174
- // govern, so the field is left off and the runtime's default stands. A Web Worker gets
1175
- // that default too: it has cookies but no `window`.
1176
- ...typeof window !== "undefined" && {
1177
- credentials: credentialsModeFor(this.config.browser.includeCredentials)
1347
+ // Only in a browser — a `window` with a `document` (`isBrowser()`). A server or edge
1348
+ // runtime has no cookie jar for the mode to govern, so the field is left off and the
1349
+ // runtime's default stands; that includes Deno 1.x, whose global is a `window` with no
1350
+ // `document`. A Web Worker gets that default too: it has cookies but no `window`.
1351
+ ...isBrowser() && {
1352
+ credentials: credentialsModeFor(this.connection.config.browser.includeCredentials)
1178
1353
  }
1179
1354
  };
1180
- const send = this.config.fetch ?? globalThis.fetch;
1355
+ const send = this.connection.config.fetch ?? globalThis.fetch;
1181
1356
  const response = await send(url, requestInit);
1182
1357
  clearTimeout(timeoutId);
1183
1358
  const responseBody = await this.parseResponse(response);
@@ -1210,10 +1385,8 @@ var _HttpClient = class _HttpClient {
1210
1385
  }
1211
1386
  if (error instanceof DOMException && error.name === "AbortError") {
1212
1387
  if (!timedOut && isAborted(caller)) throw error;
1213
- throw new TimeoutError(
1214
- `Request timed out after ${config.timeout ?? this.timeout}ms`,
1215
- config.timeout ?? this.timeout
1216
- );
1388
+ const timeout = config.timeout ?? this.connection.timeout;
1389
+ throw new TimeoutError(`Request timed out after ${timeout}ms`, timeout);
1217
1390
  }
1218
1391
  if (error instanceof TypeError && error.message.includes("CORS")) {
1219
1392
  const corsError = new NetworkError(
@@ -1227,9 +1400,10 @@ var _HttpClient = class _HttpClient {
1227
1400
  suggestion: "Check if the API server allows requests from your domain and has proper CORS headers configured."
1228
1401
  }
1229
1402
  );
1230
- if (typeof window !== "undefined" && this.config.browser.onCorsError) {
1403
+ const { onCorsError } = this.connection.config.browser;
1404
+ if (isBrowser() && onCorsError) {
1231
1405
  try {
1232
- this.config.browser.onCorsError(corsError, {
1406
+ onCorsError(corsError, {
1233
1407
  url: config.url,
1234
1408
  method: config.method,
1235
1409
  originalError: error
@@ -1246,20 +1420,20 @@ var _HttpClient = class _HttpClient {
1246
1420
  }
1247
1421
  }
1248
1422
  /**
1249
- * Filter headers that browsers don't allow to be set manually
1423
+ * The headers without those a browser controls itself, when this is a browser; unchanged when it
1424
+ * is not.
1425
+ *
1426
+ * A name is matched without regard to case (RFC 9110 §5.1), because a browser refuses `Origin`
1427
+ * whatever case a page spells it in. This matched only the all-lowercase and all-uppercase
1428
+ * spellings, so `Origin` and `User-Agent` — how people write them — reached `fetch` (REL-1409).
1250
1429
  */
1251
1430
  filterHeadersForBrowser(headers) {
1252
- if (typeof window !== "undefined") {
1253
- const filteredHeaders = { ...headers };
1254
- const forbiddenHeaders = ["user-agent", "referer", "origin", "host", "connection", "upgrade"];
1255
- forbiddenHeaders.forEach((header) => {
1256
- delete filteredHeaders[header];
1257
- delete filteredHeaders[header.toLowerCase()];
1258
- delete filteredHeaders[header.toUpperCase()];
1259
- });
1260
- return filteredHeaders;
1261
- }
1262
- return headers;
1431
+ if (!isBrowser()) return headers;
1432
+ return Object.fromEntries(
1433
+ Object.entries(headers).filter(
1434
+ ([name]) => !BROWSER_CONTROLLED_HEADERS.has(name.toLowerCase())
1435
+ )
1436
+ );
1263
1437
  }
1264
1438
  /**
1265
1439
  * Prepare request body based on content type and environment
@@ -1295,7 +1469,7 @@ var _HttpClient = class _HttpClient {
1295
1469
  if (url.startsWith("http://") || url.startsWith("https://")) {
1296
1470
  return url;
1297
1471
  }
1298
- return `${this.baseUrl}${url.startsWith("/") ? "" : "/"}${url}`;
1472
+ return `${this.connection.baseUrl}${url.startsWith("/") ? "" : "/"}${url}`;
1299
1473
  }
1300
1474
  mergeRequestConfig(config) {
1301
1475
  const url = this.resolveUrl(config.url);
@@ -1304,11 +1478,11 @@ var _HttpClient = class _HttpClient {
1304
1478
  return {
1305
1479
  ...config,
1306
1480
  url: finalUrl,
1307
- headers: {
1308
- ...this.defaultHeaders,
1309
- ...config.headers
1310
- },
1311
- timeout: config.timeout ?? this.timeout
1481
+ // The request's own headers replace the client's of the same name, whatever the case: a
1482
+ // per-request `x-relyio-dataset` sent beside the client's `X-RelyIO-Dataset` is one header
1483
+ // `Headers` joins into `main, staging`.
1484
+ headers: layerHeaders(this.connection.defaultHeaders, config.headers),
1485
+ timeout: config.timeout ?? this.connection.timeout
1312
1486
  };
1313
1487
  }
1314
1488
  buildQueryString(params) {
@@ -1332,9 +1506,10 @@ var _HttpClient = class _HttpClient {
1332
1506
  return obj;
1333
1507
  }
1334
1508
  log(level, message, data) {
1335
- if (!this.debug) return;
1336
- if (this.logger != null) {
1337
- this.logger(level, message, data);
1509
+ const { debug } = this.connection.config;
1510
+ if (!debug.enabled) return;
1511
+ if (debug.logger != null) {
1512
+ debug.logger(level, message, data);
1338
1513
  } else {
1339
1514
  console.log(`[RelyIO SDK ${level.toUpperCase()}] ${message}`, data ?? "");
1340
1515
  }
@@ -1343,29 +1518,104 @@ var _HttpClient = class _HttpClient {
1343
1518
  __name(_HttpClient, "HttpClient");
1344
1519
  var HttpClient = _HttpClient;
1345
1520
  var API_KEY_HEADER = "X-API-Key";
1521
+ var DATASET_HEADER = "X-RelyIO-Dataset";
1522
+ var BROWSER_CONTROLLED_HEADERS = /* @__PURE__ */ new Set([
1523
+ "user-agent",
1524
+ "referer",
1525
+ "origin",
1526
+ "host",
1527
+ "connection",
1528
+ "upgrade"
1529
+ ]);
1530
+ function connectionFor(config) {
1531
+ const globalWithVersion = globalThis;
1532
+ const defaultHeaders = layerHeaders(
1533
+ layerHeaders({ "Content-Type": "application/json" }, config.headers),
1534
+ {
1535
+ [API_KEY_HEADER]: presentKey(config.apiKey),
1536
+ [DATASET_HEADER]: config.dataset,
1537
+ // Only outside a browser, which controls this header. "A browser" is the core's one
1538
+ // definition, `isBrowser()` — every environment question in this file asks it.
1539
+ "User-Agent": isBrowser() ? void 0 : `RelyIO-SDK/${globalWithVersion.__VERSION__ ?? "0.0.0"}`
1540
+ }
1541
+ );
1542
+ return {
1543
+ config,
1544
+ baseUrl: config.baseUrl,
1545
+ defaultHeaders,
1546
+ timeout: config.timeout,
1547
+ // The policy, not a predicate over `(error, attempt)`: whether a request may be sent again is a
1548
+ // property of the **route** as much as of the refusal, and a callback that never sees the
1549
+ // request cannot express that.
1550
+ retryPolicy: new DefaultRetryPolicy({
1551
+ maxAttempts: config.retryAttempts,
1552
+ baseDelay: 1e3,
1553
+ maxDelay: 1e4,
1554
+ backoffFactor: 2,
1555
+ jitter: true
1556
+ })
1557
+ };
1558
+ }
1559
+ __name(connectionFor, "connectionFor");
1560
+ function layerHeaders(base, over) {
1561
+ const layered = { ...base };
1562
+ for (const [name, value] of Object.entries(over ?? {})) {
1563
+ if (value === void 0) continue;
1564
+ const lower = name.toLowerCase();
1565
+ for (const existing of Object.keys(layered)) {
1566
+ if (existing.toLowerCase() === lower) delete layered[existing];
1567
+ }
1568
+ if (value !== null) layered[name] = value;
1569
+ }
1570
+ return layered;
1571
+ }
1572
+ __name(layerHeaders, "layerHeaders");
1346
1573
  function presentKey(value) {
1347
1574
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
1348
1575
  }
1349
1576
  __name(presentKey, "presentKey");
1350
1577
  function credentialsModeFor(includeCredentials) {
1351
- switch (includeCredentials) {
1352
- case false: {
1353
- return "same-origin";
1354
- }
1355
- case true:
1356
- case void 0: {
1357
- return "include";
1358
- }
1359
- }
1578
+ return includeCredentials ? "include" : "same-origin";
1360
1579
  }
1361
1580
  __name(credentialsModeFor, "credentialsModeFor");
1362
1581
 
1582
+ // src/content/client-locale.ts
1583
+ function callLocale(named, clientLocale) {
1584
+ if (named !== void 0) return named;
1585
+ return clientLocale === void 0 || clientLocale === "" ? void 0 : clientLocale;
1586
+ }
1587
+ __name(callLocale, "callLocale");
1588
+ function readClientLocale(locale) {
1589
+ return typeof locale === "function" ? locale : () => locale;
1590
+ }
1591
+ __name(readClientLocale, "readClientLocale");
1592
+
1363
1593
  // src/content/query-builder.ts
1594
+ function namesMetadata(key) {
1595
+ return key === "_metadata" || key.startsWith("_metadata.");
1596
+ }
1597
+ __name(namesMetadata, "namesMetadata");
1598
+ function refuseMetadataConditions(conditions) {
1599
+ const named = Object.keys(conditions).filter(namesMetadata);
1600
+ if (named.length === 0) return;
1601
+ throw new ValidationError(
1602
+ `A condition cannot name ${named.join(", ")}: the API filters on the fields the content type declares, and reports _metadata beside them. Filter on a field, and read _metadata from the result.`,
1603
+ named.map((field) => ({
1604
+ field,
1605
+ code: "METADATA_CONDITION",
1606
+ message: `${field} is not a field the API can filter on`
1607
+ }))
1608
+ );
1609
+ }
1610
+ __name(refuseMetadataConditions, "refuseMetadataConditions");
1364
1611
  var _ContentQueryBuilder = class _ContentQueryBuilder {
1365
- constructor(httpClient, contentType, defaultLocale) {
1612
+ /**
1613
+ * @param locale - The client's locale (`withLocale()`, `updateConfig({ locale })`), sent when the
1614
+ * query names none: the function `TypedRelyIOClient` hands over, or a locale fixed for it
1615
+ */
1616
+ constructor(httpClient, contentType, locale) {
1366
1617
  this.httpClient = httpClient;
1367
1618
  this.contentType = contentType;
1368
- this.defaultLocale = defaultLocale;
1369
1619
  __publicField(this, "queryOptions", {});
1370
1620
  __publicField(this, "localeValue");
1371
1621
  __publicField(this, "localesArray");
@@ -1375,6 +1625,21 @@ var _ContentQueryBuilder = class _ContentQueryBuilder {
1375
1625
  __publicField(this, "pageNumber");
1376
1626
  __publicField(this, "pageSizeValue");
1377
1627
  __publicField(this, "requestHeaders");
1628
+ /** The client's locale, asked when the query is sent rather than copied when it was built. */
1629
+ __publicField(this, "clientLocale");
1630
+ __publicField(this, "freshValue");
1631
+ this.clientLocale = readClientLocale(locale);
1632
+ }
1633
+ /**
1634
+ * The feed position this read follows — a change's `fresh` (`FeedChange`), or a resync's. Sent as
1635
+ * `?_fresh`, the one term the API declares for it: no cache — the Content API's edge or the
1636
+ * browser's own — holds an answer for that position from before it, so the read is answered after
1637
+ * the change, and every read naming the same position shares that answer. The answer is the
1638
+ * read's own; omit it for a read no change prompted.
1639
+ */
1640
+ fresh(position) {
1641
+ this.freshValue = position;
1642
+ return this;
1378
1643
  }
1379
1644
  /**
1380
1645
  * Headers this query adds to the client's own, e.g. `X-RelyIO-Dataset` to list another dataset
@@ -1386,9 +1651,11 @@ var _ContentQueryBuilder = class _ContentQueryBuilder {
1386
1651
  return this;
1387
1652
  }
1388
1653
  /**
1389
- * Add where conditions to the query
1654
+ * Add where conditions to the query: conditions on the fields the content type declares. A
1655
+ * condition on `_metadata` is refused here, before anything is sent (`refuseMetadataConditions`).
1390
1656
  */
1391
1657
  where(conditions) {
1658
+ refuseMetadataConditions(conditions);
1392
1659
  this.queryOptions.where = {
1393
1660
  ...this.queryOptions.where,
1394
1661
  ...conditions
@@ -1645,7 +1912,11 @@ var _ContentQueryBuilder = class _ContentQueryBuilder {
1645
1912
  * Clone the query builder
1646
1913
  */
1647
1914
  clone() {
1648
- const cloned = new _ContentQueryBuilder(this.httpClient, this.contentType, this.defaultLocale);
1915
+ const cloned = new _ContentQueryBuilder(
1916
+ this.httpClient,
1917
+ this.contentType,
1918
+ this.clientLocale
1919
+ );
1649
1920
  cloned.queryOptions = JSON.parse(JSON.stringify(this.queryOptions));
1650
1921
  cloned.localeValue = this.localeValue;
1651
1922
  cloned.localesArray = this.localesArray ? [...this.localesArray] : void 0;
@@ -1654,6 +1925,7 @@ var _ContentQueryBuilder = class _ContentQueryBuilder {
1654
1925
  cloned.pageNumber = this.pageNumber;
1655
1926
  cloned.pageSizeValue = this.pageSizeValue;
1656
1927
  cloned.requestHeaders = this.requestHeaders ? { ...this.requestHeaders } : void 0;
1928
+ cloned.freshValue = this.freshValue;
1657
1929
  return cloned;
1658
1930
  }
1659
1931
  /**
@@ -1720,14 +1992,15 @@ var _ContentQueryBuilder = class _ContentQueryBuilder {
1720
1992
  if (this.queryOptions.cursor !== void 0) {
1721
1993
  params.cursor = this.queryOptions.cursor;
1722
1994
  }
1995
+ const clientLocale = this.clientLocale();
1723
1996
  if (this.localeValue != null && this.localeValue !== "") {
1724
1997
  params.locale = this.localeValue;
1725
1998
  } else if (this.fetchAllLocales) {
1726
1999
  params.locale = "*";
1727
2000
  } else if (this.localesArray && this.localesArray.length > 0) {
1728
2001
  params.locales = this.localesArray.join(",");
1729
- } else if (this.defaultLocale != null && this.defaultLocale !== "") {
1730
- params.locale = this.defaultLocale;
2002
+ } else if (clientLocale != null && clientLocale !== "") {
2003
+ params.locale = clientLocale;
1731
2004
  }
1732
2005
  if (this.pageNumber !== void 0) {
1733
2006
  params.page = this.pageNumber;
@@ -1745,23 +2018,44 @@ var _ContentQueryBuilder = class _ContentQueryBuilder {
1745
2018
  params.childrenLimit = this.childrenOptions.limit;
1746
2019
  }
1747
2020
  }
2021
+ if (this.freshValue !== void 0) {
2022
+ params[FRESH_PARAMETER] = this.freshValue;
2023
+ }
1748
2024
  return params;
1749
2025
  }
1750
2026
  };
1751
2027
  __name(_ContentQueryBuilder, "ContentQueryBuilder");
1752
2028
  var ContentQueryBuilder = _ContentQueryBuilder;
1753
2029
  var _ContentByIdBuilder = class _ContentByIdBuilder {
1754
- constructor(httpClient, contentType, id, defaultLocale) {
2030
+ /**
2031
+ * @param locale - The client's locale (`withLocale()`, `updateConfig({ locale })`), sent when the
2032
+ * read names none: the function `TypedRelyIOClient` hands over, or a locale fixed for it
2033
+ */
2034
+ constructor(httpClient, contentType, id, locale) {
1755
2035
  this.httpClient = httpClient;
1756
2036
  this.contentType = contentType;
1757
2037
  this.id = id;
1758
- this.defaultLocale = defaultLocale;
1759
2038
  __publicField(this, "includeRelations");
1760
2039
  __publicField(this, "localeValue");
1761
2040
  __publicField(this, "localesArray");
1762
2041
  __publicField(this, "fetchAllLocales", false);
1763
2042
  __publicField(this, "childrenOptions");
1764
2043
  __publicField(this, "requestHeaders");
2044
+ /** The client's locale, asked when the read is sent rather than copied when it was built. */
2045
+ __publicField(this, "clientLocale");
2046
+ __publicField(this, "freshValue");
2047
+ this.clientLocale = readClientLocale(locale);
2048
+ }
2049
+ /**
2050
+ * The feed position this read follows — a change's `fresh` (`FeedChange`), or a resync's. Sent as
2051
+ * `?_fresh`, the one term the API declares for it: no cache — the Content API's edge or the
2052
+ * browser's own — holds an answer for that position from before it, so the read is answered after
2053
+ * the change, and every read naming the same position shares that answer. The answer is the
2054
+ * read's own; omit it for a read no change prompted.
2055
+ */
2056
+ fresh(position) {
2057
+ this.freshValue = position;
2058
+ return this;
1765
2059
  }
1766
2060
  /**
1767
2061
  * Include related content in the response
@@ -1831,14 +2125,15 @@ var _ContentByIdBuilder = class _ContentByIdBuilder {
1831
2125
  if (this.includeRelations != null) {
1832
2126
  params.include = this.includeRelations.join(",");
1833
2127
  }
2128
+ const clientLocale = this.clientLocale();
1834
2129
  if (this.localeValue != null && this.localeValue !== "") {
1835
2130
  params.locale = this.localeValue;
1836
2131
  } else if (this.fetchAllLocales) {
1837
2132
  params.locale = "*";
1838
2133
  } else if (this.localesArray && this.localesArray.length > 0) {
1839
2134
  params.locales = this.localesArray.join(",");
1840
- } else if (this.defaultLocale != null && this.defaultLocale !== "") {
1841
- params.locale = this.defaultLocale;
2135
+ } else if (clientLocale != null && clientLocale !== "") {
2136
+ params.locale = clientLocale;
1842
2137
  }
1843
2138
  if (this.childrenOptions != null) {
1844
2139
  if (this.childrenOptions.contentTypes) {
@@ -1850,6 +2145,9 @@ var _ContentByIdBuilder = class _ContentByIdBuilder {
1850
2145
  params.childrenLimit = this.childrenOptions.limit;
1851
2146
  }
1852
2147
  }
2148
+ if (this.freshValue !== void 0) {
2149
+ params[FRESH_PARAMETER] = this.freshValue;
2150
+ }
1853
2151
  try {
1854
2152
  return await this.httpClient.fetchPayload(
1855
2153
  "/v1/{contentType}/{id}",
@@ -1903,22 +2201,28 @@ __name(entityTagOf, "entityTagOf");
1903
2201
 
1904
2202
  // src/content/content-client.ts
1905
2203
  var _ContentClient = class _ContentClient {
1906
- constructor(httpClient, contentType, defaultLocale) {
2204
+ /**
2205
+ * @param locale - The client's locale: the function `TypedRelyIOClient` hands over, asked on
2206
+ * every call, or a locale fixed for this client
2207
+ */
2208
+ constructor(httpClient, contentType, locale) {
1907
2209
  this.httpClient = httpClient;
1908
2210
  this.contentType = contentType;
1909
- this.defaultLocale = defaultLocale;
2211
+ /** The client's locale, asked on every call rather than copied when this client was taken. */
2212
+ __publicField(this, "clientLocale");
2213
+ this.clientLocale = readClientLocale(locale);
1910
2214
  }
1911
2215
  /**
1912
2216
  * Create a new query builder for this content type
1913
2217
  */
1914
2218
  query() {
1915
- return new ContentQueryBuilder(this.httpClient, this.contentType, this.defaultLocale);
2219
+ return new ContentQueryBuilder(this.httpClient, this.contentType, this.clientLocale);
1916
2220
  }
1917
2221
  /**
1918
2222
  * Find content by ID with fluent options
1919
2223
  */
1920
2224
  findById(id) {
1921
- return new ContentByIdBuilder(this.httpClient, this.contentType, id, this.defaultLocale);
2225
+ return new ContentByIdBuilder(this.httpClient, this.contentType, id, this.clientLocale);
1922
2226
  }
1923
2227
  /**
1924
2228
  * Fetch all content items
@@ -2002,7 +2306,7 @@ var _ContentClient = class _ContentClient {
2002
2306
  async fetchOneWithTag(id, options) {
2003
2307
  const query = {};
2004
2308
  if (options?.include != null) query.include = options.include.join(",");
2005
- const locale = options?.locale ?? this.defaultLocale;
2309
+ const locale = options?.locale ?? this.clientLocale();
2006
2310
  if (locale != null && locale !== "") query.locale = locale;
2007
2311
  const response = await this.httpClient.send(
2008
2312
  "/v1/{contentType}/{id}",
@@ -2032,16 +2336,17 @@ var _ContentClient = class _ContentClient {
2032
2336
  * @param options - Create options (slug, locale, parentId, sortOrder), and `headers` this one
2033
2337
  * request adds — e.g. `{ 'X-RelyIO-Dataset': 'staging' }` to write a dataset other than the
2034
2338
  * client's. They are sent, never put in the body; `idempotencyKey` still wins over an
2035
- * `Idempotency-Key` among them.
2339
+ * `Idempotency-Key` among them. `locale` defaults to the client's own (`withLocale()`).
2036
2340
  */
2037
2341
  async create(data, options) {
2038
- const { idempotencyKey, headers, ...body } = options ?? {};
2342
+ const { idempotencyKey, headers, locale, ...body } = options ?? {};
2343
+ const sent = callLocale(locale, this.clientLocale());
2039
2344
  return this.httpClient.fetchPayload(
2040
2345
  "/v1/{contentType}",
2041
2346
  "post",
2042
2347
  {
2043
2348
  path: { contentType: this.contentType },
2044
- body: { content: data, ...body },
2349
+ body: { content: data, ...body, ...sent === void 0 ? {} : { locale: sent } },
2045
2350
  headers: requestHeaders(headers, writeHeaders({ idempotencyKey }))
2046
2351
  }
2047
2352
  );
@@ -2052,16 +2357,18 @@ var _ContentClient = class _ContentClient {
2052
2357
  * @param data - Fields to update
2053
2358
  * @param options - Update options (slug, locale, parentId, sortOrder), and `headers` this one
2054
2359
  * request adds, as for {@link create}. `ifMatch` and `idempotencyKey` win over the same
2055
- * header among them.
2360
+ * header among them. `locale` defaults to the client's own, the locale whose entity tag
2361
+ * {@link fetchOneWithTag} reads, so the tag it hands back is checked against the same locale.
2056
2362
  */
2057
2363
  async update(id, data, options) {
2058
- const { idempotencyKey, ifMatch, headers, ...body } = options ?? {};
2364
+ const { idempotencyKey, ifMatch, headers, locale, ...body } = options ?? {};
2365
+ const sent = callLocale(locale, this.clientLocale());
2059
2366
  return this.httpClient.fetchPayload(
2060
2367
  "/v1/{contentType}/{id}",
2061
2368
  "patch",
2062
2369
  {
2063
2370
  path: { contentType: this.contentType, id },
2064
- body: { content: data, ...body },
2371
+ body: { content: data, ...body, ...sent === void 0 ? {} : { locale: sent } },
2065
2372
  headers: requestHeaders(headers, writeHeaders({ idempotencyKey, ifMatch }))
2066
2373
  }
2067
2374
  );
@@ -2093,32 +2400,35 @@ var _ContentClient = class _ContentClient {
2093
2400
  * Get version history for one locale of an entry.
2094
2401
  *
2095
2402
  * Versions are numbered per locale, so this is one locale's timeline: `locale` defaults to the
2096
- * schema default, no fallback is applied, and a locale with no history returns an empty array
2097
- * rather than another locale's rows. Every row names the locale it belongs to.
2403
+ * client's own (`withLocale()`), else the schema default; no fallback is applied, and a locale
2404
+ * with no history returns an empty array rather than another locale's rows. Every row names the
2405
+ * locale it belongs to.
2098
2406
  */
2099
2407
  async versions(id, options) {
2408
+ const locale = callLocale(options?.locale, this.clientLocale());
2100
2409
  return this.httpClient.fetchPayload("/v1/{contentType}/{id}/versions", "get", {
2101
2410
  path: { contentType: this.contentType, id },
2102
2411
  query: {
2103
2412
  limit: options?.limit ?? 10,
2104
- ...options?.locale === void 0 ? {} : { locale: options.locale }
2413
+ ...locale === void 0 ? {} : { locale }
2105
2414
  }
2106
2415
  });
2107
2416
  }
2108
2417
  /**
2109
2418
  * Get a specific version of an entry.
2110
2419
  *
2111
- * `locale` defaults to the schema default. Version numbers are per locale, so version 2 is
2112
- * different content in each one.
2420
+ * `locale` defaults to the client's own (`withLocale()`), else the schema default. Version
2421
+ * numbers are per locale, so version 2 is different content in each one.
2113
2422
  */
2114
2423
  async version(id, versionNumber, options) {
2424
+ const locale = callLocale(options?.locale, this.clientLocale());
2115
2425
  return this.httpClient.fetchPayload("/v1/{contentType}/{id}/versions/{versionNumber}", "get", {
2116
2426
  path: {
2117
2427
  contentType: this.contentType,
2118
2428
  id,
2119
2429
  versionNumber: String(versionNumber)
2120
2430
  },
2121
- query: options?.locale === void 0 ? void 0 : { locale: options.locale }
2431
+ query: locale === void 0 ? void 0 : { locale }
2122
2432
  });
2123
2433
  }
2124
2434
  /**
@@ -2157,7 +2467,7 @@ var _ContentClient = class _ContentClient {
2157
2467
  if (options?.include != null) {
2158
2468
  params.include = options.include.join(",");
2159
2469
  }
2160
- const locale = options?.locale ?? this.defaultLocale;
2470
+ const locale = options?.locale ?? this.clientLocale();
2161
2471
  if (locale != null && locale !== "") {
2162
2472
  params.locale = locale;
2163
2473
  }
@@ -2177,19 +2487,20 @@ var _ContentClient = class _ContentClient {
2177
2487
  * Create a new ContentClient for a different content type
2178
2488
  */
2179
2489
  ofType(contentType) {
2180
- return new _ContentClient(this.httpClient, contentType, this.defaultLocale);
2490
+ return new _ContentClient(this.httpClient, contentType, this.clientLocale);
2181
2491
  }
2182
2492
  /**
2183
- * Create a new ContentClient with a different default locale
2493
+ * Create a new ContentClient with a different default locale, fixed: an update of the client's
2494
+ * locale (`updateConfig({ locale })`) does not reach it
2184
2495
  */
2185
2496
  withLocale(locale) {
2186
2497
  return new _ContentClient(this.httpClient, this.contentType, locale);
2187
2498
  }
2188
2499
  /**
2189
- * Get the current default locale
2500
+ * Get the default locale as it is now: the client's, or the one `withLocale()` fixed
2190
2501
  */
2191
2502
  getDefaultLocale() {
2192
- return this.defaultLocale;
2503
+ return this.clientLocale();
2193
2504
  }
2194
2505
  };
2195
2506
  __name(_ContentClient, "ContentClient");
@@ -2198,7 +2509,7 @@ var ContentClient = _ContentClient;
2198
2509
  // src/errors/file-errors.ts
2199
2510
  var _FileUploadError = class _FileUploadError extends SDKError {
2200
2511
  constructor(code, message, options = {}) {
2201
- super(message, options.statusCode, options.cause, { fileErrorCode: code });
2512
+ super(message, options.statusCode, options.cause, { ...options.context, fileErrorCode: code });
2202
2513
  __publicField(this, "code");
2203
2514
  __publicField(this, "retryable");
2204
2515
  this.code = code;
@@ -2245,11 +2556,52 @@ function validateFile(file, constraints = {}) {
2245
2556
  }
2246
2557
  __name(validateFile, "validateFile");
2247
2558
 
2559
+ // src/errors/file-error-codes.ts
2560
+ var FILE_ERROR_CODES = [
2561
+ "UPLOAD_TOO_LARGE",
2562
+ "UNSUPPORTED_MIME",
2563
+ "DIMENSION_OUT_OF_RANGE",
2564
+ "ASPECT_RATIO",
2565
+ "CORRUPT_FILE",
2566
+ "UNSUPPORTED_CODEC",
2567
+ "IMAGES_LIMIT_EXCEEDED",
2568
+ "STREAM_ENCODE_FAILED",
2569
+ "SCAN_INFECTED",
2570
+ "INTERNAL"
2571
+ ];
2572
+
2248
2573
  // src/content/asset-client.ts
2574
+ function assertWaitOptions(options) {
2575
+ const { timeoutMs, pollIntervalMs } = options;
2576
+ if (timeoutMs !== void 0 && !(typeof timeoutMs === "number" && timeoutMs >= 0 && timeoutMs <= MAX_TIMER_DELAY_MS)) {
2577
+ throw new Error(
2578
+ `timeoutMs must be a number of milliseconds from 0 to ${MAX_TIMER_DELAY_MS}, and got ${String(timeoutMs)}`
2579
+ );
2580
+ }
2581
+ if (pollIntervalMs !== void 0 && !isTimerDelay(pollIntervalMs)) {
2582
+ throw new Error(
2583
+ `pollIntervalMs must be a number of milliseconds from 1 to ${MAX_TIMER_DELAY_MS} \u2014 a timer given 0, NaN, Infinity or more fires at once \u2014 and got ${String(pollIntervalMs)}`
2584
+ );
2585
+ }
2586
+ }
2587
+ __name(assertWaitOptions, "assertWaitOptions");
2249
2588
  function hasText(value) {
2250
2589
  return value != null && value !== "";
2251
2590
  }
2252
2591
  __name(hasText, "hasText");
2592
+ function processingFailure(subject, stored) {
2593
+ const storedCode = stored?.code;
2594
+ const code = FILE_ERROR_CODES.find((known) => known === storedCode) ?? "PROCESSING_FAILED";
2595
+ return new FileUploadError(
2596
+ code,
2597
+ `Asset ${subject}: ${stored?.message ?? "File processing failed"}`,
2598
+ {
2599
+ retriable: stored?.retriable === true,
2600
+ context: { processingError: stored }
2601
+ }
2602
+ );
2603
+ }
2604
+ __name(processingFailure, "processingFailure");
2253
2605
  async function computeSha256Hex(blob) {
2254
2606
  try {
2255
2607
  const subtle = globalThis.crypto?.subtle;
@@ -2265,11 +2617,17 @@ async function computeSha256Hex(blob) {
2265
2617
  }
2266
2618
  __name(computeSha256Hex, "computeSha256Hex");
2267
2619
  var _AssetFileBuilder = class _AssetFileBuilder {
2268
- constructor(httpClient, assetType, entryUid, defaultLocale) {
2620
+ /**
2621
+ * @param locale - The client's locale, sent when a call names none: the function
2622
+ * `TypedRelyIOClient` hands over, or a locale fixed for this builder
2623
+ */
2624
+ constructor(httpClient, assetType, entryUid, locale) {
2269
2625
  this.httpClient = httpClient;
2270
2626
  this.assetType = assetType;
2271
2627
  this.entryUid = entryUid;
2272
- this.defaultLocale = defaultLocale;
2628
+ /** The client's locale, asked on every call rather than copied when this builder was taken. */
2629
+ __publicField(this, "clientLocale");
2630
+ this.clientLocale = readClientLocale(locale);
2273
2631
  }
2274
2632
  /**
2275
2633
  * The path parameters every `/file/*` route on this builder takes.
@@ -2282,7 +2640,16 @@ var _AssetFileBuilder = class _AssetFileBuilder {
2282
2640
  return { contentType: this.assetType, id: this.entryUid };
2283
2641
  }
2284
2642
  withLocale(locale) {
2285
- return locale ?? this.defaultLocale;
2643
+ return locale ?? this.clientLocale();
2644
+ }
2645
+ /**
2646
+ * The `locale` body field of a multipart call: the session's, else the client's. The API finds
2647
+ * the binding by it, so a session a `withLocale()` client opened must name the same locale on
2648
+ * every later call or its `objectKey` is refused as another binding's.
2649
+ */
2650
+ localeField(locale) {
2651
+ const sent = this.withLocale(locale);
2652
+ return hasText(sent) ? { locale: sent } : {};
2286
2653
  }
2287
2654
  /**
2288
2655
  * Issue a single upload target — an R2 presigned PUT (image/file) or a
@@ -2341,7 +2708,7 @@ var _AssetFileBuilder = class _AssetFileBuilder {
2341
2708
  uploadId: target.uploadId,
2342
2709
  objectKey: target.objectKey,
2343
2710
  partNumber: target.partNumber,
2344
- ...hasText(target.locale) ? { locale: target.locale } : {}
2711
+ ...this.localeField(target.locale)
2345
2712
  }
2346
2713
  });
2347
2714
  }
@@ -2352,7 +2719,7 @@ var _AssetFileBuilder = class _AssetFileBuilder {
2352
2719
  body: {
2353
2720
  uploadId: target.uploadId,
2354
2721
  objectKey: target.objectKey,
2355
- ...hasText(target.locale) ? { locale: target.locale } : {}
2722
+ ...this.localeField(target.locale)
2356
2723
  }
2357
2724
  });
2358
2725
  return payload.parts;
@@ -2365,7 +2732,7 @@ var _AssetFileBuilder = class _AssetFileBuilder {
2365
2732
  uploadId: target.uploadId,
2366
2733
  objectKey: target.objectKey,
2367
2734
  parts: target.parts.map((p) => ({ partNumber: p.partNumber, etag: p.etag })),
2368
- ...hasText(target.locale) ? { locale: target.locale } : {}
2735
+ ...this.localeField(target.locale)
2369
2736
  }
2370
2737
  });
2371
2738
  }
@@ -2376,7 +2743,7 @@ var _AssetFileBuilder = class _AssetFileBuilder {
2376
2743
  body: {
2377
2744
  uploadId: target.uploadId,
2378
2745
  objectKey: target.objectKey,
2379
- ...hasText(target.locale) ? { locale: target.locale } : {}
2746
+ ...this.localeField(target.locale)
2380
2747
  }
2381
2748
  });
2382
2749
  }
@@ -2396,6 +2763,25 @@ var _AssetFileBuilder = class _AssetFileBuilder {
2396
2763
  { path: this.target, query: params }
2397
2764
  );
2398
2765
  }
2766
+ /**
2767
+ * Read where this file's upload got to — `status`, and the processing stage, progress and error
2768
+ * the pipeline recorded — whatever the asset entry is published in.
2769
+ *
2770
+ * An asset is a draft until its file is ready, and stays one when its file fails, so a key that
2771
+ * reads published content finds no entry for an upload in flight or one that failed. This read
2772
+ * answers for the binding itself. It takes the capability the upload calls take, so a read-only
2773
+ * key is refused with `ReadOnlyKeyError`, and it never returns the entry's fields.
2774
+ */
2775
+ async status(options = {}) {
2776
+ const params = {};
2777
+ const locale = this.withLocale(options.locale);
2778
+ if (hasText(locale)) params.locale = locale;
2779
+ return this.httpClient.fetchPayload(
2780
+ "/v1/{contentType}/{id}/file",
2781
+ "get",
2782
+ { path: this.target, query: params }
2783
+ );
2784
+ }
2399
2785
  /**
2400
2786
  * Update the binding's delivery visibility (token-based; no byte relocation —
2401
2787
  * a downgrade purges the gateway cache). Returns the refreshed asset entry.
@@ -2434,14 +2820,31 @@ var _AssetFileBuilder = class _AssetFileBuilder {
2434
2820
  });
2435
2821
  }
2436
2822
  /**
2437
- * Poll the asset entry until its file binding reaches `ready` (resolves with
2438
- * the ready entry) or `failed` (throws). Times out after `timeoutMs`.
2823
+ * Poll the asset entry until its file binding reaches `ready`, and resolve with the ready entry.
2824
+ *
2825
+ * Each poll reads the entry through the key's lens. When that finds nothing — a key that reads
2826
+ * published content, and an asset that is a draft until its file is ready and stays one when it
2827
+ * fails — the poll reads the binding itself ({@link status}), so a failed upload rejects as one
2828
+ * rather than running out the limit (REL-1481).
2829
+ *
2830
+ * @throws {FileUploadError} When processing failed. `code` is the processing code the service
2831
+ * stored — `UNSUPPORTED_CODEC`, `CORRUPT_FILE` and so on — or `PROCESSING_FAILED` where it
2832
+ * stored none this SDK knows; `context.processingError` holds what it stored, and `retryable`
2833
+ * is its `retriable` hint.
2834
+ * @throws {TimeoutError} When `timeoutMs` passes first. The file may still become ready; the
2835
+ * error's `context.timeout` is the limit and `context.fileStatus` the status last seen. A file
2836
+ * that is `ready` in an asset the key's lens never shows — created with `publish: false`, or
2837
+ * awaiting a workflow — ends here too, and the message says so.
2838
+ * @throws {ReadOnlyKeyError} When the key's lens does not show the asset and the key may not read
2839
+ * its binding either: nothing it can read would ever report the file.
2439
2840
  */
2440
2841
  async waitUntilReady(options = {}) {
2842
+ assertWaitOptions(options);
2441
2843
  const timeoutMs = options.timeoutMs ?? 6e4;
2442
2844
  const pollIntervalMs = options.pollIntervalMs ?? 1500;
2443
2845
  const locale = this.withLocale(options.locale);
2444
2846
  const deadline = Date.now() + timeoutMs;
2847
+ const subject = `${this.assetType}/${this.entryUid}`;
2445
2848
  for (; ; ) {
2446
2849
  const builder = new ContentByIdBuilder(
2447
2850
  this.httpClient,
@@ -2451,15 +2854,24 @@ var _AssetFileBuilder = class _AssetFileBuilder {
2451
2854
  );
2452
2855
  if (hasText(locale)) builder.locale(locale);
2453
2856
  const entry = await builder.execute();
2454
- const status = entry?.file?.status;
2455
- if (entry && status === "ready") return entry;
2456
- if (status === "failed") {
2457
- const message = entry?.file?.processing?.error?.message ?? "File processing failed";
2458
- throw new Error(`Asset ${this.assetType}/${this.entryUid}: ${message}`);
2857
+ let status;
2858
+ if (entry === null) {
2859
+ const binding = await this.status({ ...hasText(locale) ? { locale } : {} });
2860
+ status = binding.status;
2861
+ if (status === "failed") throw processingFailure(subject, binding.processing?.error);
2862
+ } else {
2863
+ status = entry.file?.status;
2864
+ if (status === "ready") return entry;
2865
+ if (status === "failed") throw processingFailure(subject, entry.file?.processing?.error);
2459
2866
  }
2460
2867
  if (Date.now() >= deadline) {
2461
- throw new Error(
2462
- `Timed out after ${timeoutMs}ms waiting for ${this.assetType}/${this.entryUid} (status: ${status ?? "unknown"})`
2868
+ const unseen = entry === null && status === "ready" ? "; the file is ready and this key's lens does not show the asset \u2014 it is not published" : "";
2869
+ throw new TimeoutError(
2870
+ `Timed out after ${timeoutMs}ms waiting for ${subject} (status: ${status ?? "unknown"})${unseen}`,
2871
+ timeoutMs,
2872
+ void 0,
2873
+ void 0,
2874
+ { fileStatus: status }
2463
2875
  );
2464
2876
  }
2465
2877
  await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
@@ -2469,11 +2881,14 @@ var _AssetFileBuilder = class _AssetFileBuilder {
2469
2881
  __name(_AssetFileBuilder, "AssetFileBuilder");
2470
2882
  var AssetFileBuilder = _AssetFileBuilder;
2471
2883
  var _AssetUploadBuilder = class _AssetUploadBuilder {
2472
- constructor(httpClient, assetType, file, defaultLocale) {
2884
+ /**
2885
+ * @param locale - The client's locale, the upload's when `.locale()` names none: the function
2886
+ * `TypedRelyIOClient` hands over, or a locale fixed for this upload
2887
+ */
2888
+ constructor(httpClient, assetType, file, locale) {
2473
2889
  this.httpClient = httpClient;
2474
2890
  this.assetType = assetType;
2475
2891
  this.file = file;
2476
- this.defaultLocale = defaultLocale;
2477
2892
  __publicField(this, "_fileName");
2478
2893
  __publicField(this, "_visibility");
2479
2894
  __publicField(this, "_locale");
@@ -2485,6 +2900,9 @@ var _AssetUploadBuilder = class _AssetUploadBuilder {
2485
2900
  __publicField(this, "_onProgress");
2486
2901
  __publicField(this, "_validate");
2487
2902
  __publicField(this, "_maxPartRetries", 3);
2903
+ /** The client's locale, read once when the upload starts. */
2904
+ __publicField(this, "clientLocale");
2905
+ this.clientLocale = readClientLocale(locale);
2488
2906
  }
2489
2907
  /** Subscribe to client → storage upload progress (single PUT + multipart). */
2490
2908
  onProgress(callback) {
@@ -2534,8 +2952,14 @@ var _AssetUploadBuilder = class _AssetUploadBuilder {
2534
2952
  this._slug = slug;
2535
2953
  return this;
2536
2954
  }
2537
- /** Wait until the file binding becomes `ready` before resolving. */
2955
+ /**
2956
+ * Wait until the file binding becomes `ready` before resolving.
2957
+ *
2958
+ * @throws {Error} When `timeoutMs` or `pollIntervalMs` is one the wait cannot honour — refused
2959
+ * here, before anything is uploaded, rather than after the bytes are in.
2960
+ */
2538
2961
  waitForReady(options = true) {
2962
+ if (typeof options === "object") assertWaitOptions(options);
2539
2963
  if (options === false) {
2540
2964
  this._waitForReady = false;
2541
2965
  } else {
@@ -2550,7 +2974,8 @@ var _AssetUploadBuilder = class _AssetUploadBuilder {
2550
2974
  if (!fileName) {
2551
2975
  throw new Error('A file name is required: pass a File, or call .filename("name.ext")');
2552
2976
  }
2553
- const locale = this._locale ?? this.defaultLocale;
2977
+ const clientLocale = this.clientLocale();
2978
+ const locale = this._locale ?? clientLocale;
2554
2979
  if (this._validate) {
2555
2980
  const violations = validateFile(this.file, this._validate);
2556
2981
  if (violations.length > 0) {
@@ -2561,7 +2986,7 @@ var _AssetUploadBuilder = class _AssetUploadBuilder {
2561
2986
  }
2562
2987
  }
2563
2988
  const hash = await computeSha256Hex(this.file);
2564
- const client = new AssetClient(this.httpClient, this.assetType, this.defaultLocale);
2989
+ const client = new AssetClient(this.httpClient, this.assetType, clientLocale);
2565
2990
  const created = await client.create(this._content, {
2566
2991
  ...hasText(this._slug) ? { slug: this._slug } : {},
2567
2992
  ...hasText(locale) ? { locale } : {},
@@ -2712,17 +3137,26 @@ var _AssetUploadBuilder = class _AssetUploadBuilder {
2712
3137
  __name(_AssetUploadBuilder, "AssetUploadBuilder");
2713
3138
  var AssetUploadBuilder = _AssetUploadBuilder;
2714
3139
  var _AssetClient = class _AssetClient {
2715
- constructor(httpClient, assetType, defaultLocale) {
3140
+ /**
3141
+ * @param locale - The client's locale: the function `TypedRelyIOClient` hands over, asked on
3142
+ * every call, or a locale fixed for this client
3143
+ */
3144
+ constructor(httpClient, assetType, locale) {
2716
3145
  this.httpClient = httpClient;
2717
3146
  this.assetType = assetType;
2718
- this.defaultLocale = defaultLocale;
3147
+ /** The client's locale, asked on every call rather than copied when this client was taken. */
3148
+ __publicField(this, "clientLocale");
3149
+ this.clientLocale = readClientLocale(locale);
2719
3150
  }
2720
- /** Create a query builder for this asset type. */
3151
+ /**
3152
+ * Create a query builder for this asset type. It resolves asset reads; its conditions name the
3153
+ * asset type's fields, which the `file` and `urls` blocks are not.
3154
+ */
2721
3155
  query() {
2722
3156
  return new ContentQueryBuilder(
2723
3157
  this.httpClient,
2724
3158
  this.assetType,
2725
- this.defaultLocale
3159
+ this.clientLocale
2726
3160
  );
2727
3161
  }
2728
3162
  /** Find an asset entry by id (or slug) with fluent options. */
@@ -2731,7 +3165,7 @@ var _AssetClient = class _AssetClient {
2731
3165
  this.httpClient,
2732
3166
  this.assetType,
2733
3167
  id,
2734
- this.defaultLocale
3168
+ this.clientLocale
2735
3169
  );
2736
3170
  }
2737
3171
  /** Fetch a single asset entry by id (or slug), or `null` if not found. */
@@ -2752,28 +3186,34 @@ var _AssetClient = class _AssetClient {
2752
3186
  * Create a new asset entry as a draft. The binary is uploaded separately via
2753
3187
  * `file(uid)` (or use the high-level `upload()` helper), and the entry
2754
3188
  * auto-publishes when its file becomes ready (unless `publish: false`).
3189
+ * `locale` defaults to the client's own (`withLocale()`).
2755
3190
  */
2756
3191
  async create(data, options) {
2757
- const { idempotencyKey, ...rest } = options ?? {};
3192
+ const { idempotencyKey, locale, ...rest } = options ?? {};
3193
+ const sent = callLocale(locale, this.clientLocale());
2758
3194
  return this.httpClient.fetchPayload(
2759
3195
  "/v1/{contentType}",
2760
3196
  "post",
2761
3197
  {
2762
3198
  path: { contentType: this.assetType },
2763
- body: { content: data, ...rest },
3199
+ body: { content: data, ...rest, ...sent === void 0 ? {} : { locale: sent } },
2764
3200
  headers: writeHeaders({ idempotencyKey })
2765
3201
  }
2766
3202
  );
2767
3203
  }
2768
- /** Update an asset entry's metadata fields (and/or slug). */
3204
+ /**
3205
+ * Update an asset entry's metadata fields (and/or slug). `locale` defaults to the client's own
3206
+ * (`withLocale()`).
3207
+ */
2769
3208
  async update(id, data, options) {
2770
- const { idempotencyKey, ifMatch, ...rest } = options ?? {};
3209
+ const { idempotencyKey, ifMatch, locale, ...rest } = options ?? {};
3210
+ const sent = callLocale(locale, this.clientLocale());
2771
3211
  return this.httpClient.fetchPayload(
2772
3212
  "/v1/{contentType}/{id}",
2773
3213
  "patch",
2774
3214
  {
2775
3215
  path: { contentType: this.assetType, id },
2776
- body: { content: data, ...rest },
3216
+ body: { content: data, ...rest, ...sent === void 0 ? {} : { locale: sent } },
2777
3217
  headers: writeHeaders({ idempotencyKey, ifMatch })
2778
3218
  }
2779
3219
  );
@@ -2787,21 +3227,24 @@ var _AssetClient = class _AssetClient {
2787
3227
  }
2788
3228
  /** Get the file sub-resource builder for a specific asset entry. */
2789
3229
  file(entryUid) {
2790
- return new AssetFileBuilder(this.httpClient, this.assetType, entryUid, this.defaultLocale);
3230
+ return new AssetFileBuilder(this.httpClient, this.assetType, entryUid, this.clientLocale);
2791
3231
  }
2792
3232
  /** Start a high-level upload (create → presign → PUT → confirm). */
2793
3233
  upload(file) {
2794
- return new AssetUploadBuilder(this.httpClient, this.assetType, file, this.defaultLocale);
3234
+ return new AssetUploadBuilder(this.httpClient, this.assetType, file, this.clientLocale);
2795
3235
  }
2796
3236
  /** Get the asset type id. */
2797
3237
  getAssetType() {
2798
3238
  return this.assetType;
2799
3239
  }
2800
- /** Get the current default locale. */
3240
+ /** Get the default locale as it is now: the client's, or the one `withLocale()` fixed. */
2801
3241
  getDefaultLocale() {
2802
- return this.defaultLocale;
3242
+ return this.clientLocale();
2803
3243
  }
2804
- /** Create a new AssetClient for this type with a different default locale. */
3244
+ /**
3245
+ * Create a new AssetClient for this type with a different default locale, fixed: an update of the
3246
+ * client's locale (`updateConfig({ locale })`) does not reach it.
3247
+ */
2805
3248
  withLocale(locale) {
2806
3249
  return new _AssetClient(this.httpClient, this.assetType, locale);
2807
3250
  }
@@ -2817,12 +3260,18 @@ __name(isAssetEntry, "isAssetEntry");
2817
3260
 
2818
3261
  // src/content/singleton-client.ts
2819
3262
  var _SingletonClient = class _SingletonClient {
2820
- constructor(httpClient, singletonKey, defaultLocale) {
3263
+ /**
3264
+ * @param locale - The client's locale: the function `TypedRelyIOClient` hands over, asked on
3265
+ * every call, or a locale fixed for this client
3266
+ */
3267
+ constructor(httpClient, singletonKey, locale) {
2821
3268
  this.httpClient = httpClient;
2822
3269
  this.singletonKey = singletonKey;
2823
- this.defaultLocale = defaultLocale;
2824
3270
  __publicField(this, "includeFields");
2825
3271
  __publicField(this, "localeValue");
3272
+ /** The client's locale, asked on every call rather than copied when this client was taken. */
3273
+ __publicField(this, "clientLocale");
3274
+ this.clientLocale = readClientLocale(locale);
2826
3275
  }
2827
3276
  /**
2828
3277
  * Include reference fields in the response
@@ -2862,9 +3311,10 @@ var _SingletonClient = class _SingletonClient {
2862
3311
  async fetchWithTag(options) {
2863
3312
  const query = {};
2864
3313
  const includeValue = options?.include ?? this.includeFields;
2865
- const localeValueFinal = options?.locale ?? this.localeValue ?? this.defaultLocale;
3314
+ const localeValueFinal = options?.locale ?? this.localeValue ?? this.clientLocale();
2866
3315
  if (includeValue) query.include = includeValue.join(",");
2867
3316
  if (localeValueFinal != null && localeValueFinal !== "") query.locale = localeValueFinal;
3317
+ if (options?.fresh !== void 0) query[FRESH_PARAMETER] = options.fresh;
2868
3318
  const response = await this.httpClient.send(
2869
3319
  "/v1/singletons/{key}",
2870
3320
  "get",
@@ -2876,11 +3326,12 @@ var _SingletonClient = class _SingletonClient {
2876
3326
  * Update singleton content. The API auto-publishes on PATCH — there is no
2877
3327
  * separate publish route; calling `update()` writes and publishes.
2878
3328
  * @param data - The content fields to update
2879
- * @param locale - Optional locale for localized content (overrides fluent locale)
3329
+ * @param locale - Optional locale for localized content (overrides the fluent locale, which
3330
+ * overrides the client's own)
2880
3331
  * @param options - `If-Match` / `Idempotency-Key`, both declared on this route
2881
3332
  */
2882
3333
  async update(data, locale, options) {
2883
- const finalLocale = locale ?? this.localeValue;
3334
+ const finalLocale = this.localeOf(locale);
2884
3335
  return this.httpClient.fetchPayload(
2885
3336
  "/v1/singletons/{key}",
2886
3337
  "patch",
@@ -2909,15 +3360,17 @@ var _SingletonClient = class _SingletonClient {
2909
3360
  * Get version history for one locale of the singleton.
2910
3361
  *
2911
3362
  * Versions are numbered per locale, so this is one locale's timeline: `locale` defaults to the
2912
- * schema default, no fallback is applied, and a locale with no history returns an empty array
2913
- * rather than another locale's rows. Every row names the locale it belongs to.
3363
+ * fluent `.locale()`, then the client's own (`withLocale()`), then the schema default; no
3364
+ * fallback is applied, and a locale with no history returns an empty array rather than another
3365
+ * locale's rows. Every row names the locale it belongs to.
2914
3366
  */
2915
3367
  async versions(options) {
3368
+ const locale = this.localeOf(options?.locale);
2916
3369
  const payload = await this.httpClient.fetchPayload("/v1/singletons/{key}/versions", "get", {
2917
3370
  path: { key: this.singletonKey },
2918
3371
  query: {
2919
3372
  limit: options?.limit ?? 10,
2920
- ...options?.locale === void 0 ? {} : { locale: options.locale }
3373
+ ...locale === void 0 ? {} : { locale }
2921
3374
  }
2922
3375
  });
2923
3376
  return payload.versions;
@@ -2925,20 +3378,25 @@ var _SingletonClient = class _SingletonClient {
2925
3378
  /**
2926
3379
  * Get a specific version of the singleton.
2927
3380
  *
2928
- * `locale` defaults to the schema default. Version numbers are per locale, so version 2 is
2929
- * different content in each one.
3381
+ * `locale` defaults to the fluent `.locale()`, then the client's own (`withLocale()`), then the
3382
+ * schema default. Version numbers are per locale, so version 2 is different content in each one.
2930
3383
  */
2931
3384
  async version(versionNumber, options) {
3385
+ const locale = this.localeOf(options?.locale);
2932
3386
  try {
2933
3387
  return await this.httpClient.fetchPayload("/v1/singletons/{key}/versions/{versionNumber}", "get", {
2934
3388
  path: { key: this.singletonKey, versionNumber: String(versionNumber) },
2935
- query: options?.locale === void 0 ? void 0 : { locale: options.locale }
3389
+ query: locale === void 0 ? void 0 : { locale }
2936
3390
  });
2937
3391
  } catch (error) {
2938
3392
  if (isResourceNotFound(error)) return null;
2939
3393
  throw error;
2940
3394
  }
2941
3395
  }
3396
+ /** The locale a write or history read sends: the call's, the fluent one, else the client's. */
3397
+ localeOf(named) {
3398
+ return callLocale(named ?? this.localeValue, this.clientLocale());
3399
+ }
2942
3400
  /**
2943
3401
  * Get the singleton key
2944
3402
  */
@@ -2959,12 +3417,18 @@ var SingletonClient = _SingletonClient;
2959
3417
 
2960
3418
  // src/content/settings-client.ts
2961
3419
  var _SettingsClient = class _SettingsClient {
2962
- constructor(httpClient, groupId, defaultLocale) {
3420
+ /**
3421
+ * @param locale - The client's locale: the function `TypedRelyIOClient` hands over, asked on
3422
+ * every call, or a locale fixed for this client
3423
+ */
3424
+ constructor(httpClient, groupId, locale) {
2963
3425
  this.httpClient = httpClient;
2964
3426
  this.groupId = groupId;
2965
- this.defaultLocale = defaultLocale;
2966
3427
  __publicField(this, "includeFields");
2967
3428
  __publicField(this, "localeValue");
3429
+ /** The client's locale, asked on every call rather than copied when this client was taken. */
3430
+ __publicField(this, "clientLocale");
3431
+ this.clientLocale = readClientLocale(locale);
2968
3432
  }
2969
3433
  /**
2970
3434
  * Include reference fields in the response
@@ -2996,12 +3460,20 @@ var _SettingsClient = class _SettingsClient {
2996
3460
  }
2997
3461
  /**
2998
3462
  * Update settings group
3463
+ *
3464
+ * The fields travel as `content` beside an optional `locale`, the body the route validates
3465
+ * (`UpdateSettingsDto`); any other top-level key is refused `REQUEST_INVALID`.
3466
+ *
2999
3467
  * @param settings - The settings fields to update
3000
- * @param locale - Optional locale for localized settings (overrides fluent locale)
3468
+ * @param locale - Optional locale for localized settings (overrides the fluent locale, which
3469
+ * overrides the client's own)
3001
3470
  */
3002
3471
  async update(settings, locale, options) {
3003
- const finalLocale = locale ?? this.localeValue;
3004
- const body = finalLocale != null && finalLocale !== "" ? { ...settings, locale: finalLocale } : settings;
3472
+ const finalLocale = this.localeOf(locale);
3473
+ const body = {
3474
+ content: settings,
3475
+ ...finalLocale != null && finalLocale !== "" ? { locale: finalLocale } : {}
3476
+ };
3005
3477
  return this.httpClient.fetchPayload(
3006
3478
  "/v1/settings/{groupId}",
3007
3479
  "patch",
@@ -3016,7 +3488,7 @@ var _SettingsClient = class _SettingsClient {
3016
3488
  async fetchWithTag(options) {
3017
3489
  const query = {};
3018
3490
  const includeValue = options?.include ?? this.includeFields;
3019
- const localeValueFinal = options?.locale ?? this.localeValue ?? this.defaultLocale;
3491
+ const localeValueFinal = options?.locale ?? this.localeValue ?? this.clientLocale();
3020
3492
  if (includeValue) query.include = includeValue.join(",");
3021
3493
  if (localeValueFinal != null && localeValueFinal !== "") query.locale = localeValueFinal;
3022
3494
  const response = await this.httpClient.send(
@@ -3028,16 +3500,20 @@ var _SettingsClient = class _SettingsClient {
3028
3500
  }
3029
3501
  /**
3030
3502
  * Reset settings to default values
3031
- * @param locale - Optional locale for localized settings
3503
+ *
3504
+ * The route reads its locale from `?locale=` and takes no body, so that is where it is sent.
3505
+ *
3506
+ * @param locale - Optional locale for localized settings (overrides the fluent locale, which
3507
+ * overrides the client's own)
3032
3508
  */
3033
3509
  async reset(locale, options) {
3034
- const finalLocale = locale ?? this.localeValue;
3510
+ const finalLocale = this.localeOf(locale);
3035
3511
  return this.httpClient.fetchPayload(
3036
3512
  "/v1/settings/{groupId}/reset",
3037
3513
  "post",
3038
3514
  {
3039
3515
  path: { groupId: this.groupId },
3040
- body: finalLocale != null && finalLocale !== "" ? { locale: finalLocale } : void 0,
3516
+ query: finalLocale != null && finalLocale !== "" ? { locale: finalLocale } : void 0,
3041
3517
  headers: writeHeaders(options)
3042
3518
  }
3043
3519
  );
@@ -3068,15 +3544,17 @@ var _SettingsClient = class _SettingsClient {
3068
3544
  * Get version history for one locale of the settings group.
3069
3545
  *
3070
3546
  * Versions are numbered per locale, so this is one locale's timeline: `locale` defaults to the
3071
- * schema default, no fallback is applied, and a locale with no history returns an empty array
3072
- * rather than another locale's rows. Every row names the locale it belongs to.
3547
+ * fluent `.locale()`, then the client's own (`withLocale()`), then the schema default; no
3548
+ * fallback is applied, and a locale with no history returns an empty array rather than another
3549
+ * locale's rows. Every row names the locale it belongs to.
3073
3550
  */
3074
3551
  async versions(options) {
3552
+ const locale = this.localeOf(options?.locale);
3075
3553
  const payload = await this.httpClient.fetchPayload("/v1/settings/{groupId}/versions", "get", {
3076
3554
  path: { groupId: this.groupId },
3077
3555
  query: {
3078
3556
  limit: options?.limit ?? 10,
3079
- ...options?.locale === void 0 ? {} : { locale: options.locale }
3557
+ ...locale === void 0 ? {} : { locale }
3080
3558
  }
3081
3559
  });
3082
3560
  return payload.versions;
@@ -3084,20 +3562,25 @@ var _SettingsClient = class _SettingsClient {
3084
3562
  /**
3085
3563
  * Get a specific version of the settings.
3086
3564
  *
3087
- * `locale` defaults to the schema default. Version numbers are per locale, so version 2 is
3088
- * different content in each one.
3565
+ * `locale` defaults to the fluent `.locale()`, then the client's own (`withLocale()`), then the
3566
+ * schema default. Version numbers are per locale, so version 2 is different content in each one.
3089
3567
  */
3090
3568
  async version(versionNumber, options) {
3569
+ const locale = this.localeOf(options?.locale);
3091
3570
  try {
3092
3571
  return await this.httpClient.fetchPayload("/v1/settings/{groupId}/versions/{versionNumber}", "get", {
3093
3572
  path: { groupId: this.groupId, versionNumber: String(versionNumber) },
3094
- query: options?.locale === void 0 ? void 0 : { locale: options.locale }
3573
+ query: locale === void 0 ? void 0 : { locale }
3095
3574
  });
3096
3575
  } catch (error) {
3097
3576
  if (isResourceNotFound(error)) return null;
3098
3577
  throw error;
3099
3578
  }
3100
3579
  }
3580
+ /** The locale a write or history read sends: the call's, the fluent one, else the client's. */
3581
+ localeOf(named) {
3582
+ return callLocale(named ?? this.localeValue, this.clientLocale());
3583
+ }
3101
3584
  /**
3102
3585
  * Get the settings group ID
3103
3586
  */
@@ -3119,15 +3602,19 @@ var SettingsClient = _SettingsClient;
3119
3602
  // src/search/search-client.ts
3120
3603
  var _SearchBuilder = class _SearchBuilder {
3121
3604
  /**
3122
- * @param defaultLocale - The client's locale (`withLocale()`), sent when `.locale()` names none
3605
+ * @param locale - The client's locale (`withLocale()`, `updateConfig({ locale })`), sent when
3606
+ * `.locale()` names none: the function `TypedRelyIOClient` hands over, or a locale fixed for it
3123
3607
  */
3124
- constructor(httpClient, query, defaultLocale) {
3608
+ constructor(httpClient, query, locale) {
3125
3609
  this.httpClient = httpClient;
3126
- this.defaultLocale = defaultLocale;
3127
3610
  __publicField(this, "query");
3128
3611
  __publicField(this, "searchOptions", {});
3129
3612
  __publicField(this, "requestHeaders");
3613
+ __publicField(this, "freshValue");
3614
+ /** The client's locale, asked when the query is sent rather than copied when it was built. */
3615
+ __publicField(this, "clientLocale");
3130
3616
  this.query = query;
3617
+ this.clientLocale = readClientLocale(locale);
3131
3618
  }
3132
3619
  /**
3133
3620
  * Headers this search adds to the client's own, e.g. `X-RelyIO-Dataset` to search another
@@ -3137,6 +3624,15 @@ var _SearchBuilder = class _SearchBuilder {
3137
3624
  this.requestHeaders = headers;
3138
3625
  return this;
3139
3626
  }
3627
+ /**
3628
+ * The feed position this search follows — a change's `fresh` (`FeedChange`), or a resync's. Sent
3629
+ * as `?_fresh` on the query string beside the body: the edge keys a search on both, so no answer
3630
+ * it holds from before that position can answer it. The answer is the search's own.
3631
+ */
3632
+ fresh(position) {
3633
+ this.freshValue = position;
3634
+ return this;
3635
+ }
3140
3636
  /**
3141
3637
  * Search within a single content type. Where a schema types the client, the name is one of its
3142
3638
  * content types and the results are that type's entries.
@@ -3158,10 +3654,12 @@ var _SearchBuilder = class _SearchBuilder {
3158
3654
  return this;
3159
3655
  }
3160
3656
  /**
3161
- * Add filter conditions
3657
+ * Add filter conditions, on the fields the content types declare. A condition on `_metadata` is
3658
+ * refused here, before anything is sent.
3162
3659
  * @param conditions - Filter conditions
3163
3660
  */
3164
3661
  where(conditions) {
3662
+ refuseMetadataConditions(conditions);
3165
3663
  this.searchOptions.where = {
3166
3664
  ...this.searchOptions.where,
3167
3665
  ...conditions
@@ -3269,14 +3767,18 @@ var _SearchBuilder = class _SearchBuilder {
3269
3767
  if (this.searchOptions.explain === true) {
3270
3768
  body.explain = true;
3271
3769
  }
3272
- const locale = this.searchOptions.locale ?? this.defaultLocale;
3770
+ const locale = this.searchOptions.locale ?? this.clientLocale();
3273
3771
  if (locale != null && locale !== "") {
3274
3772
  body.locale = locale;
3275
3773
  }
3276
3774
  const response = await this.httpClient.send(
3277
3775
  "/v1/search",
3278
3776
  "post",
3279
- { body, headers: this.requestHeaders }
3777
+ {
3778
+ body,
3779
+ headers: this.requestHeaders,
3780
+ ...this.freshValue === void 0 ? {} : { query: { [FRESH_PARAMETER]: this.freshValue } }
3781
+ }
3280
3782
  );
3281
3783
  return response.body;
3282
3784
  }
@@ -3297,14 +3799,17 @@ __name(_SearchBuilder, "SearchBuilder");
3297
3799
  var SearchBuilder = _SearchBuilder;
3298
3800
  var _AutocompleteBuilder = class _AutocompleteBuilder {
3299
3801
  /**
3300
- * @param defaultLocale - The client's locale (`withLocale()`), sent when `.locale()` names none
3802
+ * @param locale - The client's locale (`withLocale()`, `updateConfig({ locale })`), sent when
3803
+ * `.locale()` names none: the function `TypedRelyIOClient` hands over, or a locale fixed for it
3301
3804
  */
3302
- constructor(httpClient, query, defaultLocale) {
3805
+ constructor(httpClient, query, locale) {
3303
3806
  this.httpClient = httpClient;
3304
- this.defaultLocale = defaultLocale;
3305
3807
  __publicField(this, "query");
3306
3808
  __publicField(this, "autocompleteOptions", {});
3809
+ /** The client's locale, asked when the query is sent rather than copied when it was built. */
3810
+ __publicField(this, "clientLocale");
3307
3811
  this.query = query;
3812
+ this.clientLocale = readClientLocale(locale);
3308
3813
  }
3309
3814
  /**
3310
3815
  * Autocomplete within a content type
@@ -3330,10 +3835,12 @@ var _AutocompleteBuilder = class _AutocompleteBuilder {
3330
3835
  return this;
3331
3836
  }
3332
3837
  /**
3333
- * Add filter conditions
3838
+ * Add filter conditions, on the fields the content types declare. A condition on `_metadata` is
3839
+ * refused here, before anything is sent.
3334
3840
  * @param conditions - Filter conditions
3335
3841
  */
3336
3842
  where(conditions) {
3843
+ refuseMetadataConditions(conditions);
3337
3844
  this.autocompleteOptions.where = {
3338
3845
  ...this.autocompleteOptions.where,
3339
3846
  ...conditions
@@ -3380,7 +3887,7 @@ var _AutocompleteBuilder = class _AutocompleteBuilder {
3380
3887
  if (this.autocompleteOptions.limit !== void 0) {
3381
3888
  body.limit = this.autocompleteOptions.limit;
3382
3889
  }
3383
- const locale = this.autocompleteOptions.locale ?? this.defaultLocale;
3890
+ const locale = this.autocompleteOptions.locale ?? this.clientLocale();
3384
3891
  if (locale != null && locale !== "") {
3385
3892
  body.locale = locale;
3386
3893
  }
@@ -3398,15 +3905,18 @@ __name(_AutocompleteBuilder, "AutocompleteBuilder");
3398
3905
  var AutocompleteBuilder = _AutocompleteBuilder;
3399
3906
  var _SimilarBuilder = class _SimilarBuilder {
3400
3907
  /**
3401
- * @param defaultLocale - The client's locale (`withLocale()`), sent when `.locale()` names none
3908
+ * @param locale - The client's locale (`withLocale()`, `updateConfig({ locale })`), sent when
3909
+ * `.locale()` names none: the function `TypedRelyIOClient` hands over, or a locale fixed for it
3402
3910
  */
3403
- constructor(httpClient, sourceId, defaultLocale) {
3911
+ constructor(httpClient, sourceId, locale) {
3404
3912
  this.httpClient = httpClient;
3405
- this.defaultLocale = defaultLocale;
3406
3913
  __publicField(this, "sourceId");
3407
3914
  __publicField(this, "similarOptions", {});
3408
3915
  __publicField(this, "requestHeaders");
3916
+ /** The client's locale, asked when the read is sent rather than copied when it was built. */
3917
+ __publicField(this, "clientLocale");
3409
3918
  this.sourceId = sourceId;
3919
+ this.clientLocale = readClientLocale(locale);
3410
3920
  }
3411
3921
  /**
3412
3922
  * Headers this read adds to the client's own, e.g. `X-RelyIO-Dataset` to read another dataset
@@ -3434,17 +3944,12 @@ var _SimilarBuilder = class _SimilarBuilder {
3434
3944
  this.similarOptions.contentType = void 0;
3435
3945
  return this;
3436
3946
  }
3437
- /**
3438
- * Add filter conditions
3439
- * @param conditions - Filter conditions
3947
+ /*
3948
+ * No `where()` (REL-1452). Similar ranks from the source entry's stored vector, and the vector
3949
+ * index can pre-filter by content type and locale only — never by an entry field. The API used to
3950
+ * accept `where` here and apply nothing; it now refuses the property, so the builder does not
3951
+ * offer a filter that cannot be honoured. Narrow with `.in()`, `.across()` and `.locale()`.
3440
3952
  */
3441
- where(conditions) {
3442
- this.similarOptions.where = {
3443
- ...this.similarOptions.where,
3444
- ...conditions
3445
- };
3446
- return this;
3447
- }
3448
3953
  /**
3449
3954
  * Limit number of results
3450
3955
  * @param count - Maximum results
@@ -3478,13 +3983,10 @@ var _SimilarBuilder = class _SimilarBuilder {
3478
3983
  if (contentTypes != null) {
3479
3984
  body.contentTypes = contentTypes;
3480
3985
  }
3481
- if (this.similarOptions.where && Object.keys(this.similarOptions.where).length > 0) {
3482
- body.where = this.similarOptions.where;
3483
- }
3484
3986
  if (this.similarOptions.limit !== void 0) {
3485
3987
  body.limit = this.similarOptions.limit;
3486
3988
  }
3487
- const locale = this.similarOptions.locale ?? this.defaultLocale;
3989
+ const locale = this.similarOptions.locale ?? this.clientLocale();
3488
3990
  if (locale != null && locale !== "") {
3489
3991
  body.locale = locale;
3490
3992
  }
@@ -3512,41 +4014,36 @@ __name(_SimilarBuilder, "SimilarBuilder");
3512
4014
  var SimilarBuilder = _SimilarBuilder;
3513
4015
  var _SearchClient = class _SearchClient {
3514
4016
  /**
3515
- * @param defaultLocale - The client's locale (`withLocale()`): every builder here sends it when
3516
- * the query names none, as `content()`'s reads do
4017
+ * @param locale - The client's locale (`withLocale()`, `updateConfig({ locale })`): every builder
4018
+ * here sends it when the query names none, as `content()`'s reads do — the function
4019
+ * `TypedRelyIOClient` hands over, or a locale fixed for this client
3517
4020
  */
3518
- constructor(httpClient, defaultLocale) {
4021
+ constructor(httpClient, locale) {
3519
4022
  this.httpClient = httpClient;
3520
- this.defaultLocale = defaultLocale;
4023
+ /** The client's locale, handed to every builder here to ask when it sends. */
4024
+ __publicField(this, "clientLocale");
4025
+ this.clientLocale = readClientLocale(locale);
3521
4026
  }
3522
4027
  /**
3523
4028
  * Start a search query
3524
4029
  * @param query - Search query string
3525
4030
  */
3526
4031
  search(query) {
3527
- return new SearchBuilder(this.httpClient, query, this.defaultLocale);
4032
+ return new SearchBuilder(this.httpClient, query, this.clientLocale);
3528
4033
  }
3529
4034
  /**
3530
4035
  * Start an autocomplete query
3531
4036
  * @param query - Partial query to autocomplete
3532
4037
  */
3533
4038
  autocomplete(query) {
3534
- return new AutocompleteBuilder(this.httpClient, query, this.defaultLocale);
4039
+ return new AutocompleteBuilder(this.httpClient, query, this.clientLocale);
3535
4040
  }
3536
4041
  /**
3537
4042
  * Find content similar to a given entry
3538
4043
  * @param sourceId - Entry ID to find similar content for
3539
4044
  */
3540
4045
  similar(sourceId) {
3541
- return new SimilarBuilder(this.httpClient, sourceId, this.defaultLocale);
3542
- }
3543
- /**
3544
- * Quick search helper - searches all content types
3545
- * @param query - Search query
3546
- * @param limit - Max results (default 20)
3547
- */
3548
- async global(query, limit = 20) {
3549
- return this.search(query).across("*").limit(limit).execute();
4046
+ return new SimilarBuilder(this.httpClient, sourceId, this.clientLocale);
3550
4047
  }
3551
4048
  };
3552
4049
  __name(_SearchClient, "SearchClient");
@@ -3554,7 +4051,12 @@ var SearchClient = _SearchClient;
3554
4051
 
3555
4052
  // src/batch/batch-client.ts
3556
4053
  var _BatchBuilder = class _BatchBuilder {
3557
- constructor(httpClient) {
4054
+ /**
4055
+ * @param locale - The client's locale (`withLocale()`, `updateConfig({ locale })`), each
4056
+ * operation's when it names none: the function `TypedRelyIOClient` hands over, or a locale fixed
4057
+ * for this batch
4058
+ */
4059
+ constructor(httpClient, locale) {
3558
4060
  this.httpClient = httpClient;
3559
4061
  __publicField(this, "operations", []);
3560
4062
  __publicField(this, "defaultOptions", {
@@ -3564,6 +4066,17 @@ var _BatchBuilder = class _BatchBuilder {
3564
4066
  continueOnError: true
3565
4067
  });
3566
4068
  __publicField(this, "requestHeaders");
4069
+ /** The client's locale, asked as each operation is added rather than copied when this was built. */
4070
+ __publicField(this, "clientLocale");
4071
+ this.clientLocale = readClientLocale(locale);
4072
+ }
4073
+ /**
4074
+ * An operation's body with its locale: the one it names, else the client's as it is when the
4075
+ * operation is added.
4076
+ */
4077
+ localized(data) {
4078
+ const locale = callLocale(data.locale, this.clientLocale());
4079
+ return locale === void 0 ? data : { ...data, locale };
3567
4080
  }
3568
4081
  // ==========================================================================
3569
4082
  // ENTRY OPERATIONS
@@ -3579,7 +4092,7 @@ var _BatchBuilder = class _BatchBuilder {
3579
4092
  id,
3580
4093
  method: "POST",
3581
4094
  path: `/${contentType}`,
3582
- body: data
4095
+ body: this.localized(data)
3583
4096
  });
3584
4097
  return this;
3585
4098
  }
@@ -3599,7 +4112,7 @@ var _BatchBuilder = class _BatchBuilder {
3599
4112
  id,
3600
4113
  method: "PATCH",
3601
4114
  path: `/${contentType}/${entryId}`,
3602
- body: data
4115
+ body: this.localized(data)
3603
4116
  });
3604
4117
  return this;
3605
4118
  }
@@ -3619,7 +4132,7 @@ var _BatchBuilder = class _BatchBuilder {
3619
4132
  id,
3620
4133
  method: "PUT",
3621
4134
  path: `/${contentType}/${entryId}`,
3622
- body: data
4135
+ body: this.localized(data)
3623
4136
  });
3624
4137
  return this;
3625
4138
  }
@@ -3651,7 +4164,7 @@ var _BatchBuilder = class _BatchBuilder {
3651
4164
  id,
3652
4165
  method: "PATCH",
3653
4166
  path: `/_singletons/${singletonKey}`,
3654
- body: data
4167
+ body: this.localized(data)
3655
4168
  });
3656
4169
  return this;
3657
4170
  }
@@ -3666,7 +4179,7 @@ var _BatchBuilder = class _BatchBuilder {
3666
4179
  id,
3667
4180
  method: "POST",
3668
4181
  path: `/_singletons/${singletonKey}`,
3669
- body: data
4182
+ body: this.localized(data)
3670
4183
  });
3671
4184
  return this;
3672
4185
  }
@@ -3674,7 +4187,8 @@ var _BatchBuilder = class _BatchBuilder {
3674
4187
  // RAW OPERATIONS
3675
4188
  // ==========================================================================
3676
4189
  /**
3677
- * Add a raw operation (for advanced use cases)
4190
+ * Add a raw operation (for advanced use cases). It is sent exactly as written: the client's
4191
+ * locale is not added to it.
3678
4192
  * @param operation - The operation to add
3679
4193
  */
3680
4194
  raw(operation) {
@@ -3800,7 +4314,7 @@ var _BatchBuilder = class _BatchBuilder {
3800
4314
  * Clone the builder
3801
4315
  */
3802
4316
  clone() {
3803
- const cloned = new _BatchBuilder(this.httpClient);
4317
+ const cloned = new _BatchBuilder(this.httpClient, this.clientLocale);
3804
4318
  cloned.operations = [...this.operations];
3805
4319
  cloned.defaultOptions = { ...this.defaultOptions };
3806
4320
  cloned.requestHeaders = this.requestHeaders === void 0 ? void 0 : { ...this.requestHeaders };
@@ -3853,14 +4367,21 @@ var _BatchBuilder = class _BatchBuilder {
3853
4367
  __name(_BatchBuilder, "BatchBuilder");
3854
4368
  var BatchBuilder = _BatchBuilder;
3855
4369
  var _BatchClient = class _BatchClient {
3856
- constructor(httpClient) {
4370
+ /**
4371
+ * @param locale - The client's locale: the function `TypedRelyIOClient` hands over, or a locale
4372
+ * fixed for this client
4373
+ */
4374
+ constructor(httpClient, locale) {
3857
4375
  this.httpClient = httpClient;
4376
+ /** The client's locale, handed to every batch this builds to ask as operations are added. */
4377
+ __publicField(this, "clientLocale");
4378
+ this.clientLocale = readClientLocale(locale);
3858
4379
  }
3859
4380
  /**
3860
- * Create a new batch builder
4381
+ * Create a new batch builder, whose operations default to this client's locale
3861
4382
  */
3862
4383
  create() {
3863
- return new BatchBuilder(this.httpClient);
4384
+ return new BatchBuilder(this.httpClient, this.clientLocale);
3864
4385
  }
3865
4386
  /**
3866
4387
  * Quick bulk create entries
@@ -3911,15 +4432,18 @@ function withClientLocale(parameters, clientLocale) {
3911
4432
  __name(withClientLocale, "withClientLocale");
3912
4433
  var _AdhocQueryBuilder = class _AdhocQueryBuilder {
3913
4434
  /**
3914
- * @param defaultLocale - The client's locale (`withLocale()`), sent as the `locale` parameter
3915
- * when `.param('locale', …)` binds none
4435
+ * @param locale - The client's locale (`withLocale()`, `updateConfig({ locale })`), sent as the
4436
+ * `locale` parameter when `.param('locale', …)` binds none: the function `TypedRelyIOClient`
4437
+ * hands over, or a locale fixed for this builder
3916
4438
  */
3917
- constructor(httpClient, defaultLocale) {
4439
+ constructor(httpClient, locale) {
3918
4440
  this.httpClient = httpClient;
3919
- this.defaultLocale = defaultLocale;
3920
4441
  __publicField(this, "queryDef", {});
3921
4442
  __publicField(this, "parameters", {});
3922
4443
  __publicField(this, "options", {});
4444
+ /** The client's locale, asked when the query is sent rather than copied when it was built. */
4445
+ __publicField(this, "clientLocale");
4446
+ this.clientLocale = readClientLocale(locale);
3923
4447
  }
3924
4448
  // ==========================================================================
3925
4449
  // QUERY BLOCKS
@@ -3964,14 +4488,6 @@ var _AdhocQueryBuilder = class _AdhocQueryBuilder {
3964
4488
  this.queryDef.search = config;
3965
4489
  return this;
3966
4490
  }
3967
- /**
3968
- * Add similar block
3969
- * @param config - Similar configuration
3970
- */
3971
- similar(config) {
3972
- this.queryDef.similar = config;
3973
- return this;
3974
- }
3975
4491
  // ==========================================================================
3976
4492
  // FETCH MODIFIERS
3977
4493
  // ==========================================================================
@@ -4148,15 +4664,15 @@ var _AdhocQueryBuilder = class _AdhocQueryBuilder {
4148
4664
  * Execute the query
4149
4665
  */
4150
4666
  async execute() {
4151
- if (!this.queryDef.fetch && !this.queryDef.aggregate && !this.queryDef.search && !this.queryDef.similar) {
4667
+ if (!this.queryDef.fetch && !this.queryDef.aggregate && !this.queryDef.search) {
4152
4668
  throw new Error(
4153
- "Query must have at least one of: fetch, aggregate, search, or similar. Use .from(), .aggregate(), .search(), or .similar()"
4669
+ "Query must have at least one of: fetch, aggregate or search. Use .from(), .aggregate() or .search()"
4154
4670
  );
4155
4671
  }
4156
4672
  const body = {
4157
4673
  query: this.queryDef
4158
4674
  };
4159
- const parameters = withClientLocale(this.parameters, this.defaultLocale);
4675
+ const parameters = withClientLocale(this.parameters, this.clientLocale());
4160
4676
  if (parameters !== void 0 && Object.keys(parameters).length > 0) {
4161
4677
  body.parameters = parameters;
4162
4678
  }
@@ -4189,7 +4705,7 @@ var _AdhocQueryBuilder = class _AdhocQueryBuilder {
4189
4705
  * Clone the builder
4190
4706
  */
4191
4707
  clone() {
4192
- const cloned = new _AdhocQueryBuilder(this.httpClient, this.defaultLocale);
4708
+ const cloned = new _AdhocQueryBuilder(this.httpClient, this.clientLocale);
4193
4709
  cloned.queryDef = JSON.parse(JSON.stringify(this.queryDef));
4194
4710
  cloned.parameters = { ...this.parameters };
4195
4711
  cloned.options = { ...this.options };
@@ -4200,18 +4716,21 @@ __name(_AdhocQueryBuilder, "AdhocQueryBuilder");
4200
4716
  var AdhocQueryBuilder = _AdhocQueryBuilder;
4201
4717
  var _AdhocQueryClient = class _AdhocQueryClient {
4202
4718
  /**
4203
- * @param defaultLocale - The client's locale (`withLocale()`), sent as the `locale` parameter of
4204
- * every query that binds none
4719
+ * @param locale - The client's locale (`withLocale()`, `updateConfig({ locale })`), sent as the
4720
+ * `locale` parameter of every query that binds none: the function `TypedRelyIOClient` hands
4721
+ * over, or a locale fixed for this client
4205
4722
  */
4206
- constructor(httpClient, defaultLocale) {
4723
+ constructor(httpClient, locale) {
4207
4724
  this.httpClient = httpClient;
4208
- this.defaultLocale = defaultLocale;
4725
+ /** The client's locale, asked on every query rather than copied when this client was taken. */
4726
+ __publicField(this, "clientLocale");
4727
+ this.clientLocale = readClientLocale(locale);
4209
4728
  }
4210
4729
  /**
4211
4730
  * Create a new query builder
4212
4731
  */
4213
4732
  build() {
4214
- return new AdhocQueryBuilder(this.httpClient, this.defaultLocale);
4733
+ return new AdhocQueryBuilder(this.httpClient, this.clientLocale);
4215
4734
  }
4216
4735
  /**
4217
4736
  * Execute a query definition directly
@@ -4226,7 +4745,7 @@ var _AdhocQueryClient = class _AdhocQueryClient {
4226
4745
  const body = {
4227
4746
  query: definition
4228
4747
  };
4229
- const sent = withClientLocale(parameters, this.defaultLocale);
4748
+ const sent = withClientLocale(parameters, this.clientLocale());
4230
4749
  if (sent !== void 0 && Object.keys(sent).length > 0) {
4231
4750
  body.parameters = sent;
4232
4751
  }
@@ -4275,23 +4794,67 @@ var AdhocQueryClient = _AdhocQueryClient;
4275
4794
 
4276
4795
  // src/changes/changes.ts
4277
4796
  var CHANGES_POLL_TIMEOUT_MS = 25e3 + 5e3;
4797
+ var EXPIRED_REASONS = {
4798
+ retention: "retention",
4799
+ "dataset-replaced": "dataset-replaced",
4800
+ "ahead-of-head": "ahead-of-head",
4801
+ "foreign-cursor": "foreign-cursor",
4802
+ "too-far-behind": "too-far-behind"
4803
+ };
4278
4804
  var _ChangesExpiredError = class _ChangesExpiredError extends ResourceExpiredError {
4279
- /** The base class's signature, so `withContext` can rebuild one. */
4280
- constructor(message, statusCode = 410, cause, context) {
4805
+ /** The base class's signature, so `withContext` can rebuild one; then the position resynced to. */
4806
+ constructor(message, statusCode = 410, cause, context, fresh) {
4281
4807
  super(message, statusCode, cause, context);
4282
4808
  __publicField(this, "reason");
4809
+ /**
4810
+ * The position the feed resynced to, which a refetch of everything names as `?_fresh`, as a
4811
+ * change's {@link FeedChange.fresh} is named: the head a `/v1/changes` resync took, or the cursor
4812
+ * the edge's `feed.resync` sent. `undefined` when there was no resync to take one — the error was
4813
+ * thrown (`onExpired: 'throw'`) — and on an error a caller built itself.
4814
+ */
4815
+ __publicField(this, "fresh");
4283
4816
  this.name = "ChangesExpiredError";
4284
4817
  this.reason = expiredReasonOf(message);
4818
+ this.fresh = fresh;
4819
+ }
4820
+ /** The base class rebuilds from its four arguments, which would drop the position resynced to. */
4821
+ withContext(context) {
4822
+ const Constructor = this.constructor;
4823
+ return new Constructor(
4824
+ this.message,
4825
+ this.statusCode,
4826
+ this.cause,
4827
+ { ...this.context, ...context },
4828
+ this.fresh
4829
+ );
4830
+ }
4831
+ /** `/v1/changes`' refusal, with the head the feed resynced to when it did. */
4832
+ static of(expired, fresh) {
4833
+ return new _ChangesExpiredError(
4834
+ expired.message,
4835
+ expired.statusCode,
4836
+ expired,
4837
+ expired.context,
4838
+ fresh
4839
+ );
4285
4840
  }
4286
- static of(expired) {
4287
- return new _ChangesExpiredError(expired.message, expired.statusCode, expired, expired.context);
4841
+ /** The live edge's `feed.resync`: refetch everything, then continue from the cursor it sent. */
4842
+ static resync(reason, cursor) {
4843
+ return new _ChangesExpiredError(
4844
+ `The live edge cannot list the changes since this cursor (${reason}). Refetch everything derived from the feed; it continues from the position it sent.`,
4845
+ 410,
4846
+ void 0,
4847
+ { transport: "live-edge" },
4848
+ cursor
4849
+ );
4288
4850
  }
4289
4851
  };
4290
4852
  __name(_ChangesExpiredError, "ChangesExpiredError");
4291
4853
  var ChangesExpiredError = _ChangesExpiredError;
4292
4854
  function expiredReasonOf(message) {
4293
- if (message.includes("(retention)")) return "retention";
4294
- if (message.includes("(dataset-replaced)")) return "dataset-replaced";
4855
+ for (const reason of Object.values(EXPIRED_REASONS)) {
4856
+ if (message.includes(`(${reason})`)) return reason;
4857
+ }
4295
4858
  return "unknown";
4296
4859
  }
4297
4860
  __name(expiredReasonOf, "expiredReasonOf");
@@ -4311,7 +4874,7 @@ async function* pollChanges(http, options = {}) {
4311
4874
  const onExpired = options.onExpired ?? "throw";
4312
4875
  if (onExpired === "throw") throw expired;
4313
4876
  since = void 0;
4314
- resynced = typeof onExpired === "function" ? expired : void 0;
4877
+ resynced = typeof onExpired === "function" ? error : void 0;
4315
4878
  continue;
4316
4879
  }
4317
4880
  const { data, page } = response.body;
@@ -4320,13 +4883,13 @@ async function* pollChanges(http, options = {}) {
4320
4883
  }
4321
4884
  since = page.nextCursor;
4322
4885
  if (resynced !== void 0 && typeof options.onExpired === "function") {
4323
- const expired = resynced;
4886
+ const expired = ChangesExpiredError.of(resynced, page.nextCursor);
4324
4887
  resynced = void 0;
4325
4888
  await options.onExpired(expired);
4326
4889
  if (isAborted(signal)) return;
4327
4890
  }
4328
4891
  for (const change of data) {
4329
- yield change;
4892
+ yield { ...change, fresh: change.cursor };
4330
4893
  if (isAborted(signal)) return;
4331
4894
  }
4332
4895
  if (data.length === 0) {
@@ -4365,6 +4928,477 @@ async function pollOnce(http, since, options) {
4365
4928
  }
4366
4929
  __name(pollOnce, "pollOnce");
4367
4930
 
4931
+ // src/config/key-kind.ts
4932
+ var PUBLISHABLE_KEY_PREFIX = "rly_pub_";
4933
+ var isPublishableKey = /* @__PURE__ */ __name((apiKey) => apiKey?.startsWith(PUBLISHABLE_KEY_PREFIX) === true, "isPublishableKey");
4934
+
4935
+ // src/errors/live-errors.ts
4936
+ var _LiveEdgeError = class _LiveEdgeError extends SDKError {
4937
+ constructor(message, end, retry, options = {}) {
4938
+ super(message, options.statusCode, options.cause, options.context);
4939
+ __publicField(this, "code", "LIVE_EDGE_ENDED");
4940
+ __publicField(this, "retryable");
4941
+ __publicField(this, "end");
4942
+ __publicField(this, "retry");
4943
+ /** A refused poll's `Retry-After`, in milliseconds, where it carried a readable one. */
4944
+ __publicField(this, "retryAfterMs");
4945
+ this.end = end;
4946
+ this.retry = retry;
4947
+ this.retryable = retry !== "stop";
4948
+ this.retryAfterMs = options.retryAfterMs;
4949
+ }
4950
+ /**
4951
+ * The base class rebuilds an error through its own four-argument constructor, which this class
4952
+ * does not have; rebuilt that way, `end` would be a string and `retry` a number.
4953
+ */
4954
+ withContext(context) {
4955
+ const rebuilt = new _LiveEdgeError(this.message, this.end, this.retry, {
4956
+ ...this.statusCode === void 0 ? {} : { statusCode: this.statusCode },
4957
+ ...this.cause === void 0 ? {} : { cause: this.cause },
4958
+ ...this.retryAfterMs === void 0 ? {} : { retryAfterMs: this.retryAfterMs },
4959
+ context: { ...this.context, ...context }
4960
+ });
4961
+ return rebuilt;
4962
+ }
4963
+ getUserMessage() {
4964
+ return this.message;
4965
+ }
4966
+ };
4967
+ __name(_LiveEdgeError, "LiveEdgeError");
4968
+ var LiveEdgeError = _LiveEdgeError;
4969
+
4970
+ // src/config/live-url.ts
4971
+ var LIVE_EDGE_OF_API = {
4972
+ "https://api.relyio.com": "https://live.relyio.com"
4973
+ };
4974
+ var HTTP_SCHEME_OF = {
4975
+ "http:": "http:",
4976
+ "https:": "https:",
4977
+ "ws:": "http:",
4978
+ "wss:": "https:"
4979
+ };
4980
+ function liveEdgeAddress(config) {
4981
+ if (config.liveUrl !== void 0) return readLiveUrl(config.liveUrl);
4982
+ const derived = originOf(config.baseUrl);
4983
+ const edge = derived === void 0 ? void 0 : LIVE_EDGE_OF_API[derived];
4984
+ if (edge !== void 0) return { ok: true, url: new URL(edge) };
4985
+ return {
4986
+ ok: false,
4987
+ message: `Live updates need the address of RelyIO\u2019s live edge, and this client cannot derive it: \`baseUrl\` is not RelyIO\u2019s API (${Object.keys(LIVE_EDGE_OF_API).join(", ")}). Set \`liveUrl\` in createClient() to the live edge this API belongs with. Nothing was sent.`
4988
+ };
4989
+ }
4990
+ __name(liveEdgeAddress, "liveEdgeAddress");
4991
+ function readLiveUrl(liveUrl) {
4992
+ let url;
4993
+ try {
4994
+ url = new URL(liveUrl);
4995
+ } catch {
4996
+ return { ok: false, message: `\`liveUrl\` is not a URL: "${liveUrl}". Nothing was sent.` };
4997
+ }
4998
+ const scheme = HTTP_SCHEME_OF[url.protocol];
4999
+ if (scheme === void 0) {
5000
+ return {
5001
+ ok: false,
5002
+ message: `\`liveUrl\` must be an https:, wss:, http: or ws: URL, not ${url.protocol}. Nothing was sent.`
5003
+ };
5004
+ }
5005
+ url.protocol = scheme;
5006
+ url.search = "";
5007
+ url.hash = "";
5008
+ return { ok: true, url };
5009
+ }
5010
+ __name(readLiveUrl, "readLiveUrl");
5011
+ function originOf(baseUrl) {
5012
+ try {
5013
+ return new URL(baseUrl).origin;
5014
+ } catch {
5015
+ return void 0;
5016
+ }
5017
+ }
5018
+ __name(originOf, "originOf");
5019
+
5020
+ // src/changes/live-edge.ts
5021
+ var LIVE_KEEPALIVE_INTERVAL_MS = 3e4;
5022
+ var LIVE_POLLS_BEFORE_SOCKET = 4;
5023
+ async function* subscribeLiveEdge(client, options = {}) {
5024
+ const { signal } = options;
5025
+ if (isAborted(signal)) return;
5026
+ const key = client.apiKey;
5027
+ if (key === void 0 || !isPublishableKey(key)) {
5028
+ throw new LiveEdgeError(NEEDS_PUBLISHABLE_KEY, { kind: "needs-publishable-key" }, "stop");
5029
+ }
5030
+ const address = liveEdgeAddress(client);
5031
+ if (!address.ok) throw new LiveEdgeError(address.message, { kind: "needs-live-url" }, "stop");
5032
+ const request = {
5033
+ base: address.url,
5034
+ key,
5035
+ dataset: client.dataset,
5036
+ types: options.types,
5037
+ ids: options.ids
5038
+ };
5039
+ const reach = { position: options.since };
5040
+ for (; ; ) {
5041
+ const socket = openSocket(client, edgeUrl(request, reach.position, "socket"));
5042
+ if (socket !== void 0) {
5043
+ const ended = yield* followSocket(socket, reach, options);
5044
+ if (ended === "aborted") return;
5045
+ }
5046
+ const polled = yield* followPolls(client, request, reach, options);
5047
+ if (polled === "aborted") return;
5048
+ }
5049
+ }
5050
+ __name(subscribeLiveEdge, "subscribeLiveEdge");
5051
+ var SOCKET_CONNECTING = 0;
5052
+ var SOCKET_OPEN = 1;
5053
+ function runtimeWebSocket() {
5054
+ const candidate = globalThis.WebSocket;
5055
+ return typeof candidate === "function" ? candidate : void 0;
5056
+ }
5057
+ __name(runtimeWebSocket, "runtimeWebSocket");
5058
+ function openSocket(client, url) {
5059
+ const Socket = client.webSocket ?? runtimeWebSocket();
5060
+ if (Socket === void 0) return void 0;
5061
+ try {
5062
+ return new Socket(url);
5063
+ } catch {
5064
+ return void 0;
5065
+ }
5066
+ }
5067
+ __name(openSocket, "openSocket");
5068
+ async function* followSocket(socket, reach, options) {
5069
+ const { signal } = options;
5070
+ const inbox = socketInbox(socket, signal);
5071
+ let open = false;
5072
+ try {
5073
+ for (; ; ) {
5074
+ const event = await inbox.next();
5075
+ if (event === void 0) return "aborted";
5076
+ switch (event.kind) {
5077
+ case "open": {
5078
+ open = true;
5079
+ inbox.keepAlive();
5080
+ continue;
5081
+ }
5082
+ case "error": {
5083
+ if (!open) return "unopened";
5084
+ continue;
5085
+ }
5086
+ case "close": {
5087
+ if (!open) return "unopened";
5088
+ throw closedBy(event.code, event.reason);
5089
+ }
5090
+ case "silent": {
5091
+ throw new LiveEdgeError(
5092
+ "The live edge\u2019s socket stopped answering its keepalive, so the connection is presumed dead; reconnecting from the last cursor.",
5093
+ { kind: "lost" },
5094
+ "retry"
5095
+ );
5096
+ }
5097
+ case "message": {
5098
+ if (event.data === VISITOR_KEEPALIVE.response) continue;
5099
+ const frame = typeof event.data === "string" ? parseVisitorFrame(event.data) : null;
5100
+ if (frame === null)
5101
+ throw unreadable(`a frame the wire does not declare: ${sample(event.data)}`);
5102
+ switch (frame.type) {
5103
+ case VISITOR_FRAMES.opened: {
5104
+ reach.position = frame.cursor;
5105
+ options.opened?.();
5106
+ options.reached?.(frame.cursor);
5107
+ continue;
5108
+ }
5109
+ case VISITOR_FRAMES.changes: {
5110
+ const prior = reach.position;
5111
+ if (prior === void 0) throw unreadable("changes before the socket had a position");
5112
+ for (const change of changesOf(frame.changes, frame.cursor, prior)) {
5113
+ yield change;
5114
+ if (isAborted(signal)) return "aborted";
5115
+ }
5116
+ reach.position = frame.cursor;
5117
+ if (frame.changes.length === 0) options.reached?.(frame.cursor);
5118
+ continue;
5119
+ }
5120
+ case VISITOR_FRAMES.resync: {
5121
+ await expire(frame.reason, frame.cursor, options);
5122
+ if (isAborted(signal)) return "aborted";
5123
+ reach.position = frame.cursor;
5124
+ options.reached?.(frame.cursor);
5125
+ continue;
5126
+ }
5127
+ }
5128
+ }
5129
+ }
5130
+ }
5131
+ } finally {
5132
+ inbox.dispose();
5133
+ }
5134
+ }
5135
+ __name(followSocket, "followSocket");
5136
+ function socketInbox(socket, signal) {
5137
+ const events = [];
5138
+ let wake;
5139
+ let disposed = false;
5140
+ let heard = true;
5141
+ let keepalive;
5142
+ const rouse = /* @__PURE__ */ __name(() => {
5143
+ const waking = wake;
5144
+ wake = void 0;
5145
+ waking?.();
5146
+ }, "rouse");
5147
+ const push = /* @__PURE__ */ __name((event) => {
5148
+ if (disposed) return;
5149
+ events.push(event);
5150
+ rouse();
5151
+ }, "push");
5152
+ socket.addEventListener("open", () => push({ kind: "open" }));
5153
+ socket.addEventListener("message", (event) => {
5154
+ heard = true;
5155
+ push({ kind: "message", data: isRecord(event) ? event["data"] : void 0 });
5156
+ });
5157
+ socket.addEventListener("close", (event) => {
5158
+ const code = isRecord(event) ? event["code"] : void 0;
5159
+ const reason = isRecord(event) ? event["reason"] : void 0;
5160
+ push({
5161
+ kind: "close",
5162
+ // A close event always carries both; a socket that somehow raised one without them closed
5163
+ // abnormally, which is what 1006 says.
5164
+ code: typeof code === "number" ? code : 1006,
5165
+ reason: typeof reason === "string" ? reason : ""
5166
+ });
5167
+ });
5168
+ socket.addEventListener("error", () => push({ kind: "error" }));
5169
+ signal?.addEventListener("abort", rouse, { once: true });
5170
+ return {
5171
+ async next() {
5172
+ for (; ; ) {
5173
+ if (isAborted(signal)) return void 0;
5174
+ const event = events.shift();
5175
+ if (event !== void 0) return event;
5176
+ await new Promise((resolve) => {
5177
+ wake = resolve;
5178
+ });
5179
+ }
5180
+ },
5181
+ keepAlive() {
5182
+ keepalive = setInterval(() => {
5183
+ if (!heard) {
5184
+ push({ kind: "silent" });
5185
+ return;
5186
+ }
5187
+ heard = false;
5188
+ if (socket.readyState === SOCKET_OPEN) socket.send(VISITOR_KEEPALIVE.request);
5189
+ }, LIVE_KEEPALIVE_INTERVAL_MS);
5190
+ },
5191
+ dispose() {
5192
+ disposed = true;
5193
+ if (keepalive !== void 0) clearInterval(keepalive);
5194
+ signal?.removeEventListener("abort", rouse);
5195
+ if (socket.readyState === SOCKET_CONNECTING || socket.readyState === SOCKET_OPEN) {
5196
+ socket.close(1e3);
5197
+ }
5198
+ }
5199
+ };
5200
+ }
5201
+ __name(socketInbox, "socketInbox");
5202
+ function closedBy(code, reason) {
5203
+ const retry = closeRetryOf(code);
5204
+ const meaning = hasOwn(CLOSE_MEANING, code) ? CLOSE_MEANING[code] : code < 4e3 ? "the connection dropped" : "a refusal this version of the SDK does not know";
5205
+ const said = `close ${code}${reason === "" ? "" : ` ${reason}`}`;
5206
+ return new LiveEdgeError(
5207
+ `The live edge ended this subscription (${said}): ${meaning}. ${AFTER_A_CLOSE[retry]}`,
5208
+ { kind: "closed", code, reason },
5209
+ retry
5210
+ );
5211
+ }
5212
+ __name(closedBy, "closedBy");
5213
+ var AFTER_A_CLOSE = {
5214
+ stop: "The same request would be refused again, so it was not retried.",
5215
+ retry: "Reconnecting from the last cursor after a short back-off.",
5216
+ "back-off": "Retrying after a back-off of at least a minute, without polling, which the same admission refuses."
5217
+ };
5218
+ function closeRetryOf(code) {
5219
+ if (hasOwn(VISITOR_CLOSE_RETRY, code)) return VISITOR_CLOSE_RETRY[code];
5220
+ return code < 4e3 ? "retry" : "stop";
5221
+ }
5222
+ __name(closeRetryOf, "closeRetryOf");
5223
+ var CLOSE_MEANING = {
5224
+ [VISITOR_CLOSE_CODES.requestInvalid]: "the edge could not read the request \u2014 a malformed cursor or filter",
5225
+ [VISITOR_CLOSE_CODES.keyRefused]: "the key is not a publishable key the edge accepts \u2014 unknown, revoked or expired",
5226
+ [VISITOR_CLOSE_CODES.originRefused]: "this page\u2019s origin is not on the publishable key\u2019s allowed origins",
5227
+ [VISITOR_CLOSE_CODES.datasetRefused]: "`dataset` names no dataset the key reaches",
5228
+ [VISITOR_CLOSE_CODES.unavailable]: "the edge could not reach what it needed to decide just now",
5229
+ [VISITOR_CLOSE_CODES.allowanceReached]: "the project holds as many live connections as its plan allows",
5230
+ [VISITOR_CLOSE_CODES.addressLimitReached]: "this address holds as many live connections to the project as one address may",
5231
+ [VISITOR_CLOSE_CODES.ceilingReached]: "the project is at its safety ceiling of live connections"
5232
+ };
5233
+ async function* followPolls(client, request, reach, options) {
5234
+ const { signal } = options;
5235
+ for (let poll = 0; poll < LIVE_POLLS_BEFORE_SOCKET; poll += 1) {
5236
+ if (isAborted(signal)) return "aborted";
5237
+ const answer = await pollOnce2(client, request, reach.position, signal);
5238
+ if (answer === void 0) return "aborted";
5239
+ options.opened?.();
5240
+ if (answer.kind === "resync") {
5241
+ await expire(answer.reason, answer.cursor, options);
5242
+ if (isAborted(signal)) return "aborted";
5243
+ reach.position = answer.cursor;
5244
+ options.reached?.(answer.cursor);
5245
+ } else {
5246
+ const prior = reach.position;
5247
+ if (answer.changes.length > 0 && prior === void 0) {
5248
+ throw unreadable("a poll without a cursor answered changes");
5249
+ }
5250
+ for (const change of changesOf(answer.changes, answer.cursor, prior ?? answer.cursor)) {
5251
+ yield change;
5252
+ if (isAborted(signal)) return "aborted";
5253
+ }
5254
+ reach.position = answer.cursor;
5255
+ if (answer.changes.length === 0) options.reached?.(answer.cursor);
5256
+ }
5257
+ await sleep(VISITOR_POLL_INTERVAL_MS, signal);
5258
+ }
5259
+ return isAborted(signal) ? "aborted" : "socket";
5260
+ }
5261
+ __name(followPolls, "followPolls");
5262
+ async function pollOnce2(client, request, since, signal) {
5263
+ if (isAborted(signal)) return void 0;
5264
+ const url = edgeUrl(request, since, "poll", client.clientId);
5265
+ const context = { url: edgeUrl({ ...request, key: void 0 }, since, "poll", client.clientId) };
5266
+ const controller = new AbortController();
5267
+ let timedOut = false;
5268
+ const timer = setTimeout(() => {
5269
+ timedOut = true;
5270
+ controller.abort();
5271
+ }, client.timeout);
5272
+ const onAbort = /* @__PURE__ */ __name(() => controller.abort(), "onAbort");
5273
+ signal?.addEventListener("abort", onAbort, { once: true });
5274
+ try {
5275
+ let response;
5276
+ let text;
5277
+ try {
5278
+ const send = client.fetch ?? globalThis.fetch;
5279
+ response = await send(url, { method: "GET", signal: controller.signal });
5280
+ text = await response.text();
5281
+ } catch (error) {
5282
+ if (isAborted(signal)) return void 0;
5283
+ if (timedOut) {
5284
+ throw new TimeoutError(
5285
+ `The live edge did not answer a poll within ${client.timeout}ms`,
5286
+ client.timeout,
5287
+ void 0,
5288
+ void 0,
5289
+ context
5290
+ );
5291
+ }
5292
+ throw ErrorMapper.mapGenericError(
5293
+ error instanceof Error ? error : new Error(String(error)),
5294
+ context
5295
+ );
5296
+ }
5297
+ const body = parseJson(text);
5298
+ if (!response.ok) throw refusedPoll(response, body, context);
5299
+ const answer = readVisitorPollAnswer(isRecord(body) ? body["data"] : void 0);
5300
+ if (answer === null)
5301
+ throw unreadable(`a poll answer the wire does not declare: ${sample(text)}`);
5302
+ return answer;
5303
+ } finally {
5304
+ clearTimeout(timer);
5305
+ signal?.removeEventListener("abort", onAbort);
5306
+ }
5307
+ }
5308
+ __name(pollOnce2, "pollOnce");
5309
+ function refusedPoll(response, body, context) {
5310
+ const headers = {};
5311
+ response.headers.forEach((value, name) => {
5312
+ headers[name] = value;
5313
+ });
5314
+ const mapped = ErrorMapper.mapHttpError(
5315
+ { status: response.status, statusText: response.statusText, body, headers },
5316
+ { ...context, method: "GET" }
5317
+ );
5318
+ const refusal = readError(body);
5319
+ if (refusal === void 0 || !hasOwn(VISITOR_POLL_RETRY, refusal.code)) return mapped;
5320
+ const code = refusal.code;
5321
+ const retryAfterMs = parseRetryAfterMs(headerValue(headers, "Retry-After"), Date.now());
5322
+ return new LiveEdgeError(
5323
+ `The live edge refused a poll (${code}): ${refusal.messages[0] ?? mapped.message}`,
5324
+ { kind: "refused", code },
5325
+ VISITOR_POLL_RETRY[code],
5326
+ {
5327
+ statusCode: response.status,
5328
+ cause: mapped,
5329
+ ...retryAfterMs === void 0 ? {} : { retryAfterMs },
5330
+ context
5331
+ }
5332
+ );
5333
+ }
5334
+ __name(refusedPoll, "refusedPoll");
5335
+ function edgeUrl(request, since, transport, clientId) {
5336
+ const url = new URL(request.base.href);
5337
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}${VISITOR_LIVE_PATH}`;
5338
+ if (transport === "socket") url.protocol = url.protocol === "http:" ? "ws:" : "wss:";
5339
+ const query = new URLSearchParams();
5340
+ if (request.key !== void 0) query.set(VISITOR_QUERY.key, request.key);
5341
+ if (request.dataset !== void 0 && request.dataset !== "") {
5342
+ query.set(VISITOR_QUERY.dataset, request.dataset);
5343
+ }
5344
+ if (since !== void 0) query.set(VISITOR_QUERY.since, since);
5345
+ if (request.types !== void 0) query.set(VISITOR_QUERY.types, request.types.join(","));
5346
+ if (request.ids !== void 0) query.set(VISITOR_QUERY.ids, request.ids.join(","));
5347
+ if (transport === "poll" && clientId !== void 0) query.set(VISITOR_QUERY.client, clientId);
5348
+ url.search = query.toString();
5349
+ return url.href;
5350
+ }
5351
+ __name(edgeUrl, "edgeUrl");
5352
+ function changesOf(changes, cursor, prior) {
5353
+ const last = changes.length - 1;
5354
+ return changes.map((change, index) => ({
5355
+ entryId: change.entryId,
5356
+ contentType: change.contentType,
5357
+ locale: change.locale,
5358
+ kind: change.kind,
5359
+ cursor: index === last ? cursor : prior,
5360
+ fresh: cursor
5361
+ }));
5362
+ }
5363
+ __name(changesOf, "changesOf");
5364
+ async function expire(reason, cursor, options) {
5365
+ const expired = ChangesExpiredError.resync(reason, cursor);
5366
+ const onExpired = options.onExpired ?? "throw";
5367
+ if (onExpired === "throw") throw expired;
5368
+ if (typeof onExpired === "function") await onExpired(expired);
5369
+ }
5370
+ __name(expire, "expire");
5371
+ function unreadable(what) {
5372
+ return new LiveEdgeError(
5373
+ `The live edge sent ${what}. This version of the SDK cannot follow it, so it stopped.`,
5374
+ { kind: "unreadable", what },
5375
+ "stop"
5376
+ );
5377
+ }
5378
+ __name(unreadable, "unreadable");
5379
+ var NEEDS_PUBLISHABLE_KEY = `Live updates at RelyIO\u2019s edge need a publishable key (\`${PUBLISHABLE_KEY_PREFIX}\u2026\`), and this client holds none. A secret or preview key is never sent to the edge; on a server, follow \`changes()\` with it instead. Nothing was sent.`;
5380
+ function sample(value) {
5381
+ const text = typeof value === "string" ? value : `(${typeof value})`;
5382
+ return text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
5383
+ }
5384
+ __name(sample, "sample");
5385
+ function parseJson(text) {
5386
+ try {
5387
+ return JSON.parse(text);
5388
+ } catch {
5389
+ return void 0;
5390
+ }
5391
+ }
5392
+ __name(parseJson, "parseJson");
5393
+ function isRecord(value) {
5394
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5395
+ }
5396
+ __name(isRecord, "isRecord");
5397
+ function hasOwn(object, key) {
5398
+ return Object.prototype.hasOwnProperty.call(object, key);
5399
+ }
5400
+ __name(hasOwn, "hasOwn");
5401
+
4368
5402
  // src/config/dev-detection.ts
4369
5403
  function sandboxStateOf(resolution) {
4370
5404
  return resolution.state === "active" ? resolution.sandbox : resolution;
@@ -4475,7 +5509,7 @@ var DEFAULT_CONFIG = {
4475
5509
  environment: "production",
4476
5510
  headers: {},
4477
5511
  browser: {
4478
- includeCredentials: true,
5512
+ includeCredentials: false,
4479
5513
  useRelativeUrls: false
4480
5514
  },
4481
5515
  cache: {
@@ -4507,45 +5541,48 @@ function validateConfig(config) {
4507
5541
  if (config.baseUrl != null && typeof config.baseUrl !== "string") {
4508
5542
  throw new Error("Base URL must be a string");
4509
5543
  }
4510
- if (config.timeout != null && (typeof config.timeout !== "number" || config.timeout < 0)) {
4511
- throw new Error("Timeout must be a positive number");
5544
+ if (config.liveUrl != null && typeof config.liveUrl !== "string") {
5545
+ throw new Error("liveUrl must be a string");
5546
+ }
5547
+ if (config.webSocket != null && typeof config.webSocket !== "function") {
5548
+ throw new Error("webSocket must be a WebSocket class");
4512
5549
  }
4513
- if (config.retryAttempts != null && (typeof config.retryAttempts !== "number" || config.retryAttempts < 0)) {
4514
- throw new Error("Retry attempts must be a positive number");
5550
+ if (config.timeout != null) assertRequestTimeout(config.timeout);
5551
+ if (config.retryAttempts != null && !(Number.isInteger(config.retryAttempts) && config.retryAttempts >= 1)) {
5552
+ throw new Error(
5553
+ `retryAttempts must be a whole number of at least 1 \u2014 the most attempts one request makes, the first included, so 1 turns retrying off \u2014 and got ${String(config.retryAttempts)}`
5554
+ );
4515
5555
  }
4516
5556
  }
4517
5557
  __name(validateConfig, "validateConfig");
4518
5558
  var devOverrideNotified = false;
4519
5559
  function resolveConfig(userConfig) {
4520
5560
  validateConfig(userConfig);
4521
- const merged = {
4522
- ...DEFAULT_CONFIG,
4523
- ...userConfig,
4524
- headers: {
4525
- ...DEFAULT_CONFIG.headers,
4526
- ...userConfig.headers
4527
- },
4528
- browser: {
4529
- ...DEFAULT_CONFIG.browser,
4530
- ...userConfig.browser
4531
- },
4532
- cache: {
4533
- ...DEFAULT_CONFIG.cache,
4534
- ...userConfig.cache
4535
- },
4536
- realtime: {
4537
- ...DEFAULT_CONFIG.realtime,
4538
- ...userConfig.realtime
4539
- },
4540
- debug: {
4541
- ...DEFAULT_CONFIG.debug,
4542
- ...userConfig.debug
4543
- }
4544
- };
4545
- const dev = resolveDevOverride(userConfig.dev);
5561
+ return withHandshake(withDefaults(DEFAULT_CONFIG, userConfig, DEFAULT_CONFIG));
5562
+ }
5563
+ __name(resolveConfig, "resolveConfig");
5564
+ function resolveConfigUpdate(configured, update) {
5565
+ validateConfig(update);
5566
+ const base = update.apiKey === void 0 ? configured : withoutKeyHeader(configured);
5567
+ return withHandshake(withDefaults(base, update, DEFAULT_CONFIG));
5568
+ }
5569
+ __name(resolveConfigUpdate, "resolveConfigUpdate");
5570
+ function withConfiguredKey(configured, apiKey) {
5571
+ return { ...withoutKeyHeader(configured), apiKey };
5572
+ }
5573
+ __name(withConfiguredKey, "withConfiguredKey");
5574
+ function withoutKeyHeader(configured) {
5575
+ return { ...configured, headers: layerHeaders(configured.headers, { [API_KEY_HEADER]: null }) };
5576
+ }
5577
+ __name(withoutKeyHeader, "withoutKeyHeader");
5578
+ function withHandshake(configured) {
5579
+ const dev = resolveDevOverride(configured.dev);
5580
+ const merged = withDefaults(
5581
+ configured,
5582
+ dev.state === "active" ? { apiKey: dev.apiKey, dataset: dev.sandbox.datasetId } : {},
5583
+ DEFAULT_CONFIG
5584
+ );
4546
5585
  if (dev.state === "active") {
4547
- merged.apiKey = dev.apiKey;
4548
- merged.dataset = dev.sandbox.datasetId;
4549
5586
  if (!devOverrideNotified) {
4550
5587
  devOverrideNotified = true;
4551
5588
  console.info(
@@ -4563,30 +5600,69 @@ function resolveConfig(userConfig) {
4563
5600
  `[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.`
4564
5601
  );
4565
5602
  }
4566
- return { config: merged, sandbox: sandboxStateOf(dev) };
5603
+ return { config: merged, sandbox: sandboxStateOf(dev), configured };
4567
5604
  }
4568
- __name(resolveConfig, "resolveConfig");
5605
+ __name(withHandshake, "withHandshake");
5606
+ function withDefaults(base, given, defaults) {
5607
+ const merged = new Map(Object.entries(base));
5608
+ const givenValues = new Map(Object.entries(given ?? {}));
5609
+ const defaultValues = new Map(Object.entries(defaults));
5610
+ for (const key of /* @__PURE__ */ new Set([...merged.keys(), ...givenValues.keys()])) {
5611
+ const fallback = merged.get(key);
5612
+ const value = givenValues.get(key);
5613
+ const givenBag = isOptionBag(value) ? value : void 0;
5614
+ if (isOptionBag(fallback) && key === "headers") {
5615
+ merged.set(key, layerHeaders(fallback, givenBag));
5616
+ } else if (isOptionBag(fallback)) {
5617
+ const defaultBag = defaultValues.get(key);
5618
+ merged.set(key, withDefaults(fallback, givenBag, isOptionBag(defaultBag) ? defaultBag : {}));
5619
+ } else if (value === null) {
5620
+ if (defaultValues.has(key)) merged.set(key, defaultValues.get(key));
5621
+ else merged.delete(key);
5622
+ } else if (value !== void 0) {
5623
+ merged.set(key, value);
5624
+ }
5625
+ }
5626
+ return Object.fromEntries(merged);
5627
+ }
5628
+ __name(withDefaults, "withDefaults");
5629
+ function isOptionBag(value) {
5630
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5631
+ }
5632
+ __name(isOptionBag, "isOptionBag");
4569
5633
 
4570
5634
  // src/client/create-client.ts
4571
5635
  var API_VERSION_SEGMENT = "v1";
4572
5636
  var _TypedRelyIOClient = class _TypedRelyIOClient {
4573
5637
  constructor(config) {
5638
+ /**
5639
+ * The one HTTP client every sub-client sends through. `updateConfig()` and `setApiKey()` change it
5640
+ * in place rather than replacing it, so a sub-client taken before either sends through the change.
5641
+ */
4574
5642
  __publicField(this, "httpClient");
4575
5643
  __publicField(this, "config");
5644
+ /**
5645
+ * The configuration as `createClient()`, `setApiKey()` and `updateConfig()` gave it, before the
5646
+ * `relyio dev` handshake: what the next update is laid over, and what `withLocale()` starts from.
5647
+ */
5648
+ __publicField(this, "configured");
4576
5649
  __publicField(this, "_sandbox");
4577
- __publicField(this, "_search");
4578
- __publicField(this, "_batch");
4579
- __publicField(this, "_adhocQuery");
4580
5650
  __publicField(this, "_schema");
5651
+ /**
5652
+ * The client's locale as it is when a call is made. Every sub-client and builder is handed this
5653
+ * rather than a copy of the locale, so one taken before `updateConfig({ locale })` sends the new
5654
+ * locale, or none after `locale: null` (REL-1453).
5655
+ */
5656
+ __publicField(this, "currentLocale", /* @__PURE__ */ __name(() => this.config.locale, "currentLocale"));
5657
+ /** The live edge's poll client id: chosen once, when first needed, and kept for this client's life. */
5658
+ __publicField(this, "_liveClientId");
4581
5659
  const { schema, ...clientConfig } = config;
4582
5660
  this._schema = schema;
4583
5661
  const resolved = resolveConfig(clientConfig);
4584
5662
  this.config = resolved.config;
5663
+ this.configured = resolved.configured;
4585
5664
  this._sandbox = resolved.sandbox;
4586
5665
  this.httpClient = new HttpClient(this.config);
4587
- this._search = new SearchClient(this.httpClient, this.config.locale);
4588
- this._batch = new BatchClient(this.httpClient);
4589
- this._adhocQuery = new AdhocQueryClient(this.httpClient, this.config.locale);
4590
5666
  }
4591
5667
  // ============================================================================
4592
5668
  // CORE CLIENTS
@@ -4615,10 +5691,10 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4615
5691
  return this._sandbox;
4616
5692
  }
4617
5693
  /**
4618
- * Get the ad-hoc query client
5694
+ * Get the ad-hoc query client. Every query it sends is in the client's locale as it is then.
4619
5695
  */
4620
5696
  get query() {
4621
- return this._adhocQuery;
5697
+ return new AdhocQueryClient(this.httpClient, this.currentLocale);
4622
5698
  }
4623
5699
  // ============================================================================
4624
5700
  // CONTENT OPERATIONS
@@ -4638,7 +5714,7 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4638
5714
  * ```
4639
5715
  */
4640
5716
  content(contentType) {
4641
- return new ContentClient(this.httpClient, contentType, this.config.locale);
5717
+ return new ContentClient(this.httpClient, contentType, this.currentLocale);
4642
5718
  }
4643
5719
  /**
4644
5720
  * Get an asset client for a specific asset type.
@@ -4659,7 +5735,7 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4659
5735
  * ```
4660
5736
  */
4661
5737
  asset(assetType) {
4662
- return new AssetClient(this.httpClient, assetType, this.config.locale);
5738
+ return new AssetClient(this.httpClient, assetType, this.currentLocale);
4663
5739
  }
4664
5740
  /**
4665
5741
  * Get a singleton client for a specific singleton key
@@ -4674,7 +5750,7 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4674
5750
  * ```
4675
5751
  */
4676
5752
  singleton(key) {
4677
- return new SingletonClient(this.httpClient, key, this.config.locale);
5753
+ return new SingletonClient(this.httpClient, key, this.currentLocale);
4678
5754
  }
4679
5755
  /**
4680
5756
  * Get a settings client for a specific settings group
@@ -4688,7 +5764,7 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4688
5764
  * ```
4689
5765
  */
4690
5766
  settings(groupId) {
4691
- return new SettingsClient(this.httpClient, groupId, this.config.locale);
5767
+ return new SettingsClient(this.httpClient, groupId, this.currentLocale);
4692
5768
  }
4693
5769
  // ============================================================================
4694
5770
  // SEARCH OPERATIONS
@@ -4710,7 +5786,7 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4710
5786
  * ```
4711
5787
  */
4712
5788
  search(query) {
4713
- return this._search.search(query);
5789
+ return this.searchClient().search(query);
4714
5790
  }
4715
5791
  /**
4716
5792
  * Start an autocomplete query for search suggestions
@@ -4726,7 +5802,7 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4726
5802
  * ```
4727
5803
  */
4728
5804
  autocomplete(query) {
4729
- return this._search.autocomplete(query);
5805
+ return this.searchClient().autocomplete(query);
4730
5806
  }
4731
5807
  /**
4732
5808
  * Find content similar to a given entry using vector search
@@ -4741,13 +5817,17 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4741
5817
  * ```
4742
5818
  */
4743
5819
  similar(sourceId) {
4744
- return this._search.similar(sourceId);
5820
+ return this.searchClient().similar(sourceId);
4745
5821
  }
4746
5822
  // ============================================================================
4747
5823
  // BATCH OPERATIONS
4748
5824
  // ============================================================================
4749
5825
  /**
4750
- * Start a batch operation for bulk create/update/delete
5826
+ * Start a batch operation for bulk create/update/delete.
5827
+ *
5828
+ * By default each operation lands or fails on its own. `atomic: true` makes the batch one
5829
+ * transaction, which the API accepts only when every operation is the same kind of write and none
5830
+ * writes a singleton that does not exist yet.
4751
5831
  *
4752
5832
  * @example
4753
5833
  * ```typescript
@@ -4756,13 +5836,23 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4756
5836
  * .update('product', 'entry_abc', { content: { price: 899 } })
4757
5837
  * .delete('product', 'entry_xyz')
4758
5838
  * .upsertSingleton('homepage', { content: { hero: 'Welcome!' } })
4759
- * .execute({ atomic: true })
5839
+ * .execute()
4760
5840
  *
4761
5841
  * console.log(result.summary.succeeded)
5842
+ *
5843
+ * // All or nothing: one kind of write
5844
+ * await relyio.batch()
5845
+ * .update('product', 'entry_abc', { content: { price: 899 } })
5846
+ * .update('product', 'entry_def', { content: { price: 799 } })
5847
+ * .execute({ atomic: true })
4762
5848
  * ```
4763
5849
  */
4764
5850
  batch() {
4765
- return this._batch.create();
5851
+ return new BatchClient(this.httpClient, this.currentLocale).create();
5852
+ }
5853
+ /** The search client, whose builders send the client's locale as it is when each one is sent. */
5854
+ searchClient() {
5855
+ return new SearchClient(this.httpClient, this.currentLocale);
4766
5856
  }
4767
5857
  // ============================================================================
4768
5858
  // CHANGES FEED
@@ -4774,13 +5864,15 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4774
5864
  * Lazy — nothing is requested until the first `next()`. It runs until
4775
5865
  * `signal` aborts, which ends the `for await` cleanly rather than throwing.
4776
5866
  * A change names what changed; refetch it through the ordinary read path
4777
- * (`INV-SS-CHANGE-IS-IDS`).
5867
+ * (`INV-SS-CHANGE-IS-IDS`), naming its `fresh` so no cache answers with the
5868
+ * body from before it (`FeedChange`).
4778
5869
  *
4779
5870
  * @example
4780
5871
  * ```typescript
4781
5872
  * const controller = new AbortController()
4782
5873
  * for await (const change of relyio.changes({ since: cursor, types: ['post'], signal: controller.signal })) {
4783
- * // { entryId, contentType, locale, kind, cursor }
5874
+ * // { entryId, contentType, locale, kind, cursor, fresh }
5875
+ * await relyio.content(change.contentType).findById(change.entryId).fresh(change.fresh).execute()
4784
5876
  * }
4785
5877
  * ```
4786
5878
  *
@@ -4788,13 +5880,60 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4788
5880
  * ```typescript
4789
5881
  * for await (const change of relyio.changes({
4790
5882
  * since: cursor,
4791
- * onExpired: async () => invalidateEverything(),
5883
+ * onExpired: async expired => refetchEverything(expired.fresh),
4792
5884
  * })) { … }
4793
5885
  * ```
4794
5886
  */
4795
5887
  changes(options = {}) {
4796
5888
  return pollChanges(this.httpClient, options);
4797
5889
  }
5890
+ /**
5891
+ * Follow the dataset's **published** changes at RelyIO's live edge, with a publishable key
5892
+ * (`rly_pub_…`): each change as ids, in order — the transport a browser follows changes with.
5893
+ *
5894
+ * A WebSocket to the edge, from `since` or the dataset's current position; where a socket cannot
5895
+ * open — the runtime has none, or the network refuses it — it polls the edge instead, about every
5896
+ * fifteen seconds, from the same cursor. The edge is `liveUrl`, or derived from the default
5897
+ * `baseUrl` (see `RelyIOConfig.liveUrl`).
5898
+ *
5899
+ * One subscription ends only when `signal` aborts, which ends the `for await` cleanly; every other
5900
+ * end — a socket a deploy dropped, a refusal, a refused poll — is thrown as a `LiveEdgeError`
5901
+ * that says whether to stop, retry or back off. Hand it to `followChanges()`, the one loop that
5902
+ * reconnects from the last cursor with jittered back-off and honours those answers:
5903
+ *
5904
+ * @example
5905
+ * ```typescript
5906
+ * const relyio = createClient({ apiKey: 'rly_pub_…' })
5907
+ * const controller = new AbortController()
5908
+ * await followChanges(from => relyio.liveChanges({ ...from, types: ['post'] }), {
5909
+ * signal: controller.signal,
5910
+ * // `fresh` names a position no cache holds a body for from before the change.
5911
+ * onChange: change =>
5912
+ * relyio.content(change.contentType).findById(change.entryId).fresh(change.fresh).execute(),
5913
+ * onResync: resync => refetchEverything(resync.cause === 'expired' ? resync.expired.fresh : undefined),
5914
+ * })
5915
+ * ```
5916
+ *
5917
+ * A client without a publishable key sends nothing: a secret or preview key is never put in the
5918
+ * edge's URL. On a server, follow `changes()` with it instead.
5919
+ */
5920
+ liveChanges(options = {}) {
5921
+ const { apiKey, dataset, baseUrl, liveUrl, fetch: fetch2, webSocket, timeout } = this.config;
5922
+ this._liveClientId ?? (this._liveClientId = randomClientId());
5923
+ return subscribeLiveEdge(
5924
+ {
5925
+ apiKey,
5926
+ dataset,
5927
+ baseUrl,
5928
+ liveUrl,
5929
+ fetch: fetch2,
5930
+ webSocket,
5931
+ timeout,
5932
+ clientId: this._liveClientId
5933
+ },
5934
+ options
5935
+ );
5936
+ }
4798
5937
  /**
4799
5938
  * The dataset's current position in the changes feed, as a cursor to start
4800
5939
  * `changes({ since })` or `followChanges(…, { since })` from. One
@@ -4836,7 +5975,7 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4836
5975
  * ```
4837
5976
  */
4838
5977
  adhoc() {
4839
- return this._adhocQuery.build();
5978
+ return this.query.build();
4840
5979
  }
4841
5980
  /**
4842
5981
  * Execute a predefined query by name (from schema)
@@ -4921,16 +6060,53 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4921
6060
  }
4922
6061
  }
4923
6062
  /**
4924
- * Update the configuration and reinitialize all clients
6063
+ * Change some of the client's options and keep the rest.
6064
+ *
6065
+ * What you pass is laid over the configuration the client holds, field by field:
6066
+ *
6067
+ * - an option you name replaces the value the client holds;
6068
+ * - an option left out, or given as `undefined`, keeps the value the client holds;
6069
+ * - an option with no default given as `null` is cleared — `locale`, `dataset`, `apiKey`, `dev`,
6070
+ * `fetch`, `browser.onCorsError`, `realtime.wsUrl`, `debug.logger` — so the client runs as though
6071
+ * it had never been given one. An option with a default takes no `null`;
6072
+ * - a bag of options you name — `browser`, `cache`, `realtime`, `debug` — keeps every field of it
6073
+ * you do not name, so `updateConfig({ browser: { includeCredentials: true } })` keeps a
6074
+ * configured `onCorsError`;
6075
+ * - `headers` is laid over the headers the client sends, a name matched without regard to case: a
6076
+ * header you name is added or replaces every spelling of it, every other one is still sent, and a
6077
+ * header given as `null` is no longer sent;
6078
+ * - naming `apiKey` — a key, or `null` — replaces every key the client holds, one given in
6079
+ * `headers` included.
6080
+ *
6081
+ * The configuration it is laid over is what `createClient()`, `setApiKey()` and earlier updates
6082
+ * gave the client. The `relyio dev` handshake is read again, as `createClient()` reads it: while a
6083
+ * sandbox is active its key and dataset are sent over the configured ones, and once it has gone
6084
+ * the configured ones are.
6085
+ *
6086
+ * The client's one HTTP client is changed in place, so every request from now on is sent as the
6087
+ * update says — including one from a `content()`, `asset()`, `singleton()` or `settings()`
6088
+ * sub-client, a `query` client, or a builder such as `search()` or `batch()`, taken before the
6089
+ * call. The locale reaches them too: each reads the client's locale when it sends, so a new
6090
+ * `locale` is the default of its next request and `locale: null` leaves that request with none. A
6091
+ * locale the call names, and a sub-client's own `withLocale()`, still win over it. A client made
6092
+ * by `withLocale()` is a client of its own: an update of this one does not reach it.
6093
+ *
6094
+ * @example
6095
+ * ```typescript
6096
+ * const relyio = createClient({ apiKey, headers: { 'X-Tenant': 'acme' }, browser: { onCorsError } })
6097
+ *
6098
+ * relyio.updateConfig({ headers: { 'X-Trace': traceId } }) // X-Tenant is still sent
6099
+ * relyio.updateConfig({ browser: { includeCredentials: true } }) // onCorsError is kept
6100
+ * relyio.updateConfig({ headers: { 'X-Tenant': null } }) // X-Tenant is no longer sent
6101
+ * relyio.updateConfig({ locale: null }) // reads name no locale of the client's
6102
+ * ```
4925
6103
  */
4926
- updateConfig(newConfig) {
4927
- const resolved = resolveConfig({ ...this.config, ...newConfig });
6104
+ updateConfig(update) {
6105
+ const resolved = resolveConfigUpdate(this.configured, update);
4928
6106
  this.config = resolved.config;
6107
+ this.configured = resolved.configured;
4929
6108
  this._sandbox = resolved.sandbox;
4930
- this.httpClient = new HttpClient(this.config);
4931
- this._search = new SearchClient(this.httpClient, this.config.locale);
4932
- this._batch = new BatchClient(this.httpClient);
4933
- this._adhocQuery = new AdhocQueryClient(this.httpClient, this.config.locale);
6109
+ this.httpClient.configure(this.config);
4934
6110
  }
4935
6111
  /**
4936
6112
  * Get the current configuration
@@ -4939,11 +6115,31 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4939
6115
  return { ...this.config };
4940
6116
  }
4941
6117
  /**
4942
- * Set the API key
6118
+ * Replace the API key every later request sends as `X-API-Key`.
6119
+ *
6120
+ * It reaches every request this client makes from now on, including those of a `content()`,
6121
+ * `asset()` or other sub-client taken before the call, and `getConfig().apiKey` reports it. A key
6122
+ * given in `headers`, under any spelling, is replaced with it and does not come back on a later
6123
+ * `updateConfig()`. A client already derived with `withLocale()` is a client of its own and keeps
6124
+ * its key.
6125
+ *
6126
+ * An empty or blank key is no key, as it is when passed to `createClient`: the next request
6127
+ * rejects with `MissingApiKeyError` and nothing is sent.
6128
+ *
6129
+ * Under an active `relyio dev` sandbox the sandbox's scoped key stands, as it does over the key
6130
+ * passed to `createClient` — replacing the configured key does not retarget the client at the
6131
+ * sandbox's dataset with a key the sandbox did not issue.
6132
+ *
6133
+ * @example
6134
+ * ```typescript
6135
+ * const relyio = createClient({ apiKey: process.env.RELYIO_API_KEY! })
6136
+ * relyio.setApiKey(rotatedKey) // the next read sends `X-API-Key: <rotatedKey>`
6137
+ * ```
4943
6138
  */
4944
6139
  setApiKey(apiKey) {
4945
- this.config.apiKey = apiKey;
4946
- this.httpClient.setAuthToken(apiKey);
6140
+ this.configured = withConfiguredKey(this.configured, apiKey);
6141
+ if (this._sandbox.state === "active") return;
6142
+ this.httpClient.setApiKey(apiKey);
4947
6143
  }
4948
6144
  /**
4949
6145
  * Get the HTTP client instance (for advanced usage)
@@ -4957,9 +6153,12 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4957
6153
  /**
4958
6154
  * Create a locale-scoped client
4959
6155
  *
4960
- * Every read the returned client builds — `content()`, `asset()`, `singleton()`, `settings()`,
4961
- * `search()`, `similar()`, `autocomplete()`, `executeQuery()` and the ad-hoc queries — is in this
4962
- * locale unless the call names one of its own, which wins.
6156
+ * Every call the returned client builds is in this locale unless the call names one of its own,
6157
+ * which wins: every read — `content()`, `asset()`, `singleton()`, `settings()`, `search()`,
6158
+ * `similar()`, `autocomplete()`, `executeQuery()` and the ad-hoc queries — and every write and
6159
+ * version-history read — `create()` and `update()`, the singleton and settings writes, the asset
6160
+ * file writes, `versions()` and `version()`, and each operation `batch()` builds. `restore()`
6161
+ * always names its locale; a delete, and a `batch().raw()` operation, carry none of the client's.
4963
6162
  *
4964
6163
  * Type-safe when schema is provided: autocompletes valid locale codes!
4965
6164
  *
@@ -4967,10 +6166,11 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4967
6166
  * ```typescript
4968
6167
  * const spanish = relyio.withLocale('es-ES') // ← Autocompletes with schema!
4969
6168
  *
4970
- * // Reads use Spanish
6169
+ * // Reads and writes use Spanish
4971
6170
  * const products = await spanish.content('product').query().execute()
4972
6171
  * const homepage = await spanish.singleton('homepage').fetch()
4973
6172
  * const results = await spanish.search('zapatos').execute()
6173
+ * await spanish.content('product').update(id, { title: 'Zapatos' })
4974
6174
  *
4975
6175
  * // Original client unchanged
4976
6176
  * const englishProducts = await relyio.content('product').query().execute()
@@ -4978,7 +6178,7 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4978
6178
  */
4979
6179
  withLocale(locale) {
4980
6180
  const newConfig = {
4981
- ...this.getConfig(),
6181
+ ...this.configured,
4982
6182
  locale,
4983
6183
  schema: this._schema
4984
6184
  };
@@ -5026,7 +6226,20 @@ function createClient(config) {
5026
6226
  return new TypedRelyIOClient(config);
5027
6227
  }
5028
6228
  __name(createClient, "createClient");
6229
+ function randomClientId() {
6230
+ const bytes = new Uint8Array(16);
6231
+ const random = globalThis.crypto;
6232
+ if (typeof random?.getRandomValues === "function") {
6233
+ random.getRandomValues.call(random, bytes);
6234
+ } else {
6235
+ for (let index = 0; index < bytes.length; index += 1) {
6236
+ bytes[index] = Math.floor(Math.random() * 256);
6237
+ }
6238
+ }
6239
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
6240
+ }
6241
+ __name(randomClientId, "randomClientId");
5029
6242
 
5030
- 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, isAborted, isAssetEntry, isResourceNotFound, matchRoute, serviceInstructedDelayMs, sleep, validateFile };
5031
- //# sourceMappingURL=chunk-7ABNXVWL.js.map
5032
- //# sourceMappingURL=chunk-7ABNXVWL.js.map
6243
+ export { AdhocQueryBuilder, AdhocQueryClient, AssetClient, AssetFileBuilder, AssetUploadBuilder, AuthorizationError, AutocompleteBuilder, BaseValidationError, BatchBuilder, BatchClient, ChangesExpiredError, ConnectionError, ContentByIdBuilder, ContentClient, ContentQueryBuilder, ErrorMapper, FileUploadError, FileValidationError, HttpClient, InvalidApiKeyError, InvalidPayloadError, LIVE_KEEPALIVE_INTERVAL_MS, LiveEdgeError, MethodNotAllowedError, MissingApiKeyError, NetworkError, PUBLISHABLE_KEY_PREFIX, PayloadTooLargeError, PlanFeatureUnavailableError, RateLimitError, ReadOnlyKeyError, ResourceConflictError, ResourceExpiredError, ResourceNotFoundError, SDKError, SearchBuilder, SearchClient, ServerError, ServiceIndeterminateError, SettingsClient, SimilarBuilder, SingletonClient, TimeoutError, TransactionRolledBackError, TypedRelyIOClient, UnknownContractError, UsageQuotaExceededError, VISITOR_ADMISSION_RETRY_AFTER_SECONDS, ValidationError, createClient, entityTagOf, isAborted, isAssetEntry, isPublishableKey, isResourceNotFound, matchRoute, serviceInstructedDelayMs, sleep, validateFile };
6244
+ //# sourceMappingURL=chunk-QFHVF3YF.js.map
6245
+ //# sourceMappingURL=chunk-QFHVF3YF.js.map