@relyio/sdk 0.7.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.
@@ -410,6 +410,45 @@ var _InvalidPayloadError = class _InvalidPayloadError extends BaseValidationErro
410
410
  __name(_InvalidPayloadError, "InvalidPayloadError");
411
411
  var InvalidPayloadError = _InvalidPayloadError;
412
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");
417
+ }
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");
420
+ }
421
+ if (route.kind === "stream" && route.stream === void 0) {
422
+ throw new Error("kind 'stream' requires a StreamDeclaration");
423
+ }
424
+ if (route.kind === "collection" && route.page === void 0) {
425
+ throw new Error("kind 'collection' requires a page dialect");
426
+ }
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}"`
440
+ );
441
+ }
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
+ );
448
+ }
449
+ }
450
+ __name(refuseMismatchedAlternate, "refuseMismatchedAlternate");
451
+
413
452
  // ../../platform/api-contract/dist/index.js
414
453
  var HEADER_RETRY_AFTER = { name: "Retry-After" };
415
454
  var HEADER_ENTITY_TAG = { name: "X-Entity-Tag" };
@@ -417,6 +456,169 @@ var HEADER_IF_MATCH = {
417
456
  name: "If-Match"};
418
457
  var HEADER_IDEMPOTENCY_KEY = {
419
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");
420
622
 
421
623
  // ../../platform/api-contract/dist/client/index.js
422
624
  var _a;
@@ -685,6 +887,8 @@ var DECLARED_PATHS = {
685
887
  "/health/cache": true,
686
888
  "/health/live": true,
687
889
  "/health/ready": true,
890
+ "/internal/live/visitor-feed/{projectId}/{datasetId}/changes": true,
891
+ "/internal/live/visitor-key": true,
688
892
  "/internal/media/claim": true,
689
893
  "/internal/media/file-complete": true,
690
894
  "/internal/media/progress": true,
@@ -1423,8 +1627,20 @@ var _ContentQueryBuilder = class _ContentQueryBuilder {
1423
1627
  __publicField(this, "requestHeaders");
1424
1628
  /** The client's locale, asked when the query is sent rather than copied when it was built. */
1425
1629
  __publicField(this, "clientLocale");
1630
+ __publicField(this, "freshValue");
1426
1631
  this.clientLocale = readClientLocale(locale);
1427
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;
1643
+ }
1428
1644
  /**
1429
1645
  * Headers this query adds to the client's own, e.g. `X-RelyIO-Dataset` to list another dataset
1430
1646
  * the key can see. A name the client already sends is replaced for this query only; the client
@@ -1709,6 +1925,7 @@ var _ContentQueryBuilder = class _ContentQueryBuilder {
1709
1925
  cloned.pageNumber = this.pageNumber;
1710
1926
  cloned.pageSizeValue = this.pageSizeValue;
1711
1927
  cloned.requestHeaders = this.requestHeaders ? { ...this.requestHeaders } : void 0;
1928
+ cloned.freshValue = this.freshValue;
1712
1929
  return cloned;
1713
1930
  }
1714
1931
  /**
@@ -1801,6 +2018,9 @@ var _ContentQueryBuilder = class _ContentQueryBuilder {
1801
2018
  params.childrenLimit = this.childrenOptions.limit;
1802
2019
  }
1803
2020
  }
2021
+ if (this.freshValue !== void 0) {
2022
+ params[FRESH_PARAMETER] = this.freshValue;
2023
+ }
1804
2024
  return params;
1805
2025
  }
1806
2026
  };
@@ -1823,8 +2043,20 @@ var _ContentByIdBuilder = class _ContentByIdBuilder {
1823
2043
  __publicField(this, "requestHeaders");
1824
2044
  /** The client's locale, asked when the read is sent rather than copied when it was built. */
1825
2045
  __publicField(this, "clientLocale");
2046
+ __publicField(this, "freshValue");
1826
2047
  this.clientLocale = readClientLocale(locale);
1827
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;
2059
+ }
1828
2060
  /**
1829
2061
  * Include related content in the response
1830
2062
  */
@@ -1913,6 +2145,9 @@ var _ContentByIdBuilder = class _ContentByIdBuilder {
1913
2145
  params.childrenLimit = this.childrenOptions.limit;
1914
2146
  }
1915
2147
  }
2148
+ if (this.freshValue !== void 0) {
2149
+ params[FRESH_PARAMETER] = this.freshValue;
2150
+ }
1916
2151
  try {
1917
2152
  return await this.httpClient.fetchPayload(
1918
2153
  "/v1/{contentType}/{id}",
@@ -3079,6 +3314,7 @@ var _SingletonClient = class _SingletonClient {
3079
3314
  const localeValueFinal = options?.locale ?? this.localeValue ?? this.clientLocale();
3080
3315
  if (includeValue) query.include = includeValue.join(",");
3081
3316
  if (localeValueFinal != null && localeValueFinal !== "") query.locale = localeValueFinal;
3317
+ if (options?.fresh !== void 0) query[FRESH_PARAMETER] = options.fresh;
3082
3318
  const response = await this.httpClient.send(
3083
3319
  "/v1/singletons/{key}",
3084
3320
  "get",
@@ -3374,6 +3610,7 @@ var _SearchBuilder = class _SearchBuilder {
3374
3610
  __publicField(this, "query");
3375
3611
  __publicField(this, "searchOptions", {});
3376
3612
  __publicField(this, "requestHeaders");
3613
+ __publicField(this, "freshValue");
3377
3614
  /** The client's locale, asked when the query is sent rather than copied when it was built. */
3378
3615
  __publicField(this, "clientLocale");
3379
3616
  this.query = query;
@@ -3387,6 +3624,15 @@ var _SearchBuilder = class _SearchBuilder {
3387
3624
  this.requestHeaders = headers;
3388
3625
  return this;
3389
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
+ }
3390
3636
  /**
3391
3637
  * Search within a single content type. Where a schema types the client, the name is one of its
3392
3638
  * content types and the results are that type's entries.
@@ -3528,7 +3774,11 @@ var _SearchBuilder = class _SearchBuilder {
3528
3774
  const response = await this.httpClient.send(
3529
3775
  "/v1/search",
3530
3776
  "post",
3531
- { body, headers: this.requestHeaders }
3777
+ {
3778
+ body,
3779
+ headers: this.requestHeaders,
3780
+ ...this.freshValue === void 0 ? {} : { query: { [FRESH_PARAMETER]: this.freshValue } }
3781
+ }
3532
3782
  );
3533
3783
  return response.body;
3534
3784
  }
@@ -4544,23 +4794,67 @@ var AdhocQueryClient = _AdhocQueryClient;
4544
4794
 
4545
4795
  // src/changes/changes.ts
4546
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
+ };
4547
4804
  var _ChangesExpiredError = class _ChangesExpiredError extends ResourceExpiredError {
4548
- /** The base class's signature, so `withContext` can rebuild one. */
4549
- 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) {
4550
4807
  super(message, statusCode, cause, context);
4551
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");
4552
4816
  this.name = "ChangesExpiredError";
4553
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
+ );
4554
4840
  }
4555
- static of(expired) {
4556
- 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
+ );
4557
4850
  }
4558
4851
  };
4559
4852
  __name(_ChangesExpiredError, "ChangesExpiredError");
4560
4853
  var ChangesExpiredError = _ChangesExpiredError;
4561
4854
  function expiredReasonOf(message) {
4562
- if (message.includes("(retention)")) return "retention";
4563
- 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
+ }
4564
4858
  return "unknown";
4565
4859
  }
4566
4860
  __name(expiredReasonOf, "expiredReasonOf");
@@ -4580,7 +4874,7 @@ async function* pollChanges(http, options = {}) {
4580
4874
  const onExpired = options.onExpired ?? "throw";
4581
4875
  if (onExpired === "throw") throw expired;
4582
4876
  since = void 0;
4583
- resynced = typeof onExpired === "function" ? expired : void 0;
4877
+ resynced = typeof onExpired === "function" ? error : void 0;
4584
4878
  continue;
4585
4879
  }
4586
4880
  const { data, page } = response.body;
@@ -4589,13 +4883,13 @@ async function* pollChanges(http, options = {}) {
4589
4883
  }
4590
4884
  since = page.nextCursor;
4591
4885
  if (resynced !== void 0 && typeof options.onExpired === "function") {
4592
- const expired = resynced;
4886
+ const expired = ChangesExpiredError.of(resynced, page.nextCursor);
4593
4887
  resynced = void 0;
4594
4888
  await options.onExpired(expired);
4595
4889
  if (isAborted(signal)) return;
4596
4890
  }
4597
4891
  for (const change of data) {
4598
- yield change;
4892
+ yield { ...change, fresh: change.cursor };
4599
4893
  if (isAborted(signal)) return;
4600
4894
  }
4601
4895
  if (data.length === 0) {
@@ -4634,6 +4928,477 @@ async function pollOnce(http, since, options) {
4634
4928
  }
4635
4929
  __name(pollOnce, "pollOnce");
4636
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
+
4637
5402
  // src/config/dev-detection.ts
4638
5403
  function sandboxStateOf(resolution) {
4639
5404
  return resolution.state === "active" ? resolution.sandbox : resolution;
@@ -4776,6 +5541,12 @@ function validateConfig(config) {
4776
5541
  if (config.baseUrl != null && typeof config.baseUrl !== "string") {
4777
5542
  throw new Error("Base URL must be a string");
4778
5543
  }
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");
5549
+ }
4779
5550
  if (config.timeout != null) assertRequestTimeout(config.timeout);
4780
5551
  if (config.retryAttempts != null && !(Number.isInteger(config.retryAttempts) && config.retryAttempts >= 1)) {
4781
5552
  throw new Error(
@@ -4883,6 +5654,8 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
4883
5654
  * locale, or none after `locale: null` (REL-1453).
4884
5655
  */
4885
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");
4886
5659
  const { schema, ...clientConfig } = config;
4887
5660
  this._schema = schema;
4888
5661
  const resolved = resolveConfig(clientConfig);
@@ -5091,13 +5864,15 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
5091
5864
  * Lazy — nothing is requested until the first `next()`. It runs until
5092
5865
  * `signal` aborts, which ends the `for await` cleanly rather than throwing.
5093
5866
  * A change names what changed; refetch it through the ordinary read path
5094
- * (`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`).
5095
5869
  *
5096
5870
  * @example
5097
5871
  * ```typescript
5098
5872
  * const controller = new AbortController()
5099
5873
  * for await (const change of relyio.changes({ since: cursor, types: ['post'], signal: controller.signal })) {
5100
- * // { 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()
5101
5876
  * }
5102
5877
  * ```
5103
5878
  *
@@ -5105,13 +5880,60 @@ var _TypedRelyIOClient = class _TypedRelyIOClient {
5105
5880
  * ```typescript
5106
5881
  * for await (const change of relyio.changes({
5107
5882
  * since: cursor,
5108
- * onExpired: async () => invalidateEverything(),
5883
+ * onExpired: async expired => refetchEverything(expired.fresh),
5109
5884
  * })) { … }
5110
5885
  * ```
5111
5886
  */
5112
5887
  changes(options = {}) {
5113
5888
  return pollChanges(this.httpClient, options);
5114
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
+ }
5115
5937
  /**
5116
5938
  * The dataset's current position in the changes feed, as a cursor to start
5117
5939
  * `changes({ since })` or `followChanges(…, { since })` from. One
@@ -5404,7 +6226,20 @@ function createClient(config) {
5404
6226
  return new TypedRelyIOClient(config);
5405
6227
  }
5406
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");
5407
6242
 
5408
- export { AdhocQueryBuilder, AdhocQueryClient, AssetClient, AssetFileBuilder, AssetUploadBuilder, AuthorizationError, AutocompleteBuilder, BaseValidationError, BatchBuilder, BatchClient, ChangesExpiredError, ConnectionError, ContentByIdBuilder, ContentClient, ContentQueryBuilder, ErrorMapper, FileUploadError, FileValidationError, HttpClient, InvalidApiKeyError, InvalidPayloadError, MethodNotAllowedError, MissingApiKeyError, NetworkError, PayloadTooLargeError, PlanFeatureUnavailableError, RateLimitError, ReadOnlyKeyError, ResourceConflictError, ResourceExpiredError, ResourceNotFoundError, SDKError, SearchBuilder, SearchClient, ServerError, ServiceIndeterminateError, SettingsClient, SimilarBuilder, SingletonClient, TimeoutError, TransactionRolledBackError, TypedRelyIOClient, UnknownContractError, UsageQuotaExceededError, ValidationError, createClient, entityTagOf, isAborted, isAssetEntry, isResourceNotFound, matchRoute, serviceInstructedDelayMs, sleep, validateFile };
5409
- //# sourceMappingURL=chunk-GYHAVYND.js.map
5410
- //# sourceMappingURL=chunk-GYHAVYND.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