@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.
- package/README.md +57 -4
- package/dist/client/beacons.d.ts +17 -7
- package/dist/client/beacons.js +205 -41
- package/dist/client/beacons.js.map +1 -1
- package/dist/client/browser-reader-preferences.d.ts +15 -2
- package/dist/client/browser-reader-preferences.js +67 -30
- package/dist/client/browser-reader-preferences.js.map +1 -1
- package/dist/client/discovery.d.ts +13 -3
- package/dist/client/discovery.js +31 -16
- package/dist/client/discovery.js.map +1 -1
- package/dist/client/generated/routes.d.ts +14 -1
- package/dist/client/generated/routes.js +18 -0
- package/dist/client/generated/routes.js.map +1 -1
- package/dist/client/generated/wire.d.ts +47 -4
- package/dist/client/generated/wire.js.map +1 -1
- package/dist/client/http.d.ts +7 -3
- package/dist/client/http.js +26 -7
- package/dist/client/http.js.map +1 -1
- package/dist/client/index.d.ts +2 -2
- package/dist/client/reader-preferences.d.ts +7 -0
- package/dist/client/reader-preferences.js +16 -4
- package/dist/client/reader-preferences.js.map +1 -1
- package/dist/client.js +17 -17
- package/dist/components/content/card.js +50 -40
- package/dist/components/content/card.js.map +1 -1
- package/dist/components/content/filters.d.ts +5 -0
- package/dist/components/content/filters.js +61 -52
- package/dist/components/content/filters.js.map +1 -1
- package/dist/components/content/popular.d.ts +1 -1
- package/dist/components/content/popular.js.map +1 -1
- package/dist/components/content/reader-settings.d.ts +4 -0
- package/dist/components/content/reader-settings.js +3 -1
- package/dist/components/content/reader-settings.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/react/discovery.d.ts +7 -2
- package/dist/react/discovery.js +208 -87
- package/dist/react/discovery.js.map +1 -1
- package/dist/react/provider.js +4 -1
- package/dist/react/provider.js.map +1 -1
- package/dist/styles.css +1 -1
- 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
|
|
357
|
-
90-day, 365-day and all-time
|
|
358
|
-
|
|
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.
|
package/dist/client/beacons.d.ts
CHANGED
|
@@ -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"
|
|
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
|
|
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
|
|
60
|
+
/** A cumulative result-list stage; each newly observed target gets one receipt. */
|
|
55
61
|
exposure(e: ExposureInput): void;
|
|
56
|
-
/**
|
|
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
|
}
|
package/dist/client/beacons.js
CHANGED
|
@@ -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 =
|
|
4
|
-
|
|
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(
|
|
25
|
+
let e = sessionStorage.getItem(l);
|
|
7
26
|
if (e) return e;
|
|
8
|
-
let t =
|
|
9
|
-
return sessionStorage.setItem(
|
|
27
|
+
let t = u();
|
|
28
|
+
return sessionStorage.setItem(l, t), t;
|
|
10
29
|
} catch {
|
|
11
|
-
return
|
|
30
|
+
return d ??= u();
|
|
12
31
|
}
|
|
13
32
|
}
|
|
14
|
-
var
|
|
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 ??
|
|
26
|
-
|
|
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.
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
40
|
-
return { update: (r) => this.signal({
|
|
79
|
+
let r = u(), i = this.generation, a = {
|
|
41
80
|
content_id: e,
|
|
42
|
-
version_id: t
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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 {
|
|
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":";;AA0BA,IAAM,IAAM,KACN,IAAc,sBACP,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,eAAe,QAAQ,CAAW;EAC/C,IAAI,GAAM,OAAO;EACjB,IAAM,IAAO,EAAQ;EAErB,OADA,eAAe,QAAQ,GAAa,CAAI,GACjC;CACT,QAAQ;EACN,OAAQ,MAAa,EAAQ;CAC/B;AACF;AAUA,IAAa,IAAb,MAAqB;CAWA;CAVnB,UAAiC,CAAC;CAClC,YAAqC,CAAC;CACtC;CACA,WAAmB;CACnB;CACA;CACA;CACA;CAEA,YACE,GACA,IAAmB,CAAC,GACpB;EAIA,AANiB,KAAA,SAAA,GAGjB,KAAK,UAAU,EAAE,WAAW,GAC5B,KAAK,QAAQ,EAAE,SAAS,KACxB,KAAK,UAAU,EAAE,WAAW,KACxB,OAAO,oBAAqB,eAC9B,iBAAiB,kBAAkB,KAAK,KAAK,MAAM,EAAI,CAAC,GACxD,iBAAiB,0BAA0B;GACzC,AAAI,OAAO,WAAa,OAAe,SAAS,oBAAoB,YAAU,KAAU,MAAM,EAAI;EACpG,CAAC;CAEL;CAGA,MAAM,GAAiB,IAA2C,CAAC,GAAS;EAC1E,KAAK,OAAO;GAAE,YAAY;GAAS,MAAM;GAAS,UAAU,EAAQ;GAAG,8BAAa,IAAI,KAAK,EAAA,CAAE,YAAY;GAAG,GAAG;EAAE,CAAC;CACtH;CAGA,KAAK,GAAiB,GAAkB,IAAiB,CAAC,GAAgB;EACxE,IAAM,IAAQ,EAAQ,GAClB,IAAW;EACf,OAAO,EACL,SAAS,MAAM,KAAK,OAAO;GAAE,YAAY;GAAS,YAAY;GAAS,MAAM;GAAQ,UAAU;GAAO,UAAU,EAAE;GAAU,8BAAa,IAAI,KAAK,EAAA,CAAE,YAAY;GAAG,GAAG;GAAG,GAAG;EAAE,CAAC,EACjL;CACF;CAGA,SAAS,GAAwB;EAC3B,MAAK,OAAO,KAAM,EAAE,MAAM,WAC9B,KAAK,UAAU,KAAK,CAAC,GACrB,KAAK,SAAS;CAChB;CAGA,MAAM,MAAM,IAAY,IAAsB;EAE5C,AADA,aAAa,KAAK,KAAK,GACvB,KAAK,QAAQ,KAAA;EACb,IAAM,IAAU,KAAK,QAAQ,OAAO,CAAC,GAC/B,IAAY,KAAK,UAAU,OAAO,CAAC;EACzC,IAAI,KAAK,OAAO,KAAM,CAAC,EAAQ,UAAU,CAAC,EAAU,QAAS;EAC7D,IAAI,CAAE,MAAM,KAAK,UAAU,GAAI;GAC7B,KAAK,MAAM;GACX;EACF;EACA,IAAM,IAAU,KAAK,QAAQ,GACvB,IAAyB,CAAC,GAC1B,EAAE,iBAAc,KAAK;EAC3B,KAAK,IAAI,IAAI,GAAG,IAAI,EAAQ,QAAQ,KAAK,GAAK,EAAM,KAAK,EAAU,QAAQ,EAAQ,MAAM,GAAG,IAAI,CAAG,GAAG,GAAS,EAAE,aAAU,CAAC,CAAC;EAC7H,KAAK,IAAI,IAAI,GAAG,IAAI,EAAU,QAAQ,KAAK,GAAK,EAAM,KAAK,EAAU,UAAU,EAAU,MAAM,GAAG,IAAI,CAAG,GAAG,GAAS,EAAE,aAAU,CAAC,CAAC;EAEnI,CAAI,MADe,QAAQ,WAAW,CAAK,EAAA,CAClC,MAAM,MAAM,EAAE,WAAW,cAAc;GAAC;GAAkB;GAAe;EAAoB,CAAC,CAAC,SAAS,EAAkB,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,KAAG,KAAK,MAAM;CAClK;CAEA,OAAe,GAAsB;EAC/B,KAAK,OAAO,MAEZ,EAAE,SAAS,WAAQ,KAAK,UAAU,KAAK,QAAQ,QAAQ,MAAM,EAAE,aAAa,EAAE,QAAQ,IAC1F,KAAK,QAAQ,KAAK,CAAC,GACnB,KAAK,SAAS;CAChB;CAGA,SAA0B;EAGxB,OAFI,KAAK,WAAW,KAAK,IAAI,MAC7B,KAAK,WAAW,GACT;CACT;CAEA,QAAsB;EAEpB,AADA,KAAK,WAAW,KAAK,IAAI,IAAI,KAAK,SAClC,KAAK,WAAW,KAAA;CAClB;CAGA,YAAsC;EACpC,OAAQ,KAAK,aAAa,KAAK,OAAO,MAAM,CAAC,CAAC,MAC3C,MAAM,EAAE,eACH,EACR;CACF;CAEA,WAAyB;EAEvB,IADA,KAAU,UAAU,GAChB,KAAK,QAAQ,SAAS,KAAK,UAAU,UAAU,GAAK,OAAO,KAAK,KAAK,MAAM;EAC/E,KAAK,UAAU,iBAAiB,KAAK,KAAK,MAAM,GAAG,KAAK,KAAK;CAC/D;AACF"}
|
|
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
|
-
|
|
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:
|
|
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;
|