@openrails/contentkit-ui 0.103.1 → 0.104.0-rc.1

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 (145) hide show
  1. package/README.md +105 -43
  2. package/dist/client/beacons.d.ts +3 -13
  3. package/dist/client/beacons.js +4 -45
  4. package/dist/client/beacons.js.map +1 -1
  5. package/dist/client/client.d.ts +3 -0
  6. package/dist/client/client.js +45 -43
  7. package/dist/client/client.js.map +1 -1
  8. package/dist/client/content/favorites.d.ts +2 -0
  9. package/dist/client/content/favorites.js.map +1 -1
  10. package/dist/client/content/types.d.ts +3 -0
  11. package/dist/client/content/types.js.map +1 -1
  12. package/dist/client/discovery.d.ts +28 -3
  13. package/dist/client/discovery.js +22 -1
  14. package/dist/client/discovery.js.map +1 -1
  15. package/dist/client/engagement.d.ts +70 -0
  16. package/dist/client/engagement.js +191 -0
  17. package/dist/client/engagement.js.map +1 -0
  18. package/dist/client/generated/routes.d.ts +35 -9
  19. package/dist/client/generated/routes.js +59 -2
  20. package/dist/client/generated/routes.js.map +1 -1
  21. package/dist/client/generated/wire.d.ts +53 -12
  22. package/dist/client/generated/wire.js.map +1 -1
  23. package/dist/client/guest-reading-history.d.ts +40 -0
  24. package/dist/client/guest-reading-history.js +144 -0
  25. package/dist/client/guest-reading-history.js.map +1 -0
  26. package/dist/client/index.d.ts +4 -2
  27. package/dist/client/stats.d.ts +5 -5
  28. package/dist/client/stats.js.map +1 -1
  29. package/dist/client.js +24 -22
  30. package/dist/components/admin/content-manager.d.ts +1 -1
  31. package/dist/components/admin/content-manager.js +342 -302
  32. package/dist/components/admin/content-manager.js.map +1 -1
  33. package/dist/components/admin/page-review.js +106 -97
  34. package/dist/components/admin/page-review.js.map +1 -1
  35. package/dist/components/admin/quick-search.d.ts +14 -0
  36. package/dist/components/admin/quick-search.js +60 -0
  37. package/dist/components/admin/quick-search.js.map +1 -0
  38. package/dist/components/admin/reader-content-editor.d.ts +6 -0
  39. package/dist/components/admin/reader-content-editor.js +733 -0
  40. package/dist/components/admin/reader-content-editor.js.map +1 -0
  41. package/dist/components/admin/reader-manifest.js +120 -105
  42. package/dist/components/admin/reader-manifest.js.map +1 -1
  43. package/dist/components/admin/stats-dashboard.d.ts +4 -4
  44. package/dist/components/admin/stats-dashboard.js +132 -134
  45. package/dist/components/admin/stats-dashboard.js.map +1 -1
  46. package/dist/components/content/daily-releases.js +143 -191
  47. package/dist/components/content/daily-releases.js.map +1 -1
  48. package/dist/components/content/editor-save.d.ts +32 -0
  49. package/dist/components/content/editor-save.js +51 -0
  50. package/dist/components/content/editor-save.js.map +1 -0
  51. package/dist/components/content/editors.d.ts +17 -2
  52. package/dist/components/content/editors.js +359 -328
  53. package/dist/components/content/editors.js.map +1 -1
  54. package/dist/components/content/favorites.d.ts +6 -0
  55. package/dist/components/content/favorites.js +33 -0
  56. package/dist/components/content/favorites.js.map +1 -0
  57. package/dist/components/content/filter-dialog.js +19 -15
  58. package/dist/components/content/filter-dialog.js.map +1 -1
  59. package/dist/components/content/filters.d.ts +2 -0
  60. package/dist/components/content/filters.js +5 -1
  61. package/dist/components/content/filters.js.map +1 -1
  62. package/dist/components/content/grid.d.ts +34 -13
  63. package/dist/components/content/grid.js +300 -271
  64. package/dist/components/content/grid.js.map +1 -1
  65. package/dist/components/content/history.d.ts +6 -0
  66. package/dist/components/content/history.js +91 -0
  67. package/dist/components/content/history.js.map +1 -0
  68. package/dist/components/content/index.d.ts +1 -0
  69. package/dist/components/content/offer-fields.d.ts +1 -1
  70. package/dist/components/content/offer-fields.js +86 -64
  71. package/dist/components/content/offer-fields.js.map +1 -1
  72. package/dist/components/content/search.d.ts +11 -4
  73. package/dist/components/content/search.js +22 -11
  74. package/dist/components/content/search.js.map +1 -1
  75. package/dist/components/content/summary-tile.d.ts +14 -0
  76. package/dist/components/content/summary-tile.js +63 -0
  77. package/dist/components/content/summary-tile.js.map +1 -0
  78. package/dist/components/groups/memberships.d.ts +11 -1
  79. package/dist/components/groups/memberships.js +1 -1
  80. package/dist/components/groups/memberships.js.map +1 -1
  81. package/dist/components/media-folder-editor.d.ts +4 -0
  82. package/dist/components/media-folder-editor.js +178 -168
  83. package/dist/components/media-folder-editor.js.map +1 -1
  84. package/dist/components/video-mini-player.d.ts +4 -1
  85. package/dist/components/video-mini-player.js +77 -71
  86. package/dist/components/video-mini-player.js.map +1 -1
  87. package/dist/components/video-player.d.ts +4 -1
  88. package/dist/components/video-player.js +280 -276
  89. package/dist/components/video-player.js.map +1 -1
  90. package/dist/index.d.ts +1 -0
  91. package/dist/index.js +17 -14
  92. package/dist/locales/admin/de.js +33 -8
  93. package/dist/locales/admin/de.js.map +1 -1
  94. package/dist/locales/admin/en.d.ts +29 -4
  95. package/dist/locales/admin/en.js +33 -8
  96. package/dist/locales/admin/en.js.map +1 -1
  97. package/dist/locales/admin/es.js +33 -8
  98. package/dist/locales/admin/es.js.map +1 -1
  99. package/dist/locales/admin/ja.js +33 -8
  100. package/dist/locales/admin/ja.js.map +1 -1
  101. package/dist/locales/admin/ko.js +33 -8
  102. package/dist/locales/admin/ko.js.map +1 -1
  103. package/dist/locales/admin/zh.js +33 -8
  104. package/dist/locales/admin/zh.js.map +1 -1
  105. package/dist/locales/discovery/de.js +0 -1
  106. package/dist/locales/discovery/de.js.map +1 -1
  107. package/dist/locales/discovery/en.d.ts +0 -1
  108. package/dist/locales/discovery/en.js +0 -1
  109. package/dist/locales/discovery/en.js.map +1 -1
  110. package/dist/locales/discovery/es.js +0 -1
  111. package/dist/locales/discovery/es.js.map +1 -1
  112. package/dist/locales/discovery/ja.js +0 -1
  113. package/dist/locales/discovery/ja.js.map +1 -1
  114. package/dist/locales/discovery/ko.js +0 -1
  115. package/dist/locales/discovery/ko.js.map +1 -1
  116. package/dist/locales/discovery/zh.js +0 -1
  117. package/dist/locales/discovery/zh.js.map +1 -1
  118. package/dist/locales/en.d.ts +29 -5
  119. package/dist/react/discovery.d.ts +20 -17
  120. package/dist/react/discovery.js +273 -221
  121. package/dist/react/discovery.js.map +1 -1
  122. package/dist/react/index.d.ts +2 -1
  123. package/dist/react/page-review.d.ts +1 -1
  124. package/dist/react/page-review.js +6 -5
  125. package/dist/react/page-review.js.map +1 -1
  126. package/dist/react/resources.d.ts +3 -1
  127. package/dist/react/resources.js +4 -0
  128. package/dist/react/resources.js.map +1 -1
  129. package/dist/react/stats.d.ts +2 -2
  130. package/dist/react/stats.js.map +1 -1
  131. package/dist/react/use-draft.d.ts +8 -0
  132. package/dist/react/use-draft.js +35 -0
  133. package/dist/react/use-draft.js.map +1 -0
  134. package/dist/react/use-engagement.d.ts +26 -0
  135. package/dist/react/use-engagement.js +221 -0
  136. package/dist/react/use-engagement.js.map +1 -0
  137. package/dist/react/use-resource.d.ts +4 -3
  138. package/dist/react/use-resource.js +2 -1
  139. package/dist/react/use-resource.js.map +1 -1
  140. package/dist/react.js +12 -11
  141. package/dist/styles.css +1 -1
  142. package/dist/ui/dialog.d.ts +2 -1
  143. package/dist/ui/dialog.js +23 -20
  144. package/dist/ui/dialog.js.map +1 -1
  145. package/package.json +1 -1
package/README.md CHANGED
@@ -345,9 +345,24 @@ Custom page-thumbnail or media grids can use the same primitive: call
345
345
  gaps and responsive behavior identical to content-card lists.
346
346
 
347
347
  `ContentGrid` and `ContentListing` take `locked`: `show` (default) marks the
348
- cards the reader doesn't open, `hide` leaves them out, and `collapse` lists
349
- the open ones and puts the rest behind a counted toggle that loads them on
350
- opening (`useLockedCount` counts them).
348
+ cards the reader doesn't open, `hide` leaves them out, and `collapse` replaces
349
+ locked cards with a cover-sized summary tile. Opening it reveals the cached
350
+ cards in their original ranking order, with a reduced-motion-aware animation.
351
+ The locked and optional-filter tiles use the same component as daily releases.
352
+
353
+ `ContentGrid` and `SearchResults` request `summary=true` on the first page.
354
+ `useContents` and `useSearch` expose that selection's `summary` across pagination:
355
+ the distinct `filtered_count`, overlapping `filtered_entities` counts, and
356
+ `locked_count` before cursor pagination. All counts retain the query's hard
357
+ domain, language, entity, access and popularity-window eligibility. A bounded
358
+ keyword selection may report `truncated`; its tiles and contextual filter
359
+ counts then show `≥` to identify lower bounds. Clicking a filtered tile opens
360
+ the shared filter dialog with this selection's per-entity counts.
361
+
362
+ Preview policy belongs to the host. Set `showFilteredSummary={false}` to omit
363
+ that tile, `lockedSummaryMinColumns={6}` to offer a locked tile only when six
364
+ grid columns fit, and `cardLimit={6}` / `mobileCardLimit={4}` to limit mounted
365
+ preview cards. `summaryLabels` supplies the host's nouns for both tile labels.
351
366
 
352
367
  ### Daily releases
353
368
 
@@ -718,10 +733,28 @@ const nav = useMemo(() => ({ pathname: location.pathname, search: location.searc
718
733
  <ContentManager kind="blog" client={contentkit} basePath="/admin/blog" navigation={nav} renderEditor={richTextEditor} />
719
734
  ```
720
735
 
736
+ A `reader` kind uses one gallery workspace: localized metadata, configured
737
+ entities, access, cover, ZIP/pages, ordering and publication are available
738
+ together. A new draft is allocated internally on the first save or upload;
739
+ its URL change keeps the current fields and upload queue mounted. Versions,
740
+ chapter ranges, audio and reading anchors remain available in expandable
741
+ tools. **Publish** saves the visible edits and any dirty tools first and
742
+ checks every media window; failed uploads, processing or refused saves keep
743
+ the work unpublished. **Save draft** can save metadata while files upload.
744
+
721
745
  `RichTextEditor` (`value`, `onChange`, `uploadImage`) is Tiptap: headings,
722
746
  emphasis, lists, quotes, links and images picked, pasted or dropped. Its
723
747
  HTML keeps to that schema, so bodies stay a small, predictable subset.
724
748
 
749
+ - **`AdminQuickSearch`**: autocomplete navigation to content and entity editors,
750
+ including managed drafts and hidden content. Supply the permitted
751
+ `contentKinds` / `entityKinds` and mandatory `contentHref` / `entityHref`
752
+ callbacks. Suggestions without an editor URL are omitted; public paths are
753
+ never a fallback. It reuses `SearchBox` geometry and arrow/Enter/Escape
754
+ controls, with no recent-search history or free-text results submission.
755
+ Management authorization remains enforced by the server. Set
756
+ `presentation="header"` for the shared centered-header dropdown.
757
+
725
758
  - **`PageReview`**: retained page sources, original filenames, automatic cleanup
726
759
  reasons, previews, inclusion overrides and ordering. `ContentManager` includes
727
760
  it for reader versions; use `content` and `version` to embed it separately.
@@ -884,24 +917,30 @@ or scroll: a dimmed backdrop, text rows with colored type icons, and no
884
917
  thumbnails. Recent searches omit dates. The panel
885
918
  slides downward when opened and honors reduced motion. Its mobile/desktop
886
919
  scroll limit is 70vh/60vh. The default inline
887
- presentation is unchanged. Both presentations show prefix-matching recent
888
- queries first (newest first), then matching entities, then content titles. A
920
+ presentation is unchanged. Both presentations show matching recent queries
921
+ first, with prefixes before interior matches and newest first within each tier,
922
+ then matching entities, then content titles. A
889
923
  recent query remains selectable while network suggestions debounce or load.
890
924
  Guest history persists in localStorage across tabs; account history comes from
891
925
  the server, fetched on opening rather than on every keystroke. `recent={false}`
892
926
  omits history. Only completed searches are recorded.
893
927
 
894
- `GET /v1/typeahead` matches the beginning of a title, entity name or direct
895
- alias, using the keyword index's case/accent/whitespace normalization. It does
896
- not expand associated entities, contextual keywords, interior words or typos:
928
+ `GET /v1/typeahead` matches literal substrings of a title, entity name or direct
929
+ alias, preferring exact names and prefixes. Case, Latin accents and whitespace
930
+ are normalized, and complete English filler words `the`, `a` and `an` are
931
+ ignored in both autocomplete/history matching and full search. Filler-only
932
+ queries return no matches. Identity and slug normalization stay unchanged.
933
+ Autocomplete does not expand associated entities, contextual keywords or typos:
897
934
  Inazuma can suggest that artist, but not Business Question merely because
898
935
  Inazuma created it. Full `GET /v1/search` retains broader keyword and typo
899
936
  matching. Visibility, tenant, language and version eligibility still apply.
900
937
  The host owns search-page navigation and suggestion links.
901
938
 
902
- Core migration `0010_name_prefix_index` adds the name-only prefix index; apply
903
- normal ContentKit migrations before serving the new core. Original keyword
904
- indexes and full-search behavior remain in place.
939
+ Core migration `0011_search_substrings` adds search-only indexes for native
940
+ literal substring retrieval and normalized full search; apply normal migrations
941
+ before serving the new core. Original functions/indexes remain in place so
942
+ 0.103 writers can overlap during an upgrade. Native query-plan tests verify
943
+ the indexed Latin/CJK paths against a 50,000-document catalog.
905
944
  `ReaderSettings` combines language selection and content filters in one dialog:
906
945
 
907
946
  ```tsx
@@ -974,35 +1013,58 @@ language choice. Ordinary sign-ins never import guest preferences.
974
1013
  a retry action. ReaderSettings shows an incomplete transfer and lets the reader
975
1014
  retry it. Guest preferences are retained separately after registration and logout.
976
1015
 
977
- ### Activity tracking
978
-
979
- `useViewSignal(contentId, versionId)` starts an observation when its returned
980
- callback first reports progress. The shared `Beacons` client immediately requests
981
- an activity context from ContentKit, then includes that signed receipt in each
982
- revision. Its server timestamp stays fixed for the entire view. Pass a version
983
- when one is displayed; receipt requests use the provider's current language, or
984
- the client's configured request language outside React.
985
-
986
- `useExposure(items, options)` records cumulative rendered and visible placements.
987
- It splits long lists into stable segments of at most 32 placements, increments
988
- each segment's stage revision, and attributes clicks to the matching segment.
989
- Each newly observed placement receives its own server context. The client does
990
- not supply premium status or eligibility claims. Receipt requests are batched in
991
- groups of at most 100 targets.
992
-
993
- For a custom integration, `client.discovery.activityContexts(targets, session,
994
- language)` exposes `POST /activity-contexts`. Request its contexts when the
995
- observation starts, not when a delayed event batch is flushed. An explicit
996
- `enabled: false` response permits legacy tracking for an embedding without a
997
- signer; a failed or invalid receipt never silently becomes an untrusted event.
998
-
999
- The provider's identity changes invalidate buffered observations and active
1000
- view sessions. Events collected for one account or guest are never reassigned
1001
- to the next account. Custom callers using `Beacons.exposure` should call
1002
- `endExposure(renderId)` after the list leaves the page to release receipt state;
1003
- `useExposure` does this automatically.
1004
-
1005
- Tracking flushes pack complete events into byte-bounded requests and serialize
1006
- keepalive sends across clients. Large queues flush while the page is active.
1007
- A single cumulative event that exceeds the keepalive budget uses normal fetch;
1008
- its delivery during page exit remains best effort.
1016
+ ### Measured engagement
1017
+
1018
+ `useReadingEngagement(contentId, versionId, options)` measures loaded, visible
1019
+ content while the page is focused. Keep the hook above the horizontal/vertical
1020
+ reader switch so a mode change retains its visit. Attach `engagement.page(index,
1021
+ filename)` to the actual image element (zero-based index), or `engagement.video`
1022
+ to the playing video. `engagement.setResume(filenameOrAnchor)` records the opaque
1023
+ reader selector separately from measured coverage. Optional
1024
+ `[data-reader-viewport]` on the reader container clips page measurements to that
1025
+ viewport, including long-page breakpoint readers.
1026
+
1027
+ Opening the route, preloading an image and changing the page selector produce
1028
+ no engagement on their own. The tracker measures the visible part of each image
1029
+ and focused active milliseconds; multiple visible pages share elapsed attention.
1030
+ Static-page attention pauses after 60 seconds without genuine input (configurable
1031
+ with `idleAfter`). Visible, focused playback continues without requiring mouse
1032
+ motion, but pauses, buffering and seeks do not become watched coverage. Suspended
1033
+ timer gaps over five seconds are discarded rather than guessed.
1034
+
1035
+ Page changes stay local. Cumulative snapshots flush at most every 120 seconds
1036
+ (`checkpointInterval` may shorten this), and on hiding/exiting the page or
1037
+ unmounting the reader. `engagement.flush()` offers an explicit checkpoint.
1038
+ Each segment captures its own immutable server activity receipt when observation
1039
+ starts, retaining event-time premium/free access across retries; a later receipt
1040
+ never reclassifies earlier attention. Revisions replace the same segment, rather
1041
+ than incrementing visit or page-open counters. Positive short partials are kept
1042
+ so later measured attention can accumulate; qualification happens server-side.
1043
+
1044
+ Use `enabled` to honor the host's consent and authentication-readiness policy.
1045
+ With an explicitly signed-out provider (`viewer={null}`), positive partials also
1046
+ save browser-local resume/history across tabs. This remains useful when the signal
1047
+ plane is unavailable, without inventing remote analytics. Account history stays
1048
+ server-owned. An account switch discards old in-flight observations instead of
1049
+ assigning them to the next account. The optional `kind` groups local guest history
1050
+ only; remote requests never trust a browser-supplied content kind.
1051
+
1052
+ For a custom integration, `EngagementTracker` accepts injected `begin` and
1053
+ `submit` callbacks. `client.discovery.activityContexts(targets, session, language)`
1054
+ exposes `POST /activity-contexts`; `client.discovery.engagement(snapshots, options)`
1055
+ exposes `POST /engagement`. Capture receipts when their segments begin, not when
1056
+ delayed batches flush. A missing, disabled or invalid receipt prevents remote
1057
+ engagement submission and never falls back to legacy route-open tracking.
1058
+
1059
+ `useExposure(items, options)` separately records cumulative rendered and visible
1060
+ list placements and attributes clicks to their original ranks. Hidden paid tiles
1061
+ are not impressions. An explicit item `position` preserves gaps from omitted
1062
+ cards. Each new placement receives a server context; the client supplies no
1063
+ premium-status or eligibility claims. End custom `Beacons.exposure` receipt state
1064
+ with `endExposure(renderId)` when its list leaves the page; `useExposure` does this
1065
+ automatically. These bounded exposure/click packets serialize keepalive requests;
1066
+ exit delivery remains best effort.
1067
+
1068
+ The staff dashboard and SDK statistics queries use measured people and active
1069
+ time, with comments ranking retained. See [statistics semantics](docs/statistics.md)
1070
+ for daily distinctness, qualification and active-time units.
@@ -1,14 +1,14 @@
1
1
  import { ContentKitClient } from './client.js';
2
2
  import { ExposureInput } from './generated/wire.js';
3
3
  export interface BeaconOptions {
4
- /** A signed-out visitor's session; default one per browser tab. */
4
+ /** A signed-out visitor identity, persistent across this browser's tabs. */
5
5
  session?: () => string | undefined;
6
6
  /** How long events gather before they are sent, ms. Default 2000. */
7
7
  delay?: number;
8
8
  /** How long beacons stay off after the site records no signals, ms, before the content model is read again. Default 60000. */
9
9
  recheck?: number;
10
10
  }
11
- /** Where a click or a view came from: the list render it was shown in. */
11
+ /** Where a click came from: the list render it was shown in. */
12
12
  export interface Attribution {
13
13
  /** Language of the displayed content; defaults to the client's request language. */
14
14
  language?: string;
@@ -17,16 +17,8 @@ export interface Attribution {
17
17
  /** 1-based position in that render. */
18
18
  position?: number;
19
19
  }
20
- export interface ViewSession {
21
- /** Reports the view so far: pages read, seconds watched or percent scrolled; resume is where to pick up. */
22
- update(o: {
23
- progress?: number;
24
- duration_s?: number;
25
- resume?: string;
26
- }): void;
27
- }
28
20
  export declare const eventId: () => string;
29
- /** A signed-out reader's session: one per browser tab, kept across reloads. */
21
+ /** A random signed-out browser identity shared across tabs/reloads, never an account credential. */
30
22
  export declare function readerSession(): string;
31
23
  /**
32
24
  * Batched, best-effort signals and exposures: gathered for `delay`, sent in
@@ -55,8 +47,6 @@ export declare class Beacons {
55
47
  click(content: string, a?: Attribution & {
56
48
  version_id?: string;
57
49
  }): void;
58
- /** A consumption session: all revisions use its first server access snapshot. */
59
- view(content: string, version?: string, a?: Attribution): ViewSession;
60
50
  /** A cumulative result-list stage; each newly observed target gets one receipt. */
61
51
  exposure(e: ExposureInput): void;
62
52
  /** Keep receipt state when a mounted list resumes (including StrictMode replay). */
@@ -19,13 +19,13 @@ function c(e) {
19
19
  let t = i.then(e, e);
20
20
  return i = t.then(() => {}, () => {}), t;
21
21
  }
22
- var l = "contentkit.session", u = () => typeof crypto < "u" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`, d;
22
+ var l = "contentkit.visitor.v1", u = () => typeof crypto < "u" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`, d;
23
23
  function f() {
24
24
  try {
25
- let e = sessionStorage.getItem(l);
25
+ let e = localStorage.getItem(l);
26
26
  if (e) return e;
27
27
  let t = u();
28
- return sessionStorage.setItem(l, t), t;
28
+ return localStorage.setItem(l, t), t;
29
29
  } catch {
30
30
  return d ??= u();
31
31
  }
@@ -75,47 +75,6 @@ var p = class {
75
75
  });
76
76
  }), n);
77
77
  }
78
- view(e, t, n = {}) {
79
- let r = u(), i = this.generation, a = {
80
- content_id: e,
81
- version_id: t
82
- }, { language: o, ...s } = n, c = 0, l, d, f, p = !1, h = () => {
83
- d && f && i === this.generation && this.signal({
84
- ...d,
85
- occurred_at: f.context?.occurred_at ?? l,
86
- activity_token: f.context?.token
87
- });
88
- };
89
- return { update: (n) => {
90
- if (!(i !== this.generation || this.paused())) {
91
- if (l ??= (/* @__PURE__ */ new Date()).toISOString(), d = {
92
- content_id: e,
93
- version_id: t,
94
- type: "view",
95
- event_id: r,
96
- revision: ++c,
97
- occurred_at: l,
98
- ...s,
99
- ...n
100
- }, p) h();
101
- else {
102
- p = !0;
103
- let e = this.contexts([a], i, o).then((e) => {
104
- if (i === this.generation) {
105
- if (f = e?.get(m(a)), !f) {
106
- p = !1;
107
- return;
108
- }
109
- h();
110
- }
111
- }).catch((e) => {
112
- throw i === this.generation && (p = !1), e;
113
- });
114
- this.track(e, i);
115
- }
116
- }
117
- } };
118
- }
119
78
  exposure(e) {
120
79
  if (this.paused() || !e.shown.length) return;
121
80
  let t = this.generation, n = `${e.render_id}\u0000${e.stage}`, r = this.exposureContexts.get(n);
@@ -228,7 +187,7 @@ var p = class {
228
187
  this.pending.add(n);
229
188
  }
230
189
  signal(e) {
231
- this.paused() || (e.type === "view" && (this.signals = this.signals.filter((t) => t.event_id !== e.event_id)), this.signals.push(e), this.schedule());
190
+ this.paused() || (this.signals.push(e), this.schedule());
232
191
  }
233
192
  paused() {
234
193
  return this.offUntil > Date.now() || (this.offUntil = 0, !1);
@@ -1 +1 @@
1
- {"version":3,"file":"beacons.js","names":[],"sources":["../../src/client/beacons.ts"],"sourcesContent":["import type { ContentKitClient } from \"./client.js\";\nimport { toContentKitError } from \"./errors.js\";\nimport type { ActivityContext, ActivityTarget, ExposureInput, SignalInput } from \"./generated/wire.js\";\n\nexport interface BeaconOptions {\n /** A signed-out visitor's session; default one per browser tab. */\n session?: () => string | undefined;\n /** How long events gather before they are sent, ms. Default 2000. */\n delay?: number;\n /** How long beacons stay off after the site records no signals, ms, before the content model is read again. Default 60000. */\n recheck?: number;\n}\n\n/** Where a click or a view came from: the list render it was shown in. */\nexport interface Attribution {\n /** Language of the displayed content; defaults to the client's request language. */\n language?: string;\n render_id?: string;\n surface?: string;\n /** 1-based position in that render. */\n position?: number;\n}\n\nexport interface ViewSession {\n /** Reports the view so far: pages read, seconds watched or percent scrolled; resume is where to pick up. */\n update(o: { progress?: number; duration_s?: number; resume?: string }): void;\n}\n\nconst MAX = 100;\n// Fetch's keepalive limit includes all in-flight bodies in the fetch group.\n// Leave headroom and serialize this module's keepalive requests across clients.\nconst KEEPALIVE_BYTES = 60 * 1024;\nconst FLUSH_BYTES = 48 * 1024;\nlet keepaliveTail: Promise<void> = Promise.resolve();\nconst encoder = new TextEncoder();\nconst bytes = (field: \"signals\" | \"exposures\", rows: readonly unknown[], session?: string) => encoder.encode(JSON.stringify({ session, [field]: rows })).length;\n\nfunction packets<T>(items: readonly T[], field: \"signals\" | \"exposures\", session?: string): { items: T[]; bytes: number }[] {\n const out: { items: T[]; bytes: number }[] = [];\n let current: T[] = [];\n for (const item of items) {\n if (current.length && (current.length === MAX || bytes(field, [...current, item], session) > KEEPALIVE_BYTES)) {\n out.push({ items: current, bytes: bytes(field, current, session) });\n current = [];\n }\n current.push(item);\n }\n if (current.length) out.push({ items: current, bytes: bytes(field, current, session) });\n return out;\n}\n\nfunction serializeKeepalive(send: () => Promise<void>): Promise<void> {\n const pending = keepaliveTail.then(send, send);\n keepaliveTail = pending.then(() => {}, () => {});\n return pending;\n}\nconst SESSION_KEY = \"contentkit.session\";\nexport const eventId = () => (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`);\n\nlet unstored: string | undefined;\n\n/** A signed-out reader's session: one per browser tab, kept across reloads. */\nexport function readerSession(): string {\n try {\n const kept = sessionStorage.getItem(SESSION_KEY);\n if (kept) return kept;\n const made = eventId();\n sessionStorage.setItem(SESSION_KEY, made);\n return made;\n } catch {\n return (unstored ??= eventId());\n }\n}\n\n/**\n * Batched, best-effort signals and exposures: gathered for `delay`, sent in\n * one request each, flushed with keepalive when the page hides. While the\n * content model says the site records no signals (or a send is refused as\n * not_configured or unavailable) events are dropped; after `recheck` the\n * next event reads the model again, so signals resume when the site's\n * signal plane is back.\n */\nexport class Beacons {\n private signals: SignalInput[] = [];\n private exposures: ExposureInput[] = [];\n private timer: ReturnType<typeof setTimeout> | undefined;\n private offUntil = 0;\n private recorded: Promise<boolean> | undefined;\n private generation = 0;\n private hidden = false;\n private readonly pending = new Set<Promise<void>>();\n private readonly exposureContexts = new Map<string, { active: boolean; releaseRevision: number; occurredAt?: string; contexts: Map<string, Promise<Receipt | null>> }>();\n private readonly session: () => string | undefined;\n private readonly delay: number;\n private readonly recheck: number;\n\n constructor(\n private readonly client: Pick<ContentKitClient, \"discovery\" | \"kinds\"> & Partial<Pick<ContentKitClient, \"preferences\">>,\n o: BeaconOptions = {},\n ) {\n this.session = o.session ?? readerSession;\n this.delay = o.delay ?? 2000;\n this.recheck = o.recheck ?? 60_000;\n client.preferences?.subscribeIdentity(() => {\n // The old credentials can no longer be assumed available for a flush.\n this.generation++;\n clearTimeout(this.timer);\n this.timer = undefined;\n this.signals = [];\n this.exposures = [];\n this.pending.clear();\n for (const state of this.exposureContexts.values()) state.active = false;\n this.exposureContexts.clear();\n this.recorded = undefined;\n });\n if (typeof addEventListener === \"function\") {\n addEventListener(\"pagehide\", () => { this.hidden = true; void this.flush(true); });\n addEventListener(\"pageshow\", () => { this.hidden = false; });\n addEventListener(\"visibilitychange\", () => {\n this.hidden = typeof document !== \"undefined\" && document.visibilityState === \"hidden\";\n if (this.hidden) void this.flush(true);\n });\n }\n }\n\n /** A click on a listed card. Acquire its access snapshot at observation time. */\n click(content: string, a: Attribution & { version_id?: string } = {}): void {\n if (this.paused()) return;\n const generation = this.generation;\n const event = eventId(), at = new Date().toISOString();\n const { language, ...attribution } = a;\n const target = { content_id: content, version_id: a.version_id };\n this.track(this.contexts([target], generation, language).then((receipts) => {\n const receipt = receipts?.get(targetKey(target));\n if (!receipt || generation !== this.generation) return;\n this.signal({ content_id: content, type: \"click\", event_id: event, ...attribution,\n occurred_at: receipt.context?.occurred_at ?? at, activity_token: receipt.context?.token });\n }), generation);\n }\n\n /** A consumption session: all revisions use its first server access snapshot. */\n view(content: string, version?: string, a: Attribution = {}): ViewSession {\n const event = eventId(), generation = this.generation;\n const target = { content_id: content, version_id: version };\n const { language, ...attribution } = a;\n let revision = 0, occurredAt: string | undefined;\n let latest: SignalInput | undefined, receipt: Receipt | undefined;\n let started = false;\n const send = () => {\n if (!latest || !receipt || generation !== this.generation) return;\n this.signal({ ...latest, occurred_at: receipt.context?.occurred_at ?? occurredAt!, activity_token: receipt.context?.token });\n };\n return {\n update: (o) => {\n if (generation !== this.generation || this.paused()) return;\n occurredAt ??= new Date().toISOString();\n latest = { content_id: content, version_id: version, type: \"view\", event_id: event,\n revision: ++revision, occurred_at: occurredAt, ...attribution, ...o };\n if (!started) {\n started = true;\n const attempt = this.contexts([target], generation, language).then((receipts) => {\n if (generation !== this.generation) return;\n receipt = receipts?.get(targetKey(target));\n if (!receipt) { started = false; return; }\n send();\n }).catch((error: unknown) => {\n if (generation === this.generation) started = false;\n throw error;\n });\n this.track(attempt, generation);\n } else send();\n },\n };\n }\n\n /** A cumulative result-list stage; each newly observed target gets one receipt. */\n exposure(e: ExposureInput): void {\n if (this.paused() || !e.shown.length) return;\n const generation = this.generation;\n const key = `${e.render_id}\\u0000${e.stage}`;\n let state = this.exposureContexts.get(key);\n if (!state) {\n state = { active: true, releaseRevision: 0, contexts: new Map() };\n this.exposureContexts.set(key, state);\n }\n state.releaseRevision++;\n const captured = state;\n const unseen = e.shown.filter((p) => !captured.contexts.has(targetKey(p)));\n if (unseen.length) {\n const contexts = this.contexts(unseen, generation, e.language);\n for (const target of unseen) {\n const key = targetKey(target);\n const pending: Promise<Receipt | null> = contexts.then((values) => {\n const receipt = values?.get(key) ?? null;\n if (!receipt && captured.contexts.get(key) === pending) captured.contexts.delete(key);\n return receipt;\n }, (error: unknown) => {\n if (captured.contexts.get(key) === pending) captured.contexts.delete(key);\n throw error;\n });\n captured.contexts.set(key, pending);\n }\n }\n this.track(Promise.all(e.shown.map((p) => captured.contexts.get(targetKey(p))!)).then((receipts) => {\n if (generation !== this.generation || !captured.active || receipts.some((r) => !r)) return;\n const valid = receipts as Receipt[];\n if (new Set(valid.map((r) => r.enabled)).size !== 1) return;\n captured.occurredAt ??= valid.find((r) => r.context)?.context?.occurred_at ?? e.occurred_at ?? new Date().toISOString();\n const enriched = { ...e, occurred_at: captured.occurredAt,\n shown: e.shown.map((p, i) => ({ ...p, activity_token: valid[i]?.context?.token })) };\n // A later cumulative revision supersedes a queued earlier snapshot.\n const previous = this.exposures.find((x) => x.render_id === e.render_id && x.stage === e.stage);\n if (previous && (previous.revision ?? 0) > (e.revision ?? 0)) return;\n this.exposures = this.exposures.filter((x) => x.render_id !== e.render_id || x.stage !== e.stage);\n this.exposures.push(enriched);\n this.schedule();\n }), generation);\n }\n\n /** Keep receipt state when a mounted list resumes (including StrictMode replay). */\n beginExposure(render: string): void {\n for (const stage of [\"served\", \"rendered\", \"visible\"]) {\n const state = this.exposureContexts.get(`${render}\\u0000${stage}`);\n if (state) state.releaseRevision++;\n }\n }\n\n /** Retire receipt state after a list unmounts without cancelling observations. */\n endExposure(render: string): void {\n for (const stage of [\"served\", \"rendered\", \"visible\"]) {\n const key = `${render}\\u0000${stage}`, state = this.exposureContexts.get(key);\n if (!state) continue;\n const release = ++state.releaseRevision;\n queueMicrotask(() => {\n if (this.exposureContexts.get(key) === state && state.releaseRevision === release) this.exposureContexts.delete(key);\n });\n }\n }\n\n /** Sends ready observations; receipts are never acquired for the first time here. */\n async flush(keepalive = false): Promise<void> {\n clearTimeout(this.timer);\n this.timer = undefined;\n const generation = this.generation;\n await Promise.allSettled([...this.pending]);\n if (generation !== this.generation) return;\n clearTimeout(this.timer);\n this.timer = undefined;\n const signals = this.signals.splice(0), exposures = this.exposures.splice(0);\n if (this.paused() || (!signals.length && !exposures.length)) return;\n const session = this.session();\n const jobs = [\n ...packets(signals, \"signals\", session).map((packet) => ({ ...packet, send: (persist: boolean) => this.client.discovery.signals(packet.items, session, { keepalive: persist }) })),\n ...packets(exposures, \"exposures\", session).map((packet) => ({ ...packet, send: (persist: boolean) => this.client.discovery.exposures(packet.items, session, { keepalive: persist }) })),\n ];\n const done = await Promise.allSettled(jobs.map((job) => {\n const persist = keepalive && job.bytes <= KEEPALIVE_BYTES;\n const send = () => generation === this.generation ? job.send(persist) : Promise.resolve();\n // A single oversized cumulative exposure cannot be split without losing\n // history. Normal fetch is the best effort available during pagehide.\n return persist ? serializeKeepalive(send) : send();\n }));\n if (generation === this.generation && done.some((r) => r.status === \"rejected\" && unavailable(r.reason))) this.pause();\n }\n\n private async contexts(targets: readonly ActivityTarget[], generation: number, language?: string): Promise<Map<string, Receipt> | null> {\n if (this.paused() || !(await this.recording())) {\n if (generation === this.generation) this.pause();\n return null;\n }\n if (generation !== this.generation) return null;\n const unique = [...new Map(targets.map((target) => [targetKey(target), target])).values()];\n const batches = [];\n const session = this.session();\n for (let i = 0; i < unique.length; i += MAX) batches.push(this.client.discovery.activityContexts(unique.slice(i, i + MAX), session, language));\n const responses = await Promise.all(batches);\n if (generation !== this.generation) return null;\n const out = new Map<string, Receipt>();\n for (let i = 0; i < responses.length; i++) {\n const response = responses[i]!;\n if (typeof response?.enabled !== \"boolean\" || (response.enabled && !Array.isArray(response.contexts))) throw new Error(\"contentkit: invalid activity context response\");\n const contexts = new Map((response.contexts ?? []).map((context) => [targetKey(context), context]));\n for (const target of unique.slice(i * MAX, (i + 1) * MAX)) {\n const context = contexts.get(targetKey(target));\n if (response.enabled && (!context?.token || !Number.isFinite(Date.parse(context.occurred_at)))) throw new Error(\"contentkit: invalid activity context\");\n out.set(targetKey(target), { enabled: response.enabled, context: response.enabled ? context : undefined });\n }\n }\n return out;\n }\n\n private track(task: Promise<void>, generation: number): void {\n const pending = task.catch((error) => { if (generation === this.generation && unavailable(error)) this.pause(); }).finally(() => { this.pending.delete(pending); });\n this.pending.add(pending);\n }\n\n private signal(s: SignalInput): void {\n if (this.paused()) return;\n if (s.type === \"view\") this.signals = this.signals.filter((x) => x.event_id !== s.event_id);\n this.signals.push(s);\n this.schedule();\n }\n private paused(): boolean {\n if (this.offUntil > Date.now()) return true;\n this.offUntil = 0;\n return false;\n }\n private pause(): void { this.offUntil = Date.now() + this.recheck; this.recorded = undefined; }\n private recording(): Promise<boolean> {\n return (this.recorded ??= this.client.kinds().then((m) => m.signals, () => true));\n }\n private schedule(): void {\n const queued = bytes(\"signals\", this.signals, this.session()) + bytes(\"exposures\", this.exposures, this.session());\n if (this.signals.length + this.exposures.length >= MAX || queued >= FLUSH_BYTES) return void this.flush(this.hidden);\n this.timer ??= setTimeout(() => void this.flush(), this.delay);\n }\n}\n\ntype Receipt = { enabled: boolean; context?: ActivityContext };\nconst targetKey = (target: ActivityTarget) => `${target.content_id}\\u0000${target.version_id ?? \"\"}`;\nconst unavailable = (error: unknown) => [\"not_configured\", \"unavailable\", \"server_unavailable\"].includes(toContentKitError(error).code);\n"],"mappings":"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"}
1
+ {"version":3,"file":"beacons.js","names":[],"sources":["../../src/client/beacons.ts"],"sourcesContent":["import type { ContentKitClient } from \"./client.js\";\nimport { toContentKitError } from \"./errors.js\";\nimport type { ActivityContext, ActivityTarget, ExposureInput, SignalInput } from \"./generated/wire.js\";\n\nexport interface BeaconOptions {\n /** A signed-out visitor identity, persistent across this browser's tabs. */\n session?: () => string | undefined;\n /** How long events gather before they are sent, ms. Default 2000. */\n delay?: number;\n /** How long beacons stay off after the site records no signals, ms, before the content model is read again. Default 60000. */\n recheck?: number;\n}\n\n/** Where a click came from: the list render it was shown in. */\nexport interface Attribution {\n /** Language of the displayed content; defaults to the client's request language. */\n language?: string;\n render_id?: string;\n surface?: string;\n /** 1-based position in that render. */\n position?: number;\n}\n\n\nconst MAX = 100;\n// Fetch's keepalive limit includes all in-flight bodies in the fetch group.\n// Leave headroom and serialize this module's keepalive requests across clients.\nconst KEEPALIVE_BYTES = 60 * 1024;\nconst FLUSH_BYTES = 48 * 1024;\nlet keepaliveTail: Promise<void> = Promise.resolve();\nconst encoder = new TextEncoder();\nconst bytes = (field: \"signals\" | \"exposures\", rows: readonly unknown[], session?: string) => encoder.encode(JSON.stringify({ session, [field]: rows })).length;\n\nfunction packets<T>(items: readonly T[], field: \"signals\" | \"exposures\", session?: string): { items: T[]; bytes: number }[] {\n const out: { items: T[]; bytes: number }[] = [];\n let current: T[] = [];\n for (const item of items) {\n if (current.length && (current.length === MAX || bytes(field, [...current, item], session) > KEEPALIVE_BYTES)) {\n out.push({ items: current, bytes: bytes(field, current, session) });\n current = [];\n }\n current.push(item);\n }\n if (current.length) out.push({ items: current, bytes: bytes(field, current, session) });\n return out;\n}\n\nfunction serializeKeepalive(send: () => Promise<void>): Promise<void> {\n const pending = keepaliveTail.then(send, send);\n keepaliveTail = pending.then(() => {}, () => {});\n return pending;\n}\nconst SESSION_KEY = \"contentkit.visitor.v1\";\nexport const eventId = () => (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`);\n\nlet unstored: string | undefined;\n\n/** A random signed-out browser identity shared across tabs/reloads, never an account credential. */\nexport function readerSession(): string {\n try {\n const kept = localStorage.getItem(SESSION_KEY);\n if (kept) return kept;\n const made = eventId();\n localStorage.setItem(SESSION_KEY, made);\n return made;\n } catch {\n return (unstored ??= eventId());\n }\n}\n\n/**\n * Batched, best-effort signals and exposures: gathered for `delay`, sent in\n * one request each, flushed with keepalive when the page hides. While the\n * content model says the site records no signals (or a send is refused as\n * not_configured or unavailable) events are dropped; after `recheck` the\n * next event reads the model again, so signals resume when the site's\n * signal plane is back.\n */\nexport class Beacons {\n private signals: SignalInput[] = [];\n private exposures: ExposureInput[] = [];\n private timer: ReturnType<typeof setTimeout> | undefined;\n private offUntil = 0;\n private recorded: Promise<boolean> | undefined;\n private generation = 0;\n private hidden = false;\n private readonly pending = new Set<Promise<void>>();\n private readonly exposureContexts = new Map<string, { active: boolean; releaseRevision: number; occurredAt?: string; contexts: Map<string, Promise<Receipt | null>> }>();\n private readonly session: () => string | undefined;\n private readonly delay: number;\n private readonly recheck: number;\n\n constructor(\n private readonly client: Pick<ContentKitClient, \"discovery\" | \"kinds\"> & Partial<Pick<ContentKitClient, \"preferences\">>,\n o: BeaconOptions = {},\n ) {\n this.session = o.session ?? readerSession;\n this.delay = o.delay ?? 2000;\n this.recheck = o.recheck ?? 60_000;\n client.preferences?.subscribeIdentity(() => {\n // The old credentials can no longer be assumed available for a flush.\n this.generation++;\n clearTimeout(this.timer);\n this.timer = undefined;\n this.signals = [];\n this.exposures = [];\n this.pending.clear();\n for (const state of this.exposureContexts.values()) state.active = false;\n this.exposureContexts.clear();\n this.recorded = undefined;\n });\n if (typeof addEventListener === \"function\") {\n addEventListener(\"pagehide\", () => { this.hidden = true; void this.flush(true); });\n addEventListener(\"pageshow\", () => { this.hidden = false; });\n addEventListener(\"visibilitychange\", () => {\n this.hidden = typeof document !== \"undefined\" && document.visibilityState === \"hidden\";\n if (this.hidden) void this.flush(true);\n });\n }\n }\n\n /** A click on a listed card. Acquire its access snapshot at observation time. */\n click(content: string, a: Attribution & { version_id?: string } = {}): void {\n if (this.paused()) return;\n const generation = this.generation;\n const event = eventId(), at = new Date().toISOString();\n const { language, ...attribution } = a;\n const target = { content_id: content, version_id: a.version_id };\n this.track(this.contexts([target], generation, language).then((receipts) => {\n const receipt = receipts?.get(targetKey(target));\n if (!receipt || generation !== this.generation) return;\n this.signal({ content_id: content, type: \"click\", event_id: event, ...attribution,\n occurred_at: receipt.context?.occurred_at ?? at, activity_token: receipt.context?.token });\n }), generation);\n }\n\n /** A cumulative result-list stage; each newly observed target gets one receipt. */\n exposure(e: ExposureInput): void {\n if (this.paused() || !e.shown.length) return;\n const generation = this.generation;\n const key = `${e.render_id}\\u0000${e.stage}`;\n let state = this.exposureContexts.get(key);\n if (!state) {\n state = { active: true, releaseRevision: 0, contexts: new Map() };\n this.exposureContexts.set(key, state);\n }\n state.releaseRevision++;\n const captured = state;\n const unseen = e.shown.filter((p) => !captured.contexts.has(targetKey(p)));\n if (unseen.length) {\n const contexts = this.contexts(unseen, generation, e.language);\n for (const target of unseen) {\n const key = targetKey(target);\n const pending: Promise<Receipt | null> = contexts.then((values) => {\n const receipt = values?.get(key) ?? null;\n if (!receipt && captured.contexts.get(key) === pending) captured.contexts.delete(key);\n return receipt;\n }, (error: unknown) => {\n if (captured.contexts.get(key) === pending) captured.contexts.delete(key);\n throw error;\n });\n captured.contexts.set(key, pending);\n }\n }\n this.track(Promise.all(e.shown.map((p) => captured.contexts.get(targetKey(p))!)).then((receipts) => {\n if (generation !== this.generation || !captured.active || receipts.some((r) => !r)) return;\n const valid = receipts as Receipt[];\n if (new Set(valid.map((r) => r.enabled)).size !== 1) return;\n captured.occurredAt ??= valid.find((r) => r.context)?.context?.occurred_at ?? e.occurred_at ?? new Date().toISOString();\n const enriched = { ...e, occurred_at: captured.occurredAt,\n shown: e.shown.map((p, i) => ({ ...p, activity_token: valid[i]?.context?.token })) };\n // A later cumulative revision supersedes a queued earlier snapshot.\n const previous = this.exposures.find((x) => x.render_id === e.render_id && x.stage === e.stage);\n if (previous && (previous.revision ?? 0) > (e.revision ?? 0)) return;\n this.exposures = this.exposures.filter((x) => x.render_id !== e.render_id || x.stage !== e.stage);\n this.exposures.push(enriched);\n this.schedule();\n }), generation);\n }\n\n /** Keep receipt state when a mounted list resumes (including StrictMode replay). */\n beginExposure(render: string): void {\n for (const stage of [\"served\", \"rendered\", \"visible\"]) {\n const state = this.exposureContexts.get(`${render}\\u0000${stage}`);\n if (state) state.releaseRevision++;\n }\n }\n\n /** Retire receipt state after a list unmounts without cancelling observations. */\n endExposure(render: string): void {\n for (const stage of [\"served\", \"rendered\", \"visible\"]) {\n const key = `${render}\\u0000${stage}`, state = this.exposureContexts.get(key);\n if (!state) continue;\n const release = ++state.releaseRevision;\n queueMicrotask(() => {\n if (this.exposureContexts.get(key) === state && state.releaseRevision === release) this.exposureContexts.delete(key);\n });\n }\n }\n\n /** Sends ready observations; receipts are never acquired for the first time here. */\n async flush(keepalive = false): Promise<void> {\n clearTimeout(this.timer);\n this.timer = undefined;\n const generation = this.generation;\n await Promise.allSettled([...this.pending]);\n if (generation !== this.generation) return;\n clearTimeout(this.timer);\n this.timer = undefined;\n const signals = this.signals.splice(0), exposures = this.exposures.splice(0);\n if (this.paused() || (!signals.length && !exposures.length)) return;\n const session = this.session();\n const jobs = [\n ...packets(signals, \"signals\", session).map((packet) => ({ ...packet, send: (persist: boolean) => this.client.discovery.signals(packet.items, session, { keepalive: persist }) })),\n ...packets(exposures, \"exposures\", session).map((packet) => ({ ...packet, send: (persist: boolean) => this.client.discovery.exposures(packet.items, session, { keepalive: persist }) })),\n ];\n const done = await Promise.allSettled(jobs.map((job) => {\n const persist = keepalive && job.bytes <= KEEPALIVE_BYTES;\n const send = () => generation === this.generation ? job.send(persist) : Promise.resolve();\n // A single oversized cumulative exposure cannot be split without losing\n // history. Normal fetch is the best effort available during pagehide.\n return persist ? serializeKeepalive(send) : send();\n }));\n if (generation === this.generation && done.some((r) => r.status === \"rejected\" && unavailable(r.reason))) this.pause();\n }\n\n private async contexts(targets: readonly ActivityTarget[], generation: number, language?: string): Promise<Map<string, Receipt> | null> {\n if (this.paused() || !(await this.recording())) {\n if (generation === this.generation) this.pause();\n return null;\n }\n if (generation !== this.generation) return null;\n const unique = [...new Map(targets.map((target) => [targetKey(target), target])).values()];\n const batches = [];\n const session = this.session();\n for (let i = 0; i < unique.length; i += MAX) batches.push(this.client.discovery.activityContexts(unique.slice(i, i + MAX), session, language));\n const responses = await Promise.all(batches);\n if (generation !== this.generation) return null;\n const out = new Map<string, Receipt>();\n for (let i = 0; i < responses.length; i++) {\n const response = responses[i]!;\n if (typeof response?.enabled !== \"boolean\" || (response.enabled && !Array.isArray(response.contexts))) throw new Error(\"contentkit: invalid activity context response\");\n const contexts = new Map((response.contexts ?? []).map((context) => [targetKey(context), context]));\n for (const target of unique.slice(i * MAX, (i + 1) * MAX)) {\n const context = contexts.get(targetKey(target));\n if (response.enabled && (!context?.token || !Number.isFinite(Date.parse(context.occurred_at)))) throw new Error(\"contentkit: invalid activity context\");\n out.set(targetKey(target), { enabled: response.enabled, context: response.enabled ? context : undefined });\n }\n }\n return out;\n }\n\n private track(task: Promise<void>, generation: number): void {\n const pending = task.catch((error) => { if (generation === this.generation && unavailable(error)) this.pause(); }).finally(() => { this.pending.delete(pending); });\n this.pending.add(pending);\n }\n\n private signal(s: SignalInput): void {\n if (this.paused()) return;\n this.signals.push(s);\n this.schedule();\n }\n private paused(): boolean {\n if (this.offUntil > Date.now()) return true;\n this.offUntil = 0;\n return false;\n }\n private pause(): void { this.offUntil = Date.now() + this.recheck; this.recorded = undefined; }\n private recording(): Promise<boolean> {\n return (this.recorded ??= this.client.kinds().then((m) => m.signals, () => true));\n }\n private schedule(): void {\n const queued = bytes(\"signals\", this.signals, this.session()) + bytes(\"exposures\", this.exposures, this.session());\n if (this.signals.length + this.exposures.length >= MAX || queued >= FLUSH_BYTES) return void this.flush(this.hidden);\n this.timer ??= setTimeout(() => void this.flush(), this.delay);\n }\n}\n\ntype Receipt = { enabled: boolean; context?: ActivityContext };\nconst targetKey = (target: ActivityTarget) => `${target.content_id}\\u0000${target.version_id ?? \"\"}`;\nconst unavailable = (error: unknown) => [\"not_configured\", \"unavailable\", \"server_unavailable\"].includes(toContentKitError(error).code);\n"],"mappings":";;AAwBA,IAAM,IAAM,KAGN,IAAkB,OAClB,IAAc,OAChB,IAA+B,QAAQ,QAAQ,GAC7C,IAAU,IAAI,YAAY,GAC1B,KAAS,GAAgC,GAA0B,MAAqB,EAAQ,OAAO,KAAK,UAAU;CAAE;EAAU,IAAQ;AAAK,CAAC,CAAC,CAAC,CAAC;AAEzJ,SAAS,EAAW,GAAqB,GAAgC,GAAmD;CAC1H,IAAM,IAAuC,CAAC,GAC1C,IAAe,CAAC;CACpB,KAAK,IAAM,KAAQ,GAKjB,AAJI,EAAQ,WAAW,EAAQ,WAAW,KAAO,EAAM,GAAO,CAAC,GAAG,GAAS,CAAI,GAAG,CAAO,IAAI,OAC3F,EAAI,KAAK;EAAE,OAAO;EAAS,OAAO,EAAM,GAAO,GAAS,CAAO;CAAE,CAAC,GAClE,IAAU,CAAC,IAEb,EAAQ,KAAK,CAAI;CAGnB,OADI,EAAQ,UAAQ,EAAI,KAAK;EAAE,OAAO;EAAS,OAAO,EAAM,GAAO,GAAS,CAAO;CAAE,CAAC,GAC/E;AACT;AAEA,SAAS,EAAmB,GAA0C;CACpE,IAAM,IAAU,EAAc,KAAK,GAAM,CAAI;CAE7C,OADA,IAAgB,EAAQ,WAAW,CAAC,SAAS,CAAC,CAAC,GACxC;AACT;AACA,IAAM,IAAc,yBACP,UAAiB,OAAO,SAAW,OAAe,gBAAgB,SAAS,OAAO,WAAW,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,KAE1K;AAGJ,SAAgB,IAAwB;CACtC,IAAI;EACF,IAAM,IAAO,aAAa,QAAQ,CAAW;EAC7C,IAAI,GAAM,OAAO;EACjB,IAAM,IAAO,EAAQ;EAErB,OADA,aAAa,QAAQ,GAAa,CAAI,GAC/B;CACT,QAAQ;EACN,OAAQ,MAAa,EAAQ;CAC/B;AACF;AAUA,IAAa,IAAb,MAAqB;CAeA;CAdnB,UAAiC,CAAC;CAClC,YAAqC,CAAC;CACtC;CACA,WAAmB;CACnB;CACA,aAAqB;CACrB,SAAiB;CACjB,0BAA2B,IAAI,IAAmB;CAClD,mCAAoC,IAAI,IAA+H;CACvK;CACA;CACA;CAEA,YACE,GACA,IAAmB,CAAC,GACpB;EAgBA,AAlBiB,KAAA,SAAA,GAGjB,KAAK,UAAU,EAAE,WAAW,GAC5B,KAAK,QAAQ,EAAE,SAAS,KACxB,KAAK,UAAU,EAAE,WAAW,KAC5B,EAAO,aAAa,wBAAwB;GAO1C,AALA,KAAK,cACL,aAAa,KAAK,KAAK,GACvB,KAAK,QAAQ,KAAA,GACb,KAAK,UAAU,CAAC,GAChB,KAAK,YAAY,CAAC,GAClB,KAAK,QAAQ,MAAM;GACnB,KAAK,IAAM,KAAS,KAAK,iBAAiB,OAAO,GAAG,EAAM,SAAS;GAEnE,AADA,KAAK,iBAAiB,MAAM,GAC5B,KAAK,WAAW,KAAA;EAClB,CAAC,GACG,OAAO,oBAAqB,eAC9B,iBAAiB,kBAAkB;GAAsB,AAApB,KAAK,SAAS,IAAM,KAAU,MAAM,EAAI;EAAG,CAAC,GACjF,iBAAiB,kBAAkB;GAAE,KAAK,SAAS;EAAO,CAAC,GAC3D,iBAAiB,0BAA0B;GAEzC,AADA,KAAK,SAAS,OAAO,WAAa,OAAe,SAAS,oBAAoB,UAC1E,KAAK,UAAQ,KAAU,MAAM,EAAI;EACvC,CAAC;CAEL;CAGA,MAAM,GAAiB,IAA2C,CAAC,GAAS;EAC1E,IAAI,KAAK,OAAO,GAAG;EACnB,IAAM,IAAa,KAAK,YAClB,IAAQ,EAAQ,GAAG,qBAAK,IAAI,KAAK,EAAA,CAAE,YAAY,GAC/C,EAAE,aAAU,GAAG,MAAgB,GAC/B,IAAS;GAAE,YAAY;GAAS,YAAY,EAAE;EAAW;EAC/D,KAAK,MAAM,KAAK,SAAS,CAAC,CAAM,GAAG,GAAY,CAAQ,CAAC,CAAC,MAAM,MAAa;GAC1E,IAAM,IAAU,GAAU,IAAI,EAAU,CAAM,CAAC;GAC3C,AAAC,KAAW,MAAe,KAAK,cACpC,KAAK,OAAO;IAAE,YAAY;IAAS,MAAM;IAAS,UAAU;IAAO,GAAG;IACpE,aAAa,EAAQ,SAAS,eAAe;IAAI,gBAAgB,EAAQ,SAAS;GAAM,CAAC;EAC7F,CAAC,GAAG,CAAU;CAChB;CAGA,SAAS,GAAwB;EAC/B,IAAI,KAAK,OAAO,KAAK,CAAC,EAAE,MAAM,QAAQ;EACtC,IAAM,IAAa,KAAK,YAClB,IAAM,GAAG,EAAE,UAAU,QAAQ,EAAE,SACjC,IAAQ,KAAK,iBAAiB,IAAI,CAAG;EAKzC,AAJK,MACH,IAAQ;GAAE,QAAQ;GAAM,iBAAiB;GAAG,0BAAU,IAAI,IAAI;EAAE,GAChE,KAAK,iBAAiB,IAAI,GAAK,CAAK,IAEtC,EAAM;EACN,IAAM,IAAW,GACX,IAAS,EAAE,MAAM,QAAQ,MAAM,CAAC,EAAS,SAAS,IAAI,EAAU,CAAC,CAAC,CAAC;EACzE,IAAI,EAAO,QAAQ;GACjB,IAAM,IAAW,KAAK,SAAS,GAAQ,GAAY,EAAE,QAAQ;GAC7D,KAAK,IAAM,KAAU,GAAQ;IAC3B,IAAM,IAAM,EAAU,CAAM,GACtB,IAAmC,EAAS,MAAM,MAAW;KACjE,IAAM,IAAU,GAAQ,IAAI,CAAG,KAAK;KAEpC,OADI,CAAC,KAAW,EAAS,SAAS,IAAI,CAAG,MAAM,KAAS,EAAS,SAAS,OAAO,CAAG,GAC7E;IACT,IAAI,MAAmB;KAErB,MADI,EAAS,SAAS,IAAI,CAAG,MAAM,KAAS,EAAS,SAAS,OAAO,CAAG,GAClE;IACR,CAAC;IACD,EAAS,SAAS,IAAI,GAAK,CAAO;GACpC;EACF;EACA,KAAK,MAAM,QAAQ,IAAI,EAAE,MAAM,KAAK,MAAM,EAAS,SAAS,IAAI,EAAU,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,MAAM,MAAa;GAClG,IAAI,MAAe,KAAK,cAAc,CAAC,EAAS,UAAU,EAAS,MAAM,MAAM,CAAC,CAAC,GAAG;GACpF,IAAM,IAAQ;GACd,IAAI,IAAI,IAAI,EAAM,KAAK,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,GAAG;GACrD,EAAS,eAAe,EAAM,MAAM,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,eAAe,EAAE,gCAAe,IAAI,KAAK,EAAA,CAAE,YAAY;GACtH,IAAM,IAAW;IAAE,GAAG;IAAG,aAAa,EAAS;IAC7C,OAAO,EAAE,MAAM,KAAK,GAAG,OAAO;KAAE,GAAG;KAAG,gBAAgB,EAAM,EAAE,EAAE,SAAS;IAAM,EAAE;GAAE,GAE/E,IAAW,KAAK,UAAU,MAAM,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,KAAK;GAC1F,MAAa,EAAS,YAAY,MAAM,EAAE,YAAY,OAC1D,KAAK,YAAY,KAAK,UAAU,QAAQ,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,KAAK,GAChG,KAAK,UAAU,KAAK,CAAQ,GAC5B,KAAK,SAAS;EAChB,CAAC,GAAG,CAAU;CAChB;CAGA,cAAc,GAAsB;EAClC,KAAK,IAAM,KAAS;GAAC;GAAU;GAAY;EAAS,GAAG;GACrD,IAAM,IAAQ,KAAK,iBAAiB,IAAI,GAAG,EAAO,QAAQ,GAAO;GACjE,AAAI,KAAO,EAAM;EACnB;CACF;CAGA,YAAY,GAAsB;EAChC,KAAK,IAAM,KAAS;GAAC;GAAU;GAAY;EAAS,GAAG;GACrD,IAAM,IAAM,GAAG,EAAO,QAAQ,KAAS,IAAQ,KAAK,iBAAiB,IAAI,CAAG;GAC5E,IAAI,CAAC,GAAO;GACZ,IAAM,IAAU,EAAE,EAAM;GACxB,qBAAqB;IACnB,AAAI,KAAK,iBAAiB,IAAI,CAAG,MAAM,KAAS,EAAM,oBAAoB,KAAS,KAAK,iBAAiB,OAAO,CAAG;GACrH,CAAC;EACH;CACF;CAGA,MAAM,MAAM,IAAY,IAAsB;EAE5C,AADA,aAAa,KAAK,KAAK,GACvB,KAAK,QAAQ,KAAA;EACb,IAAM,IAAa,KAAK;EAExB,IADA,MAAM,QAAQ,WAAW,CAAC,GAAG,KAAK,OAAO,CAAC,GACtC,MAAe,KAAK,YAAY;EAEpC,AADA,aAAa,KAAK,KAAK,GACvB,KAAK,QAAQ,KAAA;EACb,IAAM,IAAU,KAAK,QAAQ,OAAO,CAAC,GAAG,IAAY,KAAK,UAAU,OAAO,CAAC;EAC3E,IAAI,KAAK,OAAO,KAAM,CAAC,EAAQ,UAAU,CAAC,EAAU,QAAS;EAC7D,IAAM,IAAU,KAAK,QAAQ,GACvB,IAAO,CACX,GAAG,EAAQ,GAAS,WAAW,CAAO,CAAC,CAAC,KAAK,OAAY;GAAE,GAAG;GAAQ,OAAO,MAAqB,KAAK,OAAO,UAAU,QAAQ,EAAO,OAAO,GAAS,EAAE,WAAW,EAAQ,CAAC;EAAE,EAAE,GACjL,GAAG,EAAQ,GAAW,aAAa,CAAO,CAAC,CAAC,KAAK,OAAY;GAAE,GAAG;GAAQ,OAAO,MAAqB,KAAK,OAAO,UAAU,UAAU,EAAO,OAAO,GAAS,EAAE,WAAW,EAAQ,CAAC;EAAE,EAAE,CACzL,GACM,IAAO,MAAM,QAAQ,WAAW,EAAK,KAAK,MAAQ;GACtD,IAAM,IAAU,KAAa,EAAI,SAAS,GACpC,UAAa,MAAe,KAAK,aAAa,EAAI,KAAK,CAAO,IAAI,QAAQ,QAAQ;GAGxF,OAAO,IAAU,EAAmB,CAAI,IAAI,EAAK;EACnD,CAAC,CAAC;EACF,AAAI,MAAe,KAAK,cAAc,EAAK,MAAM,MAAM,EAAE,WAAW,cAAc,EAAY,EAAE,MAAM,CAAC,KAAG,KAAK,MAAM;CACvH;CAEA,MAAc,SAAS,GAAoC,GAAoB,GAAyD;EACtI,IAAI,KAAK,OAAO,KAAK,CAAE,MAAM,KAAK,UAAU,GAE1C,OADI,MAAe,KAAK,cAAY,KAAK,MAAM,GACxC;EAET,IAAI,MAAe,KAAK,YAAY,OAAO;EAC3C,IAAM,IAAS,CAAC,GAAG,IAAI,IAAI,EAAQ,KAAK,MAAW,CAAC,EAAU,CAAM,GAAG,CAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GACnF,IAAU,CAAC,GACX,IAAU,KAAK,QAAQ;EAC7B,KAAK,IAAI,IAAI,GAAG,IAAI,EAAO,QAAQ,KAAK,GAAK,EAAQ,KAAK,KAAK,OAAO,UAAU,iBAAiB,EAAO,MAAM,GAAG,IAAI,CAAG,GAAG,GAAS,CAAQ,CAAC;EAC7I,IAAM,IAAY,MAAM,QAAQ,IAAI,CAAO;EAC3C,IAAI,MAAe,KAAK,YAAY,OAAO;EAC3C,IAAM,oBAAM,IAAI,IAAqB;EACrC,KAAK,IAAI,IAAI,GAAG,IAAI,EAAU,QAAQ,KAAK;GACzC,IAAM,IAAW,EAAU;GAC3B,IAAI,OAAO,GAAU,WAAY,aAAc,EAAS,WAAW,CAAC,MAAM,QAAQ,EAAS,QAAQ,GAAI,MAAU,MAAM,+CAA+C;GACtK,IAAM,IAAW,IAAI,KAAK,EAAS,YAAY,CAAC,EAAA,CAAG,KAAK,MAAY,CAAC,EAAU,CAAO,GAAG,CAAO,CAAC,CAAC;GAClG,KAAK,IAAM,KAAU,EAAO,MAAM,IAAI,IAAM,IAAI,KAAK,CAAG,GAAG;IACzD,IAAM,IAAU,EAAS,IAAI,EAAU,CAAM,CAAC;IAC9C,IAAI,EAAS,YAAY,CAAC,GAAS,SAAS,CAAC,OAAO,SAAS,KAAK,MAAM,EAAQ,WAAW,CAAC,IAAI,MAAU,MAAM,sCAAsC;IACtJ,EAAI,IAAI,EAAU,CAAM,GAAG;KAAE,SAAS,EAAS;KAAS,SAAS,EAAS,UAAU,IAAU,KAAA;IAAU,CAAC;GAC3G;EACF;EACA,OAAO;CACT;CAEA,MAAc,GAAqB,GAA0B;EAC3D,IAAM,IAAU,EAAK,OAAO,MAAU;GAAE,AAAI,MAAe,KAAK,cAAc,EAAY,CAAK,KAAG,KAAK,MAAM;EAAG,CAAC,CAAC,CAAC,cAAc;GAAE,KAAK,QAAQ,OAAO,CAAO;EAAG,CAAC;EAClK,KAAK,QAAQ,IAAI,CAAO;CAC1B;CAEA,OAAe,GAAsB;EAC/B,KAAK,OAAO,MAChB,KAAK,QAAQ,KAAK,CAAC,GACnB,KAAK,SAAS;CAChB;CACA,SAA0B;EAGxB,OAFI,KAAK,WAAW,KAAK,IAAI,MAC7B,KAAK,WAAW,GACT;CACT;CACA,QAAsB;EAA6C,AAA3C,KAAK,WAAW,KAAK,IAAI,IAAI,KAAK,SAAS,KAAK,WAAW,KAAA;CAAW;CAC9F,YAAsC;EACpC,OAAQ,KAAK,aAAa,KAAK,OAAO,MAAM,CAAC,CAAC,MAAM,MAAM,EAAE,eAAe,EAAI;CACjF;CACA,WAAyB;EACvB,IAAM,IAAS,EAAM,WAAW,KAAK,SAAS,KAAK,QAAQ,CAAC,IAAI,EAAM,aAAa,KAAK,WAAW,KAAK,QAAQ,CAAC;EACjH,IAAI,KAAK,QAAQ,SAAS,KAAK,UAAU,UAAU,KAAO,KAAU,GAAa,OAAO,KAAK,KAAK,MAAM,KAAK,MAAM;EACnH,KAAK,UAAU,iBAAiB,KAAK,KAAK,MAAM,GAAG,KAAK,KAAK;CAC/D;AACF,GAGM,KAAa,MAA2B,GAAG,EAAO,WAAW,QAAQ,EAAO,cAAc,MAC1F,KAAe,MAAmB;CAAC;CAAkB;CAAe;AAAoB,CAAC,CAAC,SAAS,EAAkB,CAAK,CAAC,CAAC,IAAI"}
@@ -1,3 +1,4 @@
1
+ import { GuestReadingHistory } from './guest-reading-history.js';
1
2
  import { CatalogClient } from './catalog.js';
2
3
  import { CodesClient } from './codes.js';
3
4
  import { ContentsClient } from './contents.js';
@@ -62,6 +63,8 @@ export interface ContentKitClient {
62
63
  readonly notifications: NotificationsClient;
63
64
  /** Browser guest preferences and explicit registration inheritance. */
64
65
  readonly preferences: ReaderPreferencesClient;
66
+ /** Browser-owned guest reading progress, isolated to this catalog mount. */
67
+ readonly guestHistory: GuestReadingHistory;
65
68
  /** What the content module allows: which interactions signed-out visitors may make. */
66
69
  config(signal?: AbortSignal): Promise<Config>;
67
70
  /** The site's content model: languages, tiers, kinds, group kinds and content filters. */
@@ -1,52 +1,54 @@
1
1
  import { call as e } from "./route.js";
2
2
  import { MediaClient as t } from "./media/client.js";
3
- import { CatalogClient as n } from "./catalog.js";
4
- import { CodesClient as r } from "./codes.js";
5
- import { ContentsClient as i } from "./contents.js";
6
- import { BansClient as a } from "./content/bans.js";
7
- import { CommentsClient as o } from "./content/comments.js";
8
- import { DiscoveryClient as s } from "./discovery.js";
9
- import { FavoritesClient as c } from "./content/favorites.js";
10
- import { ModerationClient as l } from "./content/moderation.js";
11
- import { PollsClient as u } from "./content/polls.js";
12
- import { ReactionsClient as d } from "./content/reactions.js";
13
- import { EntitiesClient as f } from "./entities.js";
14
- import { OffersClient as p } from "./offers.js";
15
- import { StatsClient as m } from "./stats.js";
16
- import { VersionsClient as h } from "./versions.js";
17
- import { Http as g } from "./http.js";
18
- import { NotificationsClient as _ } from "./notifications.js";
19
- import { ReaderPreferencesClient as v } from "./reader-preferences.js";
3
+ import { guestReadingHistory as n } from "./guest-reading-history.js";
4
+ import { CatalogClient as r } from "./catalog.js";
5
+ import { CodesClient as i } from "./codes.js";
6
+ import { ContentsClient as a } from "./contents.js";
7
+ import { BansClient as o } from "./content/bans.js";
8
+ import { CommentsClient as s } from "./content/comments.js";
9
+ import { DiscoveryClient as c } from "./discovery.js";
10
+ import { FavoritesClient as l } from "./content/favorites.js";
11
+ import { ModerationClient as u } from "./content/moderation.js";
12
+ import { PollsClient as d } from "./content/polls.js";
13
+ import { ReactionsClient as f } from "./content/reactions.js";
14
+ import { EntitiesClient as p } from "./entities.js";
15
+ import { OffersClient as m } from "./offers.js";
16
+ import { StatsClient as h } from "./stats.js";
17
+ import { VersionsClient as g } from "./versions.js";
18
+ import { Http as _ } from "./http.js";
19
+ import { NotificationsClient as v } from "./notifications.js";
20
+ import { ReaderPreferencesClient as y } from "./reader-preferences.js";
20
21
  //#region src/client/client.ts
21
- function y(y) {
22
- let { media: b, folders: x, ...S } = y, C = new g(S), w = new t(C, b, x);
22
+ function b(b) {
23
+ let { media: x, folders: S, ...C } = b, w = new _(C), T = new t(w, x, S);
23
24
  return {
24
- media: w,
25
- contents: new i(C),
26
- catalog: new n(C),
27
- comments: new o(C),
28
- reactions: new d(C),
29
- favorites: new c(C),
30
- polls: new u(C, w),
31
- bans: new a(C),
32
- moderation: new l(C),
33
- entities: new f(C, w),
34
- discovery: new s(C),
35
- versions: new h(C),
36
- offers: new p(C),
37
- codes: new r(C),
38
- stats: new m(C),
39
- notifications: new _(C),
40
- preferences: new v(C),
41
- config: (t) => e(C, "GET", "/config", { signal: t }),
42
- kinds: (t) => e(C, "GET", "/kinds", { signal: t }),
43
- import: (t) => e(C, "POST", "/import", { body: t }),
44
- url: (e, t) => C.mount(e, t),
45
- setReadScope: (e) => C.setReadScope(e),
46
- subscribe: (e) => C.subscribe(e)
25
+ media: T,
26
+ contents: new a(w),
27
+ catalog: new r(w),
28
+ comments: new s(w),
29
+ reactions: new f(w),
30
+ favorites: new l(w),
31
+ polls: new d(w, T),
32
+ bans: new o(w),
33
+ moderation: new u(w),
34
+ entities: new p(w, T),
35
+ discovery: new c(w),
36
+ versions: new g(w),
37
+ offers: new m(w),
38
+ codes: new i(w),
39
+ stats: new h(w),
40
+ notifications: new v(w),
41
+ preferences: new y(w),
42
+ guestHistory: n(w.mount("discovery")),
43
+ config: (t) => e(w, "GET", "/config", { signal: t }),
44
+ kinds: (t) => e(w, "GET", "/kinds", { signal: t }),
45
+ import: (t) => e(w, "POST", "/import", { body: t }),
46
+ url: (e, t) => w.mount(e, t),
47
+ setReadScope: (e) => w.setReadScope(e),
48
+ subscribe: (e) => w.subscribe(e)
47
49
  };
48
50
  }
49
51
  //#endregion
50
- export { y as createContentKitClient };
52
+ export { b as createContentKitClient };
51
53
 
52
54
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":["import { CatalogClient } from \"./catalog.js\";\nimport { CodesClient } from \"./codes.js\";\nimport { ContentsClient } from \"./contents.js\";\nimport { BansClient } from \"./content/bans.js\";\nimport { CommentsClient } from \"./content/comments.js\";\nimport { FavoritesClient } from \"./content/favorites.js\";\nimport { ModerationClient } from \"./content/moderation.js\";\nimport { PollsClient } from \"./content/polls.js\";\nimport { ReactionsClient } from \"./content/reactions.js\";\nimport type { Batch, Config, ContentModel, RefBody, Result } from \"./generated/wire.js\";\nimport { DiscoveryClient } from \"./discovery.js\";\nimport { EntitiesClient } from \"./entities.js\";\nimport { OffersClient } from \"./offers.js\";\nimport { StatsClient } from \"./stats.js\";\nimport { VersionsClient } from \"./versions.js\";\nimport { Http, type ContentKitChange, type ContentKitModule, type HttpOptions } from \"./http.js\";\nimport { MediaClient, type ContentFolders, type MediaOptions } from \"./media/client.js\";\nimport { call } from \"./route.js\";\nimport { NotificationsClient } from \"./notifications.js\";\nimport { ReaderPreferencesClient } from \"./reader-preferences.js\";\n\nexport interface ContentKitClientOptions extends HttpOptions {\n /** Upload tuning (transport, part concurrency, retries). */\n media?: MediaOptions;\n /** The media kind of poll folders, when the host renamed it (content.Media). */\n folders?: ContentFolders;\n}\n\n/**\n * One ContentKit deployment as the browser sees it. Each module is a property\n * over one shared transport (mounts, auth, language, errors, change events).\n */\nexport interface ContentKitClient {\n /** Uploads, multipart, commits, reads, frames, HLS bases, the HLS loader and inline images. */\n readonly media: MediaClient;\n /** Content items: reads, and writes for their creators, owner-group editors and staff. */\n readonly contents: ContentsClient;\n /** The catalog's totals by kind, language and audience. */\n readonly catalog: CatalogClient;\n /** Threads, replies, edits, tombstones, reactions, standing, the latest feed, moderation. */\n readonly comments: CommentsClient;\n /** Likes and dislikes of an item. */\n readonly reactions: ReactionsClient;\n /** The signed-in caller's favorites. */\n readonly favorites: FavoritesClient;\n /** Polls: votes, free-text answers and the staff editor. */\n readonly polls: PollsClient;\n /** Comment bans, owner and global. */\n readonly bans: BansClient;\n /** The review queue of held comments and content. */\n readonly moderation: ModerationClient;\n /** Groups: listings, pages, staff writes and memberships. */\n readonly entities: EntitiesClient;\n /** Lists, search, typeahead, rails, release dates, content filters, history, signals and exposures. */\n readonly discovery: DiscoveryClient;\n /** Content versions: reader plans, reader metadata, forks and downloads. */\n readonly versions: VersionsClient;\n /** Offers on contents, versions and groups; group joins; the reader's library. */\n readonly offers: OffersClient;\n /** Content-code lookup. */\n readonly codes: CodesClient;\n /** Staff activity and rankings; the signed-in reader's own statistics. */\n readonly stats: StatsClient;\n /** The signed-in person's native and imported notifications. */\n readonly notifications: NotificationsClient;\n /** Browser guest preferences and explicit registration inheritance. */\n readonly preferences: ReaderPreferencesClient;\n\n /** What the content module allows: which interactions signed-out visitors may make. */\n config(signal?: AbortSignal): Promise<Config>;\n /** The site's content model: languages, tiers, kinds, group kinds and content filters. */\n kinds(signal?: AbortSignal): Promise<ContentModel>;\n /** Staff bulk import (root:import:write): groups, contents, memberships, offers and aliases, idempotent per source. */\n import(batch: Batch): Promise<Result>;\n /** A module's base URL as configured (no trailing slash). */\n url(module: ContentKitModule, ref?: RefBody): string;\n /** Cache context only, not authentication. The React provider keeps this in sync. */\n setReadScope(scope: string): void;\n /** Every successful mutation; a null scope means its data predates a context change. */\n subscribe(listener: (change: ContentKitChange, scope: string | null) => void): () => void;\n}\n\nexport function createContentKitClient(options: ContentKitClientOptions): ContentKitClient {\n const { media: mediaOptions, folders, ...http } = options;\n const h = new Http(http);\n const media = new MediaClient(h, mediaOptions, folders);\n return {\n media,\n contents: new ContentsClient(h),\n catalog: new CatalogClient(h),\n comments: new CommentsClient(h),\n reactions: new ReactionsClient(h),\n favorites: new FavoritesClient(h),\n polls: new PollsClient(h, media),\n bans: new BansClient(h),\n moderation: new ModerationClient(h),\n entities: new EntitiesClient(h, media),\n discovery: new DiscoveryClient(h),\n versions: new VersionsClient(h),\n offers: new OffersClient(h),\n codes: new CodesClient(h),\n stats: new StatsClient(h),\n notifications: new NotificationsClient(h),\n preferences: new ReaderPreferencesClient(h),\n config: (signal) => call(h, \"GET\", \"/config\", { signal }),\n kinds: (signal) => call(h, \"GET\", \"/kinds\", { signal }),\n import: (batch) => call(h, \"POST\", \"/import\", { body: batch }),\n url: (module, ref) => h.mount(module, ref),\n setReadScope: (scope) => h.setReadScope(scope),\n subscribe: (listener) => h.subscribe(listener),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAkFA,SAAgB,EAAuB,GAAoD;CACzF,IAAM,EAAE,OAAO,GAAc,YAAS,GAAG,MAAS,GAC5C,IAAI,IAAI,EAAK,CAAI,GACjB,IAAQ,IAAI,EAAY,GAAG,GAAc,CAAO;CACtD,OAAO;EACL;EACA,UAAU,IAAI,EAAe,CAAC;EAC9B,SAAS,IAAI,EAAc,CAAC;EAC5B,UAAU,IAAI,EAAe,CAAC;EAC9B,WAAW,IAAI,EAAgB,CAAC;EAChC,WAAW,IAAI,EAAgB,CAAC;EAChC,OAAO,IAAI,EAAY,GAAG,CAAK;EAC/B,MAAM,IAAI,EAAW,CAAC;EACtB,YAAY,IAAI,EAAiB,CAAC;EAClC,UAAU,IAAI,EAAe,GAAG,CAAK;EACrC,WAAW,IAAI,EAAgB,CAAC;EAChC,UAAU,IAAI,EAAe,CAAC;EAC9B,QAAQ,IAAI,EAAa,CAAC;EAC1B,OAAO,IAAI,EAAY,CAAC;EACxB,OAAO,IAAI,EAAY,CAAC;EACxB,eAAe,IAAI,EAAoB,CAAC;EACxC,aAAa,IAAI,EAAwB,CAAC;EAC1C,SAAS,MAAW,EAAK,GAAG,OAAO,WAAW,EAAE,UAAO,CAAC;EACxD,QAAQ,MAAW,EAAK,GAAG,OAAO,UAAU,EAAE,UAAO,CAAC;EACtD,SAAS,MAAU,EAAK,GAAG,QAAQ,WAAW,EAAE,MAAM,EAAM,CAAC;EAC7D,MAAM,GAAQ,MAAQ,EAAE,MAAM,GAAQ,CAAG;EACzC,eAAe,MAAU,EAAE,aAAa,CAAK;EAC7C,YAAY,MAAa,EAAE,UAAU,CAAQ;CAC/C;AACF"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":["import { guestReadingHistory, type GuestReadingHistory } from \"./guest-reading-history.js\";\nimport { CatalogClient } from \"./catalog.js\";\nimport { CodesClient } from \"./codes.js\";\nimport { ContentsClient } from \"./contents.js\";\nimport { BansClient } from \"./content/bans.js\";\nimport { CommentsClient } from \"./content/comments.js\";\nimport { FavoritesClient } from \"./content/favorites.js\";\nimport { ModerationClient } from \"./content/moderation.js\";\nimport { PollsClient } from \"./content/polls.js\";\nimport { ReactionsClient } from \"./content/reactions.js\";\nimport type { Batch, Config, ContentModel, RefBody, Result } from \"./generated/wire.js\";\nimport { DiscoveryClient } from \"./discovery.js\";\nimport { EntitiesClient } from \"./entities.js\";\nimport { OffersClient } from \"./offers.js\";\nimport { StatsClient } from \"./stats.js\";\nimport { VersionsClient } from \"./versions.js\";\nimport { Http, type ContentKitChange, type ContentKitModule, type HttpOptions } from \"./http.js\";\nimport { MediaClient, type ContentFolders, type MediaOptions } from \"./media/client.js\";\nimport { call } from \"./route.js\";\nimport { NotificationsClient } from \"./notifications.js\";\nimport { ReaderPreferencesClient } from \"./reader-preferences.js\";\n\nexport interface ContentKitClientOptions extends HttpOptions {\n /** Upload tuning (transport, part concurrency, retries). */\n media?: MediaOptions;\n /** The media kind of poll folders, when the host renamed it (content.Media). */\n folders?: ContentFolders;\n}\n\n/**\n * One ContentKit deployment as the browser sees it. Each module is a property\n * over one shared transport (mounts, auth, language, errors, change events).\n */\nexport interface ContentKitClient {\n /** Uploads, multipart, commits, reads, frames, HLS bases, the HLS loader and inline images. */\n readonly media: MediaClient;\n /** Content items: reads, and writes for their creators, owner-group editors and staff. */\n readonly contents: ContentsClient;\n /** The catalog's totals by kind, language and audience. */\n readonly catalog: CatalogClient;\n /** Threads, replies, edits, tombstones, reactions, standing, the latest feed, moderation. */\n readonly comments: CommentsClient;\n /** Likes and dislikes of an item. */\n readonly reactions: ReactionsClient;\n /** The signed-in caller's favorites. */\n readonly favorites: FavoritesClient;\n /** Polls: votes, free-text answers and the staff editor. */\n readonly polls: PollsClient;\n /** Comment bans, owner and global. */\n readonly bans: BansClient;\n /** The review queue of held comments and content. */\n readonly moderation: ModerationClient;\n /** Groups: listings, pages, staff writes and memberships. */\n readonly entities: EntitiesClient;\n /** Lists, search, typeahead, rails, release dates, content filters, history, signals and exposures. */\n readonly discovery: DiscoveryClient;\n /** Content versions: reader plans, reader metadata, forks and downloads. */\n readonly versions: VersionsClient;\n /** Offers on contents, versions and groups; group joins; the reader's library. */\n readonly offers: OffersClient;\n /** Content-code lookup. */\n readonly codes: CodesClient;\n /** Staff activity and rankings; the signed-in reader's own statistics. */\n readonly stats: StatsClient;\n /** The signed-in person's native and imported notifications. */\n readonly notifications: NotificationsClient;\n /** Browser guest preferences and explicit registration inheritance. */\n readonly preferences: ReaderPreferencesClient;\n /** Browser-owned guest reading progress, isolated to this catalog mount. */\n readonly guestHistory: GuestReadingHistory;\n\n /** What the content module allows: which interactions signed-out visitors may make. */\n config(signal?: AbortSignal): Promise<Config>;\n /** The site's content model: languages, tiers, kinds, group kinds and content filters. */\n kinds(signal?: AbortSignal): Promise<ContentModel>;\n /** Staff bulk import (root:import:write): groups, contents, memberships, offers and aliases, idempotent per source. */\n import(batch: Batch): Promise<Result>;\n /** A module's base URL as configured (no trailing slash). */\n url(module: ContentKitModule, ref?: RefBody): string;\n /** Cache context only, not authentication. The React provider keeps this in sync. */\n setReadScope(scope: string): void;\n /** Every successful mutation; a null scope means its data predates a context change. */\n subscribe(listener: (change: ContentKitChange, scope: string | null) => void): () => void;\n}\n\nexport function createContentKitClient(options: ContentKitClientOptions): ContentKitClient {\n const { media: mediaOptions, folders, ...http } = options;\n const h = new Http(http);\n const media = new MediaClient(h, mediaOptions, folders);\n return {\n media,\n contents: new ContentsClient(h),\n catalog: new CatalogClient(h),\n comments: new CommentsClient(h),\n reactions: new ReactionsClient(h),\n favorites: new FavoritesClient(h),\n polls: new PollsClient(h, media),\n bans: new BansClient(h),\n moderation: new ModerationClient(h),\n entities: new EntitiesClient(h, media),\n discovery: new DiscoveryClient(h),\n versions: new VersionsClient(h),\n offers: new OffersClient(h),\n codes: new CodesClient(h),\n stats: new StatsClient(h),\n notifications: new NotificationsClient(h),\n preferences: new ReaderPreferencesClient(h),\n guestHistory: guestReadingHistory(h.mount(\"discovery\")),\n config: (signal) => call(h, \"GET\", \"/config\", { signal }),\n kinds: (signal) => call(h, \"GET\", \"/kinds\", { signal }),\n import: (batch) => call(h, \"POST\", \"/import\", { body: batch }),\n url: (module, ref) => h.mount(module, ref),\n setReadScope: (scope) => h.setReadScope(scope),\n subscribe: (listener) => h.subscribe(listener),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqFA,SAAgB,EAAuB,GAAoD;CACzF,IAAM,EAAE,OAAO,GAAc,YAAS,GAAG,MAAS,GAC5C,IAAI,IAAI,EAAK,CAAI,GACjB,IAAQ,IAAI,EAAY,GAAG,GAAc,CAAO;CACtD,OAAO;EACL;EACA,UAAU,IAAI,EAAe,CAAC;EAC9B,SAAS,IAAI,EAAc,CAAC;EAC5B,UAAU,IAAI,EAAe,CAAC;EAC9B,WAAW,IAAI,EAAgB,CAAC;EAChC,WAAW,IAAI,EAAgB,CAAC;EAChC,OAAO,IAAI,EAAY,GAAG,CAAK;EAC/B,MAAM,IAAI,EAAW,CAAC;EACtB,YAAY,IAAI,EAAiB,CAAC;EAClC,UAAU,IAAI,EAAe,GAAG,CAAK;EACrC,WAAW,IAAI,EAAgB,CAAC;EAChC,UAAU,IAAI,EAAe,CAAC;EAC9B,QAAQ,IAAI,EAAa,CAAC;EAC1B,OAAO,IAAI,EAAY,CAAC;EACxB,OAAO,IAAI,EAAY,CAAC;EACxB,eAAe,IAAI,EAAoB,CAAC;EACxC,aAAa,IAAI,EAAwB,CAAC;EAC1C,cAAc,EAAoB,EAAE,MAAM,WAAW,CAAC;EACtD,SAAS,MAAW,EAAK,GAAG,OAAO,WAAW,EAAE,UAAO,CAAC;EACxD,QAAQ,MAAW,EAAK,GAAG,OAAO,UAAU,EAAE,UAAO,CAAC;EACtD,SAAS,MAAU,EAAK,GAAG,QAAQ,WAAW,EAAE,MAAM,EAAM,CAAC;EAC7D,MAAM,GAAQ,MAAQ,EAAE,MAAM,GAAQ,CAAG;EACzC,eAAe,MAAU,EAAE,aAAa,CAAK;EAC7C,YAAY,MAAa,EAAE,UAAU,CAAQ;CAC/C;AACF"}
@@ -15,6 +15,8 @@ export interface MineQuery {
15
15
  language?: string;
16
16
  display_language?: string;
17
17
  mode?: AccessMode;
18
+ /** Counts for the caller's matching content selection, on the first page. */
19
+ summary?: boolean;
18
20
  }
19
21
  /** Favorites: the target's count for everyone, the signed-in caller's own. */
20
22
  export declare class FavoritesClient {
@@ -1 +1 @@
1
- {"version":3,"file":"favorites.js","names":[],"sources":["../../../src/client/content/favorites.ts"],"sourcesContent":["import type { FavoriteState, InteractionPage } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { query, type AccessMode } from \"../discovery.js\";\nimport { call } from \"../route.js\";\nimport type { Target } from \"./types.js\";\n\n/** A page of the caller's favorites or reactions. */\nexport interface MineQuery {\n /** contents (the default) or groups. */\n target?: \"contents\" | \"entities\";\n /** One content kind or group kind. */\n kind?: string;\n /** The group kinds each card names; every kind with a page by default. */\n card_entities?: readonly string[];\n cursor?: string;\n limit?: number;\n language?: string;\n display_language?: string;\n mode?: AccessMode;\n}\n\n/** Favorites: the target's count for everyone, the signed-in caller's own. */\nexport class FavoritesClient {\n constructor(private readonly http: Http) {}\n\n /** The target's favorite count and whether the caller favorited it (never, signed out). */\n get(t: Target, signal?: AbortSignal): Promise<FavoriteState> {\n const o = { params: { id: t.id }, signal };\n return t.type === \"entities\" ? call(this.http, \"GET\", \"/entities/{id}/favorite\", o) : call(this.http, \"GET\", \"/contents/{id}/favorite\", { ...o, query: { version: t.version } });\n }\n /** Favorites or unfavorites the target (signed in); repeating is a no-op. Resolves with the new state and count. */\n async set(t: Target, favorited: boolean): Promise<FavoriteState> {\n const emit = this.http.captureChanges();\n const method = favorited ? \"POST\" : \"DELETE\";\n const o = { params: { id: t.id } };\n const state: FavoriteState =\n t.type === \"entities\" ? await call(this.http, method, \"/entities/{id}/favorite\", o) : await call(this.http, method, \"/contents/{id}/favorite\", { ...o, query: { version: t.version } });\n emit({ type: \"favorite.changed\", target: t, favorited: state.favorited, count: state.count });\n return state;\n }\n /** The caller's favorites, most recent first: content cards or groups. */\n mine(q: MineQuery = {}, signal?: AbortSignal): Promise<InteractionPage> {\n return call(this.http, \"GET\", \"/me/favorites\", { query: query(q) as never, signal });\n }\n}\n"],"mappings":";;;AAsBA,IAAa,IAAb,MAA6B;CACE;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,IAAI,GAAW,GAA8C;EAC3D,IAAM,IAAI;GAAE,QAAQ,EAAE,IAAI,EAAE,GAAG;GAAG;EAAO;EACzC,OAAO,EAAE,SAAS,aAAa,EAAK,KAAK,MAAM,OAAO,2BAA2B,CAAC,IAAI,EAAK,KAAK,MAAM,OAAO,2BAA2B;GAAE,GAAG;GAAG,OAAO,EAAE,SAAS,EAAE,QAAQ;EAAE,CAAC;CACjL;CAEA,MAAM,IAAI,GAAW,GAA4C;EAC/D,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAS,IAAY,SAAS,UAC9B,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,GAC3B,IACJ,EAAE,SAAS,aAAa,MAAM,EAAK,KAAK,MAAM,GAAQ,2BAA2B,CAAC,IAAI,MAAM,EAAK,KAAK,MAAM,GAAQ,2BAA2B;GAAE,GAAG;GAAG,OAAO,EAAE,SAAS,EAAE,QAAQ;EAAE,CAAC;EAExL,OADA,EAAK;GAAE,MAAM;GAAoB,QAAQ;GAAG,WAAW,EAAM;GAAW,OAAO,EAAM;EAAM,CAAC,GACrF;CACT;CAEA,KAAK,IAAe,CAAC,GAAG,GAAgD;EACtE,OAAO,EAAK,KAAK,MAAM,OAAO,iBAAiB;GAAE,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;CACrF;AACF"}
1
+ {"version":3,"file":"favorites.js","names":[],"sources":["../../../src/client/content/favorites.ts"],"sourcesContent":["import type { FavoriteState, InteractionPage } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { query, type AccessMode } from \"../discovery.js\";\nimport { call } from \"../route.js\";\nimport type { Target } from \"./types.js\";\n\n/** A page of the caller's favorites or reactions. */\nexport interface MineQuery {\n /** contents (the default) or groups. */\n target?: \"contents\" | \"entities\";\n /** One content kind or group kind. */\n kind?: string;\n /** The group kinds each card names; every kind with a page by default. */\n card_entities?: readonly string[];\n cursor?: string;\n limit?: number;\n language?: string;\n display_language?: string;\n mode?: AccessMode;\n /** Counts for the caller's matching content selection, on the first page. */\n summary?: boolean;\n}\n\n/** Favorites: the target's count for everyone, the signed-in caller's own. */\nexport class FavoritesClient {\n constructor(private readonly http: Http) {}\n\n /** The target's favorite count and whether the caller favorited it (never, signed out). */\n get(t: Target, signal?: AbortSignal): Promise<FavoriteState> {\n const o = { params: { id: t.id }, signal };\n return t.type === \"entities\" ? call(this.http, \"GET\", \"/entities/{id}/favorite\", o) : call(this.http, \"GET\", \"/contents/{id}/favorite\", { ...o, query: { version: t.version } });\n }\n /** Favorites or unfavorites the target (signed in); repeating is a no-op. Resolves with the new state and count. */\n async set(t: Target, favorited: boolean): Promise<FavoriteState> {\n const emit = this.http.captureChanges();\n const method = favorited ? \"POST\" : \"DELETE\";\n const o = { params: { id: t.id } };\n const state: FavoriteState =\n t.type === \"entities\" ? await call(this.http, method, \"/entities/{id}/favorite\", o) : await call(this.http, method, \"/contents/{id}/favorite\", { ...o, query: { version: t.version } });\n emit({ type: \"favorite.changed\", target: t, favorited: state.favorited, count: state.count });\n return state;\n }\n /** The caller's favorites, most recent first: content cards or groups. */\n mine(q: MineQuery = {}, signal?: AbortSignal): Promise<InteractionPage> {\n return call(this.http, \"GET\", \"/me/favorites\", { query: query(q) as never, signal });\n }\n}\n"],"mappings":";;;AAwBA,IAAa,IAAb,MAA6B;CACE;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,IAAI,GAAW,GAA8C;EAC3D,IAAM,IAAI;GAAE,QAAQ,EAAE,IAAI,EAAE,GAAG;GAAG;EAAO;EACzC,OAAO,EAAE,SAAS,aAAa,EAAK,KAAK,MAAM,OAAO,2BAA2B,CAAC,IAAI,EAAK,KAAK,MAAM,OAAO,2BAA2B;GAAE,GAAG;GAAG,OAAO,EAAE,SAAS,EAAE,QAAQ;EAAE,CAAC;CACjL;CAEA,MAAM,IAAI,GAAW,GAA4C;EAC/D,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAS,IAAY,SAAS,UAC9B,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,GAC3B,IACJ,EAAE,SAAS,aAAa,MAAM,EAAK,KAAK,MAAM,GAAQ,2BAA2B,CAAC,IAAI,MAAM,EAAK,KAAK,MAAM,GAAQ,2BAA2B;GAAE,GAAG;GAAG,OAAO,EAAE,SAAS,EAAE,QAAQ;EAAE,CAAC;EAExL,OADA,EAAK;GAAE,MAAM;GAAoB,QAAQ;GAAG,WAAW,EAAM;GAAW,OAAO,EAAM;EAAM,CAAC,GACrF;CACT;CAEA,KAAK,IAAe,CAAC,GAAG,GAAgD;EACtE,OAAO,EAAK,KAAK,MAAM,OAAO,iBAAiB;GAAE,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;CACrF;AACF"}
@@ -114,6 +114,9 @@ export type ContentChange =
114
114
  type: "contentFilters.changed";
115
115
  } | {
116
116
  type: "history.changed";
117
+ } | {
118
+ type: "engagement.changed";
119
+ ids: string[];
117
120
  }
118
121
  /** A search ran or recent searches were cleared. */
119
122
  | {