@openrails/contentkit-ui 0.97.1 → 0.98.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 (41) hide show
  1. package/README.md +57 -4
  2. package/dist/client/beacons.d.ts +17 -7
  3. package/dist/client/beacons.js +205 -41
  4. package/dist/client/beacons.js.map +1 -1
  5. package/dist/client/browser-reader-preferences.d.ts +15 -2
  6. package/dist/client/browser-reader-preferences.js +67 -30
  7. package/dist/client/browser-reader-preferences.js.map +1 -1
  8. package/dist/client/discovery.d.ts +13 -3
  9. package/dist/client/discovery.js +31 -16
  10. package/dist/client/discovery.js.map +1 -1
  11. package/dist/client/generated/routes.d.ts +14 -1
  12. package/dist/client/generated/routes.js +18 -0
  13. package/dist/client/generated/routes.js.map +1 -1
  14. package/dist/client/generated/wire.d.ts +47 -4
  15. package/dist/client/generated/wire.js.map +1 -1
  16. package/dist/client/http.d.ts +7 -3
  17. package/dist/client/http.js +26 -7
  18. package/dist/client/http.js.map +1 -1
  19. package/dist/client/index.d.ts +2 -2
  20. package/dist/client/reader-preferences.d.ts +7 -0
  21. package/dist/client/reader-preferences.js +16 -4
  22. package/dist/client/reader-preferences.js.map +1 -1
  23. package/dist/client.js +17 -17
  24. package/dist/components/content/card.js +50 -40
  25. package/dist/components/content/card.js.map +1 -1
  26. package/dist/components/content/filters.d.ts +5 -0
  27. package/dist/components/content/filters.js +61 -52
  28. package/dist/components/content/filters.js.map +1 -1
  29. package/dist/components/content/popular.d.ts +1 -1
  30. package/dist/components/content/popular.js.map +1 -1
  31. package/dist/components/content/reader-settings.d.ts +4 -0
  32. package/dist/components/content/reader-settings.js +3 -1
  33. package/dist/components/content/reader-settings.js.map +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/react/discovery.d.ts +7 -2
  36. package/dist/react/discovery.js +208 -87
  37. package/dist/react/discovery.js.map +1 -1
  38. package/dist/react/provider.js +4 -1
  39. package/dist/react/provider.js.map +1 -1
  40. package/dist/styles.css +1 -1
  41. package/package.json +1 -1
package/README.md CHANGED
@@ -353,9 +353,11 @@ For a custom layout, `useDailyReleases(query)` exposes the same shared read and
353
353
  ranking window. For example, `{ sort: "popular:7d", published_within: "7d" }`
354
354
  ranks only content published during the past seven days.
355
355
 
356
- `<PopularContentGrid query={{ kind: "gallery" }} />` supplies the 7-day,
357
- 90-day, 365-day and all-time selector, keeps score and age windows matched,
358
- and loads further pages as the reader scrolls. `period` and `onPeriodChange`
356
+ `<PopularContentGrid query={{ kind: "gallery" }} />` supplies four visible tabs
357
+ for 7-day, 90-day, 365-day and all-time activity and loads further pages as the
358
+ reader scrolls. Publication age is unrestricted, so older content can trend
359
+ again. Supply `query.published_within` only when a release-age cutoff is wanted.
360
+ `period` and `onPeriodChange`
359
361
  optionally keep its state in the host's URL. For a homepage preview, pass
360
362
  `popularQuery("7d", { kind: "gallery", limit: 6 })` to a `ContentRail`'s `list`.
361
363
 
@@ -816,6 +818,7 @@ the host owns search-page navigation.
816
818
  ```tsx
817
819
  <SearchBox presentation="header" onSearch={(query) => navigate(`/search?q=${encodeURIComponent(query)}`)} />
818
820
  <ReaderSettings
821
+ kinds={["gallery"]}
819
822
  languages={[{ value: "en", label: "English" }, { value: "es", label: "Español" }]}
820
823
  language={language}
821
824
  onLanguageChange={changeLanguage}
@@ -829,7 +832,24 @@ account languages use the provider's `languagePreference` adapter. Each domain
829
832
  only offers its configured, user-toggleable entities; internal classifications
830
833
  and required or locked rules never appear in reader settings. Labels use the
831
834
  entities' localized names. Saving this domain's settings preserves choices for
832
- entities offered by other domains.
835
+ entities offered by other domains and other content kinds. `kinds` limits the
836
+ controls shown by either `ReaderSettings` or `ContentFilterSettings`; omit it to
837
+ show all configured kinds. Multiple kinds get separate headings; `labels` can
838
+ supply their display names. The same entity can have independent gallery and
839
+ blog preferences. Resets follow the current domain default, including after it
840
+ changes. Old unscoped browser choices remain a fallback until replaced
841
+ by a choice for that content kind.
842
+
843
+ The HTTP shape groups preferences by kind:
844
+
845
+ ```json
846
+ {"kinds":{"gallery":[{"entity_id":"tag-id","visible":false}]},"include_deleted":false}
847
+ ```
848
+
849
+ `PUT /me/content-filters` replaces only the submitted kinds. An empty gallery
850
+ list resets that kind to its domain defaults without changing blog preferences.
851
+ The registration initializer also accepts `legacy_filters` so old browser
852
+ choices can be transferred without dropping preferences for another domain.
833
853
 
834
854
  ```tsx
835
855
  <ContentKitProvider
@@ -860,3 +880,36 @@ language choice. Ordinary sign-ins never import guest preferences.
860
880
  `useReaderPreferences()` exposes the preferred language, pending/error state and
861
881
  a retry action. ReaderSettings shows an incomplete transfer and lets the reader
862
882
  retry it. Guest preferences are retained separately after registration and logout.
883
+
884
+ ### Activity tracking
885
+
886
+ `useViewSignal(contentId, versionId)` starts an observation when its returned
887
+ callback first reports progress. The shared `Beacons` client immediately requests
888
+ an activity context from ContentKit, then includes that signed receipt in each
889
+ revision. Its server timestamp stays fixed for the entire view. Pass a version
890
+ when one is displayed; receipt requests use the provider's current language, or
891
+ the client's configured request language outside React.
892
+
893
+ `useExposure(items, options)` records cumulative rendered and visible placements.
894
+ It splits long lists into stable segments of at most 32 placements, increments
895
+ each segment's stage revision, and attributes clicks to the matching segment.
896
+ Each newly observed placement receives its own server context. The client does
897
+ not supply premium status or eligibility claims. Receipt requests are batched in
898
+ groups of at most 100 targets.
899
+
900
+ For a custom integration, `client.discovery.activityContexts(targets, session,
901
+ language)` exposes `POST /activity-contexts`. Request its contexts when the
902
+ observation starts, not when a delayed event batch is flushed. An explicit
903
+ `enabled: false` response permits legacy tracking for an embedding without a
904
+ signer; a failed or invalid receipt never silently becomes an untrusted event.
905
+
906
+ The provider's identity changes invalidate buffered observations and active
907
+ view sessions. Events collected for one account or guest are never reassigned
908
+ to the next account. Custom callers using `Beacons.exposure` should call
909
+ `endExposure(renderId)` after the list leaves the page to release receipt state;
910
+ `useExposure` does this automatically.
911
+
912
+ Tracking flushes pack complete events into byte-bounded requests and serialize
913
+ keepalive sends across clients. Large queues flush while the page is active.
914
+ A single cumulative event that exceeds the keepalive budget uses normal fetch;
915
+ its delivery during page exit remains best effort.
@@ -10,6 +10,8 @@ export interface BeaconOptions {
10
10
  }
11
11
  /** Where a click or a view came from: the list render it was shown in. */
12
12
  export interface Attribution {
13
+ /** Language of the displayed content; defaults to the client's request language. */
14
+ language?: string;
13
15
  render_id?: string;
14
16
  surface?: string;
15
17
  /** 1-based position in that render. */
@@ -41,25 +43,33 @@ export declare class Beacons {
41
43
  private timer;
42
44
  private offUntil;
43
45
  private recorded;
46
+ private generation;
47
+ private hidden;
48
+ private readonly pending;
49
+ private readonly exposureContexts;
44
50
  private readonly session;
45
51
  private readonly delay;
46
52
  private readonly recheck;
47
- constructor(client: Pick<ContentKitClient, "discovery" | "kinds">, o?: BeaconOptions);
48
- /** A click on a listed card. */
53
+ constructor(client: Pick<ContentKitClient, "discovery" | "kinds"> & Partial<Pick<ContentKitClient, "preferences">>, o?: BeaconOptions);
54
+ /** A click on a listed card. Acquire its access snapshot at observation time. */
49
55
  click(content: string, a?: Attribution & {
50
56
  version_id?: string;
51
57
  }): void;
52
- /** A consumption session of a content (or one version): each update a revision of one event. */
58
+ /** A consumption session: all revisions use its first server access snapshot. */
53
59
  view(content: string, version?: string, a?: Attribution): ViewSession;
54
- /** A result list served, rendered or shown. */
60
+ /** A cumulative result-list stage; each newly observed target gets one receipt. */
55
61
  exposure(e: ExposureInput): void;
56
- /** Sends what has gathered now. */
62
+ /** Keep receipt state when a mounted list resumes (including StrictMode replay). */
63
+ beginExposure(render: string): void;
64
+ /** Retire receipt state after a list unmounts without cancelling observations. */
65
+ endExposure(render: string): void;
66
+ /** Sends ready observations; receipts are never acquired for the first time here. */
57
67
  flush(keepalive?: boolean): Promise<void>;
68
+ private contexts;
69
+ private track;
58
70
  private signal;
59
- /** Off until `recheck` passes; then the model is read again. */
60
71
  private paused;
61
72
  private pause;
62
- /** Whether the site records signals, asked at the first event and after a pause; an unreadable model sends anyway (a refusal still pauses them). */
63
73
  private recording;
64
74
  private schedule;
65
75
  }
@@ -1,72 +1,231 @@
1
1
  import { toContentKitError as e } from "./errors.js";
2
2
  //#region src/client/beacons.ts
3
- var t = 100, n = "contentkit.session", r = () => typeof crypto < "u" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`, i;
4
- function a() {
3
+ var t = 100, n = 61440, r = 49152, i = Promise.resolve(), a = new TextEncoder(), o = (e, t, n) => a.encode(JSON.stringify({
4
+ session: n,
5
+ [e]: t
6
+ })).length;
7
+ function s(e, r, i) {
8
+ let a = [], s = [];
9
+ for (let c of e) s.length && (s.length === t || o(r, [...s, c], i) > n) && (a.push({
10
+ items: s,
11
+ bytes: o(r, s, i)
12
+ }), s = []), s.push(c);
13
+ return s.length && a.push({
14
+ items: s,
15
+ bytes: o(r, s, i)
16
+ }), a;
17
+ }
18
+ function c(e) {
19
+ let t = i.then(e, e);
20
+ return i = t.then(() => {}, () => {}), t;
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;
23
+ function f() {
5
24
  try {
6
- let e = sessionStorage.getItem(n);
25
+ let e = sessionStorage.getItem(l);
7
26
  if (e) return e;
8
- let t = r();
9
- return sessionStorage.setItem(n, t), t;
27
+ let t = u();
28
+ return sessionStorage.setItem(l, t), t;
10
29
  } catch {
11
- return i ??= r();
30
+ return d ??= u();
12
31
  }
13
32
  }
14
- var o = class {
33
+ var p = class {
15
34
  client;
16
35
  signals = [];
17
36
  exposures = [];
18
37
  timer;
19
38
  offUntil = 0;
20
39
  recorded;
40
+ generation = 0;
41
+ hidden = !1;
42
+ pending = /* @__PURE__ */ new Set();
43
+ exposureContexts = /* @__PURE__ */ new Map();
21
44
  session;
22
45
  delay;
23
46
  recheck;
24
47
  constructor(e, t = {}) {
25
- this.client = e, this.session = t.session ?? a, this.delay = t.delay ?? 2e3, this.recheck = t.recheck ?? 6e4, typeof addEventListener == "function" && (addEventListener("pagehide", () => void this.flush(!0)), addEventListener("visibilitychange", () => {
26
- typeof document < "u" && document.visibilityState === "hidden" && this.flush(!0);
48
+ this.client = e, this.session = t.session ?? f, this.delay = t.delay ?? 2e3, this.recheck = t.recheck ?? 6e4, e.preferences?.subscribeIdentity(() => {
49
+ this.generation++, clearTimeout(this.timer), this.timer = void 0, this.signals = [], this.exposures = [], this.pending.clear();
50
+ for (let e of this.exposureContexts.values()) e.active = !1;
51
+ this.exposureContexts.clear(), this.recorded = void 0;
52
+ }), typeof addEventListener == "function" && (addEventListener("pagehide", () => {
53
+ this.hidden = !0, this.flush(!0);
54
+ }), addEventListener("pageshow", () => {
55
+ this.hidden = !1;
56
+ }), addEventListener("visibilitychange", () => {
57
+ this.hidden = typeof document < "u" && document.visibilityState === "hidden", this.hidden && this.flush(!0);
27
58
  }));
28
59
  }
29
60
  click(e, t = {}) {
30
- this.signal({
61
+ if (this.paused()) return;
62
+ let n = this.generation, r = u(), i = (/* @__PURE__ */ new Date()).toISOString(), { language: a, ...o } = t, s = {
31
63
  content_id: e,
32
- type: "click",
33
- event_id: r(),
34
- occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
35
- ...t
36
- });
64
+ version_id: t.version_id
65
+ };
66
+ this.track(this.contexts([s], n, a).then((t) => {
67
+ let a = t?.get(m(s));
68
+ a && n === this.generation && this.signal({
69
+ content_id: e,
70
+ type: "click",
71
+ event_id: r,
72
+ ...o,
73
+ occurred_at: a.context?.occurred_at ?? i,
74
+ activity_token: a.context?.token
75
+ });
76
+ }), n);
37
77
  }
38
78
  view(e, t, n = {}) {
39
- let i = r(), a = 0;
40
- return { update: (r) => this.signal({
79
+ let r = u(), i = this.generation, a = {
41
80
  content_id: e,
42
- version_id: t,
43
- type: "view",
44
- event_id: i,
45
- revision: ++a,
46
- occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
47
- ...n,
48
- ...r
49
- }) };
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
+ } };
50
118
  }
51
119
  exposure(e) {
52
- !this.paused() && e.shown.length && (this.exposures.push(e), this.schedule());
120
+ if (this.paused() || !e.shown.length) return;
121
+ let t = this.generation, n = `${e.render_id}\u0000${e.stage}`, r = this.exposureContexts.get(n);
122
+ r || (r = {
123
+ active: !0,
124
+ releaseRevision: 0,
125
+ contexts: /* @__PURE__ */ new Map()
126
+ }, this.exposureContexts.set(n, r)), r.releaseRevision++;
127
+ let i = r, a = e.shown.filter((e) => !i.contexts.has(m(e)));
128
+ if (a.length) {
129
+ let n = this.contexts(a, t, e.language);
130
+ for (let e of a) {
131
+ let t = m(e), r = n.then((e) => {
132
+ let n = e?.get(t) ?? null;
133
+ return !n && i.contexts.get(t) === r && i.contexts.delete(t), n;
134
+ }, (e) => {
135
+ throw i.contexts.get(t) === r && i.contexts.delete(t), e;
136
+ });
137
+ i.contexts.set(t, r);
138
+ }
139
+ }
140
+ this.track(Promise.all(e.shown.map((e) => i.contexts.get(m(e)))).then((n) => {
141
+ if (t !== this.generation || !i.active || n.some((e) => !e)) return;
142
+ let r = n;
143
+ if (new Set(r.map((e) => e.enabled)).size !== 1) return;
144
+ i.occurredAt ??= r.find((e) => e.context)?.context?.occurred_at ?? e.occurred_at ?? (/* @__PURE__ */ new Date()).toISOString();
145
+ let a = {
146
+ ...e,
147
+ occurred_at: i.occurredAt,
148
+ shown: e.shown.map((e, t) => ({
149
+ ...e,
150
+ activity_token: r[t]?.context?.token
151
+ }))
152
+ }, o = this.exposures.find((t) => t.render_id === e.render_id && t.stage === e.stage);
153
+ o && (o.revision ?? 0) > (e.revision ?? 0) || (this.exposures = this.exposures.filter((t) => t.render_id !== e.render_id || t.stage !== e.stage), this.exposures.push(a), this.schedule());
154
+ }), t);
53
155
  }
54
- async flush(n = !1) {
156
+ beginExposure(e) {
157
+ for (let t of [
158
+ "served",
159
+ "rendered",
160
+ "visible"
161
+ ]) {
162
+ let n = this.exposureContexts.get(`${e}\u0000${t}`);
163
+ n && n.releaseRevision++;
164
+ }
165
+ }
166
+ endExposure(e) {
167
+ for (let t of [
168
+ "served",
169
+ "rendered",
170
+ "visible"
171
+ ]) {
172
+ let n = `${e}\u0000${t}`, r = this.exposureContexts.get(n);
173
+ if (!r) continue;
174
+ let i = ++r.releaseRevision;
175
+ queueMicrotask(() => {
176
+ this.exposureContexts.get(n) === r && r.releaseRevision === i && this.exposureContexts.delete(n);
177
+ });
178
+ }
179
+ }
180
+ async flush(e = !1) {
181
+ clearTimeout(this.timer), this.timer = void 0;
182
+ let t = this.generation;
183
+ if (await Promise.allSettled([...this.pending]), t !== this.generation) return;
55
184
  clearTimeout(this.timer), this.timer = void 0;
56
185
  let r = this.signals.splice(0), i = this.exposures.splice(0);
57
186
  if (this.paused() || !r.length && !i.length) return;
58
- if (!await this.recording()) {
59
- this.pause();
60
- return;
187
+ let a = this.session(), o = [...s(r, "signals", a).map((e) => ({
188
+ ...e,
189
+ send: (t) => this.client.discovery.signals(e.items, a, { keepalive: t })
190
+ })), ...s(i, "exposures", a).map((e) => ({
191
+ ...e,
192
+ send: (t) => this.client.discovery.exposures(e.items, a, { keepalive: t })
193
+ }))], l = await Promise.allSettled(o.map((r) => {
194
+ let i = e && r.bytes <= n, a = () => t === this.generation ? r.send(i) : Promise.resolve();
195
+ return i ? c(a) : a();
196
+ }));
197
+ t === this.generation && l.some((e) => e.status === "rejected" && h(e.reason)) && this.pause();
198
+ }
199
+ async contexts(e, n, r) {
200
+ if (this.paused() || !await this.recording()) return n === this.generation && this.pause(), null;
201
+ if (n !== this.generation) return null;
202
+ let i = [...new Map(e.map((e) => [m(e), e])).values()], a = [], o = this.session();
203
+ for (let e = 0; e < i.length; e += t) a.push(this.client.discovery.activityContexts(i.slice(e, e + t), o, r));
204
+ let s = await Promise.all(a);
205
+ if (n !== this.generation) return null;
206
+ let c = /* @__PURE__ */ new Map();
207
+ for (let e = 0; e < s.length; e++) {
208
+ let n = s[e];
209
+ if (typeof n?.enabled != "boolean" || n.enabled && !Array.isArray(n.contexts)) throw Error("contentkit: invalid activity context response");
210
+ let r = new Map((n.contexts ?? []).map((e) => [m(e), e]));
211
+ for (let a of i.slice(e * t, (e + 1) * t)) {
212
+ let e = r.get(m(a));
213
+ if (n.enabled && (!e?.token || !Number.isFinite(Date.parse(e.occurred_at)))) throw Error("contentkit: invalid activity context");
214
+ c.set(m(a), {
215
+ enabled: n.enabled,
216
+ context: n.enabled ? e : void 0
217
+ });
218
+ }
61
219
  }
62
- let a = this.session(), o = [], { discovery: s } = this.client;
63
- for (let e = 0; e < r.length; e += t) o.push(s.signals(r.slice(e, e + t), a, { keepalive: n }));
64
- for (let e = 0; e < i.length; e += t) o.push(s.exposures(i.slice(e, e + t), a, { keepalive: n }));
65
- (await Promise.allSettled(o)).some((t) => t.status === "rejected" && [
66
- "not_configured",
67
- "unavailable",
68
- "server_unavailable"
69
- ].includes(e(t.reason).code)) && this.pause();
220
+ return c;
221
+ }
222
+ track(e, t) {
223
+ let n = e.catch((e) => {
224
+ t === this.generation && h(e) && this.pause();
225
+ }).finally(() => {
226
+ this.pending.delete(n);
227
+ });
228
+ this.pending.add(n);
70
229
  }
71
230
  signal(e) {
72
231
  this.paused() || (e.type === "view" && (this.signals = this.signals.filter((t) => t.event_id !== e.event_id)), this.signals.push(e), this.schedule());
@@ -81,11 +240,16 @@ var o = class {
81
240
  return this.recorded ??= this.client.kinds().then((e) => e.signals, () => !0);
82
241
  }
83
242
  schedule() {
84
- if (this.recording(), this.signals.length + this.exposures.length >= t) return void this.flush();
243
+ let e = o("signals", this.signals, this.session()) + o("exposures", this.exposures, this.session());
244
+ if (this.signals.length + this.exposures.length >= t || e >= r) return void this.flush(this.hidden);
85
245
  this.timer ??= setTimeout(() => void this.flush(), this.delay);
86
246
  }
87
- };
247
+ }, m = (e) => `${e.content_id}\u0000${e.version_id ?? ""}`, h = (t) => [
248
+ "not_configured",
249
+ "unavailable",
250
+ "server_unavailable"
251
+ ].includes(e(t).code);
88
252
  //#endregion
89
- export { o as Beacons, r as eventId, a as readerSession };
253
+ export { p as Beacons, u as eventId, f as readerSession };
90
254
 
91
255
  //# sourceMappingURL=beacons.js.map
@@ -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 { 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 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;\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 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\">,\n o: BeaconOptions = {},\n ) {\n this.session = o.session ?? readerSession;\n this.delay = o.delay ?? 2000;\n this.recheck = o.recheck ?? 60_000;\n if (typeof addEventListener === \"function\") {\n addEventListener(\"pagehide\", () => void this.flush(true));\n addEventListener(\"visibilitychange\", () => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"hidden\") void this.flush(true);\n });\n }\n }\n\n /** A click on a listed card. */\n click(content: string, a: Attribution & { version_id?: string } = {}): void {\n this.signal({ content_id: content, type: \"click\", event_id: eventId(), occurred_at: new Date().toISOString(), ...a });\n }\n\n /** A consumption session of a content (or one version): each update a revision of one event. */\n view(content: string, version?: string, a: Attribution = {}): ViewSession {\n const event = eventId();\n let revision = 0;\n return {\n update: (o) => this.signal({ content_id: content, version_id: version, type: \"view\", event_id: event, revision: ++revision, occurred_at: new Date().toISOString(), ...a, ...o }),\n };\n }\n\n /** A result list served, rendered or shown. */\n exposure(e: ExposureInput): void {\n if (this.paused() || !e.shown.length) return;\n this.exposures.push(e);\n this.schedule();\n }\n\n /** Sends what has gathered now. */\n async flush(keepalive = false): Promise<void> {\n clearTimeout(this.timer);\n this.timer = undefined;\n const signals = this.signals.splice(0);\n const exposures = this.exposures.splice(0);\n if (this.paused() || (!signals.length && !exposures.length)) return;\n if (!(await this.recording())) {\n this.pause();\n return;\n }\n const session = this.session();\n const sends: Promise<void>[] = [];\n const { discovery } = this.client;\n for (let i = 0; i < signals.length; i += MAX) sends.push(discovery.signals(signals.slice(i, i + MAX), session, { keepalive }));\n for (let i = 0; i < exposures.length; i += MAX) sends.push(discovery.exposures(exposures.slice(i, i + MAX), session, { keepalive }));\n const done = await Promise.allSettled(sends);\n if (done.some((r) => r.status === \"rejected\" && [\"not_configured\", \"unavailable\", \"server_unavailable\"].includes(toContentKitError(r.reason).code))) this.pause();\n }\n\n private signal(s: SignalInput): void {\n if (this.paused()) return;\n // A view's later revision replaces its earlier one still waiting.\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\n /** Off until `recheck` passes; then the model is read again. */\n private paused(): boolean {\n if (this.offUntil > Date.now()) return true;\n this.offUntil = 0;\n return false;\n }\n\n private pause(): void {\n this.offUntil = Date.now() + this.recheck;\n this.recorded = undefined;\n }\n\n /** Whether the site records signals, asked at the first event and after a pause; an unreadable model sends anyway (a refusal still pauses them). */\n private recording(): Promise<boolean> {\n return (this.recorded ??= this.client.kinds().then(\n (m) => m.signals,\n () => true,\n ));\n }\n\n private schedule(): void {\n void this.recording();\n if (this.signals.length + this.exposures.length >= MAX) return void this.flush();\n this.timer ??= setTimeout(() => void this.flush(), this.delay);\n }\n}\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'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,7 +1,20 @@
1
1
  import { FilterSetting } from './generated/wire.js';
2
+ /** Explicit visibility or a reset that follows the current domain default. */
3
+ export type ReaderFilterChoice = {
4
+ entity_id: string;
5
+ visible: boolean;
6
+ default?: never;
7
+ } | {
8
+ entity_id: string;
9
+ default: true;
10
+ visible?: never;
11
+ };
12
+ export type ReaderFilterKinds = Record<string, readonly ReaderFilterChoice[]>;
2
13
  export interface GuestReaderPreferences {
3
14
  language?: string;
4
- filters: readonly FilterSetting[];
15
+ kinds: ReaderFilterKinds;
16
+ /** Choices saved before content kinds were introduced. Explicit kind choices win. */
17
+ legacy_filters: readonly FilterSetting[];
5
18
  }
6
19
  export interface PendingReaderPreferences extends GuestReaderPreferences {
7
20
  account: string;
@@ -23,7 +36,7 @@ export declare class BrowserReaderPreferences {
23
36
  readonly snapshot: () => GuestReaderPreferences;
24
37
  readonly subscribe: (listener: () => void) => (() => void);
25
38
  setLanguage(value: string): void;
26
- setFilters(value: readonly FilterSetting[]): void;
39
+ setFilters(value: ReaderFilterKinds): void;
27
40
  pending(account: string): PendingReaderPreferences | undefined;
28
41
  /** Capture only on a confirmed registration, never on a login transition. */
29
42
  register(account: string): void;