@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
@@ -1,6 +1,7 @@
1
1
  //#region src/client/browser-reader-preferences.ts
2
2
  var e = Object.freeze({
3
- filters: Object.freeze([]),
3
+ kinds: Object.freeze({}),
4
+ legacy_filters: Object.freeze([]),
4
5
  registrations: Object.freeze([])
5
6
  });
6
7
  function t(e) {
@@ -13,36 +14,70 @@ function t(e) {
13
14
  function n(e) {
14
15
  if (!Array.isArray(e)) return [];
15
16
  let t = /* @__PURE__ */ new Map();
16
- for (let n of e.slice(0, 100)) n && typeof n.group_id == "string" && n.group_id.length > 0 && n.group_id.length <= 128 && typeof n.visible == "boolean" && t.set(n.group_id, n.visible);
17
+ for (let n of e) n && typeof (n.entity_id ?? n.group_id) == "string" && (n.entity_id ?? n.group_id).length > 0 && (n.entity_id ?? n.group_id).length <= 128 && typeof n.visible == "boolean" && t.set(n.entity_id ?? n.group_id, n.visible);
17
18
  return [...t].sort(([e], [t]) => e.localeCompare(t)).map(([e, t]) => ({
18
- group_id: e,
19
+ entity_id: e,
19
20
  visible: t
20
21
  }));
21
22
  }
22
- function r(r) {
23
+ function r(e) {
24
+ if (!Array.isArray(e)) return [];
25
+ let t = /* @__PURE__ */ new Map();
26
+ for (let n of e) {
27
+ let e = n?.entity_id;
28
+ typeof e != "string" || !e || e.length > 128 || (n.default === !0 && n.visible === void 0 ? t.set(e, {
29
+ entity_id: e,
30
+ default: !0
31
+ }) : n.default === void 0 && typeof n.visible == "boolean" && t.set(e, {
32
+ entity_id: e,
33
+ visible: n.visible
34
+ }));
35
+ }
36
+ return [...t].sort(([e], [t]) => e.localeCompare(t)).map(([, e]) => e);
37
+ }
38
+ function i(e) {
39
+ return !e || typeof e != "object" || Array.isArray(e) ? {} : Object.fromEntries(Object.entries(e).filter(([e]) => /^[a-z][a-z0-9_]*$/.test(e)).sort(([e], [t]) => e.localeCompare(t)).map(([e, t]) => [e, r(t)]));
40
+ }
41
+ function a(e) {
42
+ return {
43
+ kinds: i(e.kinds),
44
+ legacy_filters: n(e.legacy_filters ?? e.filters)
45
+ };
46
+ }
47
+ function o(e) {
48
+ return e.legacy_filters.length > 0 || Object.values(e.kinds).some((e) => e.length > 0);
49
+ }
50
+ function s(e) {
51
+ return {
52
+ language: e.language,
53
+ kinds: Object.freeze(Object.fromEntries(Object.entries(e.kinds).map(([e, t]) => [e, Object.freeze(t.map((e) => Object.freeze({ ...e })))]))),
54
+ legacy_filters: Object.freeze(e.legacy_filters.map((e) => Object.freeze({ ...e })))
55
+ };
56
+ }
57
+ function c(n) {
23
58
  try {
24
- let i = JSON.parse(r ?? "{}");
25
- if (!i || typeof i != "object") return e;
26
- let a = i, o = [];
27
- if (Array.isArray(a.registrations)) for (let e of a.registrations.slice(0, 20)) !e || typeof e.account != "string" || !e.account || e.account.length > 128 || o.some((t) => t.account === e.account) || o.push({
59
+ let r = JSON.parse(n ?? "{}");
60
+ if (!r || typeof r != "object") return e;
61
+ let i = r, o = [];
62
+ if (Array.isArray(i.registrations)) for (let e of i.registrations.slice(0, 20)) !e || typeof e.account != "string" || !e.account || e.account.length > 128 || o.some((t) => t.account === e.account) || o.push({
28
63
  account: e.account,
29
64
  language: t(e.language),
30
- filters: n(e.filters),
65
+ ...a(e),
31
66
  languageDone: e.languageDone === !0,
32
67
  filtersDone: e.filtersDone === !0,
33
68
  languageObserved: e.languageObserved === !0,
34
69
  initialLanguage: e.initialLanguage === null ? null : t(e.initialLanguage)
35
70
  });
36
71
  return {
37
- language: t(a.language),
38
- filters: n(a.filters),
72
+ language: t(i.language),
73
+ ...a(i),
39
74
  registrations: o
40
75
  };
41
76
  } catch {
42
77
  return e;
43
78
  }
44
79
  }
45
- var i = class {
80
+ var l = class {
46
81
  key;
47
82
  browser;
48
83
  value = e;
@@ -68,7 +103,7 @@ var i = class {
68
103
  setFilters(e) {
69
104
  this.sync(), this.save({
70
105
  ...this.value,
71
- filters: n(e)
106
+ kinds: i(e)
72
107
  });
73
108
  }
74
109
  pending(e) {
@@ -76,16 +111,17 @@ var i = class {
76
111
  }
77
112
  register(e) {
78
113
  if (this.sync(), !e || e.length > 128) throw Error("contentkit: registration account is required");
79
- if (!this.pending(e) && (this.value.language || this.value.filters.length)) {
114
+ if (!this.pending(e) && (this.value.language || o(this.value))) {
80
115
  if (this.value.registrations.length >= 20) throw Error("contentkit: too many pending preference registrations");
81
116
  this.save({
82
117
  ...this.value,
83
118
  registrations: [...this.value.registrations, {
84
119
  account: e,
85
120
  language: this.value.language,
86
- filters: this.value.filters,
121
+ kinds: this.value.kinds,
122
+ legacy_filters: this.value.legacy_filters,
87
123
  languageDone: !this.value.language,
88
- filtersDone: !this.value.filters.length
124
+ filtersDone: !o(this.value)
89
125
  }]
90
126
  });
91
127
  }
@@ -116,7 +152,7 @@ var i = class {
116
152
  };
117
153
  sync() {
118
154
  if (this.browser && !this.unavailable) try {
119
- this.replace(r(this.browser.localStorage.getItem(this.key)));
155
+ this.replace(c(this.browser.localStorage.getItem(this.key)));
120
156
  } catch {
121
157
  this.unavailable = !0;
122
158
  }
@@ -132,41 +168,42 @@ var i = class {
132
168
  replace(e) {
133
169
  let t = JSON.stringify({
134
170
  language: e.language,
135
- filters: e.filters,
171
+ kinds: e.kinds,
172
+ legacy_filters: e.legacy_filters,
136
173
  registrations: e.registrations
137
174
  });
138
175
  if (t === this.encoded) return;
139
176
  this.encoded = t, this.value = Object.freeze({
140
- ...e,
141
- filters: Object.freeze(e.filters.map((e) => Object.freeze({ ...e }))),
177
+ ...s(e),
142
178
  registrations: Object.freeze(e.registrations.map((e) => Object.freeze({
143
179
  ...e,
144
- filters: Object.freeze(e.filters.map((e) => Object.freeze({ ...e })))
180
+ ...s(e)
145
181
  })))
146
182
  });
147
183
  let n = {
148
184
  language: this.value.language,
149
- filters: this.value.filters
185
+ kinds: this.value.kinds,
186
+ legacy_filters: this.value.legacy_filters
150
187
  };
151
188
  JSON.stringify(n) !== JSON.stringify(this.publicValue) && (this.publicValue = Object.freeze(n));
152
189
  for (let e of this.listeners) try {
153
190
  e();
154
191
  } catch {}
155
192
  }
156
- }, a = /* @__PURE__ */ new WeakMap();
157
- function o(e) {
193
+ }, u = /* @__PURE__ */ new WeakMap();
194
+ function d(e) {
158
195
  let t = typeof window > "u" ? void 0 : window, n = e.replace(/\/+$/, "");
159
196
  if (t) try {
160
197
  n = new URL(n, t.location.href).href;
161
198
  } catch {}
162
199
  let r = `contentkit:reader-preferences:v1:${n}`;
163
- if (!t) return new i(r);
164
- let o = a.get(t);
165
- o || a.set(t, o = /* @__PURE__ */ new Map());
166
- let s = o.get(r);
167
- return s || o.set(r, s = new i(r, t)), s;
200
+ if (!t) return new l(r);
201
+ let i = u.get(t);
202
+ i || u.set(t, i = /* @__PURE__ */ new Map());
203
+ let a = i.get(r);
204
+ return a || i.set(r, a = new l(r, t)), a;
168
205
  }
169
206
  //#endregion
170
- export { i as BrowserReaderPreferences, o as browserReaderPreferences };
207
+ export { l as BrowserReaderPreferences, d as browserReaderPreferences };
171
208
 
172
209
  //# sourceMappingURL=browser-reader-preferences.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"browser-reader-preferences.js","names":[],"sources":["../../src/client/browser-reader-preferences.ts"],"sourcesContent":["import type { FilterSetting } from \"./generated/wire.js\";\n\nexport interface GuestReaderPreferences {\n language?: string;\n filters: readonly FilterSetting[];\n}\n\nexport interface PendingReaderPreferences extends GuestReaderPreferences {\n account: string;\n languageDone: boolean;\n filtersDone: boolean;\n languageObserved?: boolean;\n initialLanguage?: string | null;\n}\n\ntype Document = GuestReaderPreferences & { registrations: readonly PendingReaderPreferences[] };\nconst EMPTY: Document = Object.freeze({ filters: Object.freeze([]), registrations: Object.freeze([]) });\n\nfunction language(value: unknown): string | undefined {\n if (typeof value !== \"string\" || value.length > 64) return;\n try { return Intl.getCanonicalLocales(value)[0]; } catch { return; }\n}\n\nfunction filters(value: unknown): FilterSetting[] {\n if (!Array.isArray(value)) return [];\n const entries = new Map<string, boolean>();\n for (const row of value.slice(0, 100)) {\n if (row && typeof row.group_id === \"string\" && row.group_id.length > 0 && row.group_id.length <= 128 && typeof row.visible === \"boolean\") entries.set(row.group_id, row.visible);\n }\n return [...entries].sort(([a], [b]) => a.localeCompare(b)).map(([group_id, visible]) => ({ group_id, visible }));\n}\n\nfunction decode(raw: string | null): Document {\n try {\n const data: unknown = JSON.parse(raw ?? \"{}\");\n if (!data || typeof data !== \"object\") return EMPTY;\n const value = data as Record<string, unknown>;\n const registrations: PendingReaderPreferences[] = [];\n if (Array.isArray(value.registrations)) {\n for (const entry of value.registrations.slice(0, 20)) {\n if (!entry || typeof entry.account !== \"string\" || !entry.account || entry.account.length > 128 || registrations.some((r) => r.account === entry.account)) continue;\n registrations.push({ account: entry.account, language: language(entry.language), filters: filters(entry.filters), languageDone: entry.languageDone === true, filtersDone: entry.filtersDone === true,\n languageObserved: entry.languageObserved === true, initialLanguage: entry.initialLanguage === null ? null : language(entry.initialLanguage) });\n }\n }\n return { language: language(value.language), filters: filters(value.filters), registrations };\n } catch { return EMPTY; }\n}\n\n/** Anonymous preferences and registration retries, isolated to one app URL. */\nexport class BrowserReaderPreferences {\n private value: Document = EMPTY;\n private publicValue: GuestReaderPreferences = EMPTY;\n private encoded = JSON.stringify(EMPTY);\n private unavailable = false;\n private readonly listeners = new Set<() => void>();\n\n constructor(private readonly key: string, private readonly browser?: Window) { this.sync(); }\n\n readonly snapshot = (): GuestReaderPreferences => this.publicValue;\n\n readonly subscribe = (listener: () => void): (() => void) => {\n this.sync();\n if (!this.listeners.size) this.browser?.addEventListener(\"storage\", this.onStorage);\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n if (!this.listeners.size) this.browser?.removeEventListener(\"storage\", this.onStorage);\n };\n };\n\n setLanguage(value: string): void {\n const next = language(value);\n if (!next) throw new Error(\"contentkit: invalid preferred language\");\n this.sync();\n this.save({ ...this.value, language: next });\n }\n\n setFilters(value: readonly FilterSetting[]): void {\n this.sync();\n this.save({ ...this.value, filters: filters(value) });\n }\n\n pending(account: string): PendingReaderPreferences | undefined {\n this.sync();\n return this.value.registrations.find((r) => r.account === account);\n }\n\n /** Capture only on a confirmed registration, never on a login transition. */\n register(account: string): void {\n this.sync();\n if (!account || account.length > 128) throw new Error(\"contentkit: registration account is required\");\n if (this.pending(account)) return;\n if (!this.value.language && !this.value.filters.length) return;\n if (this.value.registrations.length >= 20) throw new Error(\"contentkit: too many pending preference registrations\");\n this.save({ ...this.value, registrations: [...this.value.registrations, {\n account, language: this.value.language, filters: this.value.filters,\n languageDone: !this.value.language, filtersDone: !this.value.filters.length,\n }] });\n }\n\n complete(account: string, step: \"language\" | \"filters\"): void {\n this.sync();\n const registrations = this.value.registrations.map((r) => r.account === account ? { ...r, [step === \"language\" ? \"languageDone\" : \"filtersDone\"]: true } : r)\n .filter((r) => !r.languageDone || !r.filtersDone);\n this.save({ ...this.value, registrations });\n }\n\n observeLanguage(account: string, initialLanguage: string | null): void {\n this.sync();\n this.save({ ...this.value, registrations: this.value.registrations.map((r) => r.account === account && !r.languageObserved ? { ...r, languageObserved: true, initialLanguage } : r) });\n }\n\n private readonly onStorage = (event: StorageEvent): void => {\n if (event.key === null || event.key === this.key) this.sync();\n };\n\n private sync(): void {\n if (!this.browser || this.unavailable) return;\n try { this.replace(decode(this.browser.localStorage.getItem(this.key))); }\n catch { this.unavailable = true; }\n }\n\n private save(next: Document): void {\n if (this.browser && !this.unavailable) {\n try { this.browser.localStorage.setItem(this.key, JSON.stringify(next)); }\n catch { this.unavailable = true; }\n }\n this.replace(next);\n }\n\n private replace(next: Document): void {\n const encoded = JSON.stringify({ language: next.language, filters: next.filters, registrations: next.registrations });\n if (encoded === this.encoded) return;\n this.encoded = encoded;\n this.value = Object.freeze({ ...next, filters: Object.freeze(next.filters.map((f) => Object.freeze({ ...f }))), registrations: Object.freeze(next.registrations.map((r) => Object.freeze({ ...r, filters: Object.freeze(r.filters.map((f) => Object.freeze({ ...f }))) }))) });\n const publicValue = { language: this.value.language, filters: this.value.filters };\n if (JSON.stringify(publicValue) !== JSON.stringify(this.publicValue)) this.publicValue = Object.freeze(publicValue);\n for (const listener of this.listeners) { try { listener(); } catch { /* Observers do not own persistence. */ } }\n }\n}\n\nconst stores = new WeakMap<Window, Map<string, BrowserReaderPreferences>>();\n\n/** Same-app clients share a store; server-rendered requests never do. */\nexport function browserReaderPreferences(mount: string): BrowserReaderPreferences {\n const browser = typeof window === \"undefined\" ? undefined : window;\n let scope = mount.replace(/\\/+$/, \"\");\n if (browser) { try { scope = new URL(scope, browser.location.href).href; } catch { /* Keep the explicit app key. */ } }\n const key = `contentkit:reader-preferences:v1:${scope}`;\n if (!browser) return new BrowserReaderPreferences(key);\n let scoped = stores.get(browser);\n if (!scoped) stores.set(browser, (scoped = new Map()));\n let store = scoped.get(key);\n if (!store) scoped.set(key, (store = new BrowserReaderPreferences(key, browser)));\n return store;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"browser-reader-preferences.js","names":[],"sources":["../../src/client/browser-reader-preferences.ts"],"sourcesContent":["import type { FilterSetting } from \"./generated/wire.js\";\n\n/** Explicit visibility or a reset that follows the current domain default. */\nexport type ReaderFilterChoice = { entity_id: string; visible: boolean; default?: never } | { entity_id: string; default: true; visible?: never };\nexport type ReaderFilterKinds = Record<string, readonly ReaderFilterChoice[]>;\n\nexport interface GuestReaderPreferences {\n language?: string;\n kinds: ReaderFilterKinds;\n /** Choices saved before content kinds were introduced. Explicit kind choices win. */\n legacy_filters: readonly FilterSetting[];\n}\n\nexport interface PendingReaderPreferences extends GuestReaderPreferences {\n account: string;\n languageDone: boolean;\n filtersDone: boolean;\n languageObserved?: boolean;\n initialLanguage?: string | null;\n}\n\ntype Document = GuestReaderPreferences & { registrations: readonly PendingReaderPreferences[] };\nconst EMPTY: Document = Object.freeze({ kinds: Object.freeze({}), legacy_filters: Object.freeze([]), registrations: Object.freeze([]) });\n\nfunction language(value: unknown): string | undefined {\n if (typeof value !== \"string\" || value.length > 64) return;\n try { return Intl.getCanonicalLocales(value)[0]; } catch { return; }\n}\n\nfunction filters(value: unknown): FilterSetting[] {\n if (!Array.isArray(value)) return [];\n const entries = new Map<string, boolean>();\n for (const row of value) {\n if (row && typeof (row.entity_id ?? row.group_id) === \"string\" && (row.entity_id ?? row.group_id).length > 0 && (row.entity_id ?? row.group_id).length <= 128 && typeof row.visible === \"boolean\") entries.set(row.entity_id ?? row.group_id, row.visible);\n }\n return [...entries].sort(([a], [b]) => a.localeCompare(b)).map(([entity_id, visible]) => ({ entity_id, visible }));\n}\n\nfunction scopedFilters(value: unknown): ReaderFilterChoice[] {\n if (!Array.isArray(value)) return [];\n const entries = new Map<string, ReaderFilterChoice>();\n for (const row of value) {\n const id = row?.entity_id;\n if (typeof id !== \"string\" || !id || id.length > 128) continue;\n if (row.default === true && row.visible === undefined) entries.set(id, { entity_id: id, default: true });\n else if (row.default === undefined && typeof row.visible === \"boolean\") entries.set(id, { entity_id: id, visible: row.visible });\n }\n return [...entries].sort(([a], [b]) => a.localeCompare(b)).map(([, row]) => row);\n}\n\nfunction kinds(value: unknown): ReaderFilterKinds {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n return Object.fromEntries(Object.entries(value)\n .filter(([kind]) => /^[a-z][a-z0-9_]*$/.test(kind))\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([kind, value]) => [kind, scopedFilters(value)]));\n}\n\nfunction choices(value: Record<string, unknown>) {\n return { kinds: kinds(value.kinds), legacy_filters: filters(value.legacy_filters ?? value.filters) };\n}\n\nfunction hasChoices(value: GuestReaderPreferences): boolean {\n return value.legacy_filters.length > 0 || Object.values(value.kinds).some((rows) => rows.length > 0);\n}\n\nfunction freezeChoices(value: GuestReaderPreferences): GuestReaderPreferences {\n return { language: value.language,\n kinds: Object.freeze(Object.fromEntries(Object.entries(value.kinds).map(([kind, rows]) => [kind, Object.freeze(rows.map((f) => Object.freeze({ ...f })))]))),\n legacy_filters: Object.freeze(value.legacy_filters.map((f) => Object.freeze({ ...f }))) };\n}\n\nfunction decode(raw: string | null): Document {\n try {\n const data: unknown = JSON.parse(raw ?? \"{}\");\n if (!data || typeof data !== \"object\") return EMPTY;\n const value = data as Record<string, unknown>;\n const registrations: PendingReaderPreferences[] = [];\n if (Array.isArray(value.registrations)) {\n for (const entry of value.registrations.slice(0, 20)) {\n if (!entry || typeof entry.account !== \"string\" || !entry.account || entry.account.length > 128 || registrations.some((r) => r.account === entry.account)) continue;\n registrations.push({ account: entry.account, language: language(entry.language), ...choices(entry), languageDone: entry.languageDone === true, filtersDone: entry.filtersDone === true,\n languageObserved: entry.languageObserved === true, initialLanguage: entry.initialLanguage === null ? null : language(entry.initialLanguage) });\n }\n }\n return { language: language(value.language), ...choices(value), registrations };\n } catch { return EMPTY; }\n}\n\n/** Anonymous preferences and registration retries, isolated to one app URL. */\nexport class BrowserReaderPreferences {\n private value: Document = EMPTY;\n private publicValue: GuestReaderPreferences = EMPTY;\n private encoded = JSON.stringify(EMPTY);\n private unavailable = false;\n private readonly listeners = new Set<() => void>();\n\n constructor(private readonly key: string, private readonly browser?: Window) { this.sync(); }\n\n readonly snapshot = (): GuestReaderPreferences => this.publicValue;\n\n readonly subscribe = (listener: () => void): (() => void) => {\n this.sync();\n if (!this.listeners.size) this.browser?.addEventListener(\"storage\", this.onStorage);\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n if (!this.listeners.size) this.browser?.removeEventListener(\"storage\", this.onStorage);\n };\n };\n\n setLanguage(value: string): void {\n const next = language(value);\n if (!next) throw new Error(\"contentkit: invalid preferred language\");\n this.sync();\n this.save({ ...this.value, language: next });\n }\n\n setFilters(value: ReaderFilterKinds): void {\n this.sync();\n this.save({ ...this.value, kinds: kinds(value) });\n }\n\n pending(account: string): PendingReaderPreferences | undefined {\n this.sync();\n return this.value.registrations.find((r) => r.account === account);\n }\n\n /** Capture only on a confirmed registration, never on a login transition. */\n register(account: string): void {\n this.sync();\n if (!account || account.length > 128) throw new Error(\"contentkit: registration account is required\");\n if (this.pending(account)) return;\n if (!this.value.language && !hasChoices(this.value)) return;\n if (this.value.registrations.length >= 20) throw new Error(\"contentkit: too many pending preference registrations\");\n this.save({ ...this.value, registrations: [...this.value.registrations, {\n account, language: this.value.language, kinds: this.value.kinds, legacy_filters: this.value.legacy_filters,\n languageDone: !this.value.language, filtersDone: !hasChoices(this.value),\n }] });\n }\n\n complete(account: string, step: \"language\" | \"filters\"): void {\n this.sync();\n const registrations = this.value.registrations.map((r) => r.account === account ? { ...r, [step === \"language\" ? \"languageDone\" : \"filtersDone\"]: true } : r)\n .filter((r) => !r.languageDone || !r.filtersDone);\n this.save({ ...this.value, registrations });\n }\n\n observeLanguage(account: string, initialLanguage: string | null): void {\n this.sync();\n this.save({ ...this.value, registrations: this.value.registrations.map((r) => r.account === account && !r.languageObserved ? { ...r, languageObserved: true, initialLanguage } : r) });\n }\n\n private readonly onStorage = (event: StorageEvent): void => {\n if (event.key === null || event.key === this.key) this.sync();\n };\n\n private sync(): void {\n if (!this.browser || this.unavailable) return;\n try { this.replace(decode(this.browser.localStorage.getItem(this.key))); }\n catch { this.unavailable = true; }\n }\n\n private save(next: Document): void {\n if (this.browser && !this.unavailable) {\n try { this.browser.localStorage.setItem(this.key, JSON.stringify(next)); }\n catch { this.unavailable = true; }\n }\n this.replace(next);\n }\n\n private replace(next: Document): void {\n const encoded = JSON.stringify({ language: next.language, kinds: next.kinds, legacy_filters: next.legacy_filters, registrations: next.registrations });\n if (encoded === this.encoded) return;\n this.encoded = encoded;\n this.value = Object.freeze({ ...freezeChoices(next), registrations: Object.freeze(next.registrations.map((r) => Object.freeze({ ...r, ...freezeChoices(r) }))) });\n const publicValue = { language: this.value.language, kinds: this.value.kinds, legacy_filters: this.value.legacy_filters };\n if (JSON.stringify(publicValue) !== JSON.stringify(this.publicValue)) this.publicValue = Object.freeze(publicValue);\n for (const listener of this.listeners) { try { listener(); } catch { /* Observers do not own persistence. */ } }\n }\n}\n\nconst stores = new WeakMap<Window, Map<string, BrowserReaderPreferences>>();\n\n/** Same-app clients share a store; server-rendered requests never do. */\nexport function browserReaderPreferences(mount: string): BrowserReaderPreferences {\n const browser = typeof window === \"undefined\" ? undefined : window;\n let scope = mount.replace(/\\/+$/, \"\");\n if (browser) { try { scope = new URL(scope, browser.location.href).href; } catch { /* Keep the explicit app key. */ } }\n const key = `contentkit:reader-preferences:v1:${scope}`;\n if (!browser) return new BrowserReaderPreferences(key);\n let scoped = stores.get(browser);\n if (!scoped) stores.set(browser, (scoped = new Map()));\n let store = scoped.get(key);\n if (!store) scoped.set(key, (store = new BrowserReaderPreferences(key, browser)));\n return store;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { ContentFilters, ContentFiltersUpdate, ContentPage, DailyReleases, ExposureInput, RecentSearches, ReleaseDates, SignalInput, Typeahead } from './generated/wire.js';
1
+ import { ActivityContexts, ActivityTarget, ContentFilters, ContentFiltersUpdate, ContentPage, DailyReleases, ExposureInput, RecentSearches, ReleaseDates, SignalInput, Typeahead } from './generated/wire.js';
2
2
  import { Http } from './http.js';
3
3
  import { BrowserSearchHistory } from './browser-search-history.js';
4
4
  /** A popularity period. */
@@ -53,11 +53,19 @@ export interface PageQuery {
53
53
  }
54
54
  export type ContentQuery = ContentFilterQuery & ReaderQuery & CardQuery & PageQuery & {
55
55
  sort?: ListSort;
56
+ /** Inclusive UTC date (YYYY-MM-DD); requires activity_until, kind and sort=popular. */
57
+ activity_from?: string;
58
+ /** Exclusive UTC date (YYYY-MM-DD); requires activity_from, kind and sort=popular. */
59
+ activity_until?: string;
56
60
  };
57
61
  export type PopularPeriod = "7d" | "90d" | "365d" | "all";
58
- export type PopularQuery = Omit<ContentQuery, "sort" | "published_within">;
59
- /** Match the popularity score window and publication age window. */
62
+ export type PopularQuery = Omit<ContentQuery, "sort" | "activity_from" | "activity_until">;
63
+ /** Rank activity in the selected window; publication age is an optional separate filter. */
60
64
  export declare function popularQuery(period?: PopularPeriod, filters?: PopularQuery): ContentQuery;
65
+ /** Rank activity between two UTC dates: from inclusive, until exclusive. */
66
+ export declare function popularBetween(from: string, until: string, filters: PopularQuery & {
67
+ kind: string;
68
+ }): ContentQuery;
61
69
  export type SearchQuery = ContentFilterQuery & ReaderQuery & CardQuery & PageQuery & {
62
70
  q: string;
63
71
  sort?: "relevance" | ListSort;
@@ -138,6 +146,8 @@ export declare class DiscoveryClient {
138
146
  kind?: string;
139
147
  content_id?: string;
140
148
  }): Promise<void>;
149
+ /** Capture server-owned access and eligibility at an observation's start. */
150
+ activityContexts(targets: readonly ActivityTarget[], session?: string, language?: string | undefined): Promise<ActivityContexts>;
141
151
  /** Views and clicks; signed out, of the session named. */
142
152
  signals(signals: SignalInput[], session?: string, o?: {
143
153
  keepalive?: boolean;
@@ -4,11 +4,18 @@ import { browserSearchHistory as n } from "./browser-search-history.js";
4
4
  function r(e = "7d", t = {}) {
5
5
  return {
6
6
  ...t,
7
- sort: `popular:${e}`,
8
- published_within: e
7
+ sort: `popular:${e}`
9
8
  };
10
9
  }
11
- var i = /* @__PURE__ */ new Set([
10
+ function i(e, t, n) {
11
+ return {
12
+ ...n,
13
+ sort: "popular",
14
+ activity_from: e,
15
+ activity_until: t
16
+ };
17
+ }
18
+ var a = /* @__PURE__ */ new Set([
12
19
  "groups",
13
20
  "exclude_groups",
14
21
  "creators",
@@ -18,12 +25,12 @@ var i = /* @__PURE__ */ new Set([
18
25
  "card_entities",
19
26
  "entity_kinds"
20
27
  ]);
21
- function a(e) {
28
+ function o(e) {
22
29
  let t = {};
23
- for (let [n, r] of Object.entries(e)) t[n] = i.has(n) && Array.isArray(r) ? r.length ? r.join(",") : void 0 : r;
30
+ for (let [n, r] of Object.entries(e)) t[n] = a.has(n) && Array.isArray(r) ? r.length ? r.join(",") : void 0 : r;
24
31
  return t;
25
32
  }
26
- var o = class {
33
+ var s = class {
27
34
  http;
28
35
  browserSearches;
29
36
  constructor(e) {
@@ -31,13 +38,13 @@ var o = class {
31
38
  }
32
39
  list(t = {}, n) {
33
40
  return e(this.http, "GET", "/contents", {
34
- query: a(t),
41
+ query: o(t),
35
42
  signal: n
36
43
  });
37
44
  }
38
45
  async search(t, n) {
39
46
  let r = t.cursor ? null : this.http.captureChanges(), i = await e(this.http, "GET", "/search", {
40
- query: a(t),
47
+ query: o(t),
41
48
  signal: n
42
49
  });
43
50
  return r?.({ type: "searches.changed" }), i;
@@ -54,32 +61,32 @@ var o = class {
54
61
  }
55
62
  typeahead(t, n) {
56
63
  return e(this.http, "GET", "/typeahead", {
57
- query: a(t),
64
+ query: o(t),
58
65
  signal: n
59
66
  });
60
67
  }
61
68
  similar(t, n = {}, r) {
62
69
  return e(this.http, "GET", "/contents/{id}/similar", {
63
70
  params: { id: t },
64
- query: a(n),
71
+ query: o(n),
65
72
  signal: r
66
73
  });
67
74
  }
68
75
  recommendations(t = {}, n) {
69
76
  return e(this.http, "GET", "/recommendations", {
70
- query: a(t),
77
+ query: o(t),
71
78
  signal: n
72
79
  });
73
80
  }
74
81
  releaseDates(t = {}, n) {
75
82
  return e(this.http, "GET", "/release-dates", {
76
- query: a(t),
83
+ query: o(t),
77
84
  signal: n
78
85
  });
79
86
  }
80
87
  releases(t = {}, n) {
81
88
  return e(this.http, "GET", "/releases", {
82
- query: a(t),
89
+ query: o(t),
83
90
  signal: n
84
91
  });
85
92
  }
@@ -88,7 +95,7 @@ var o = class {
88
95
  signal: n,
89
96
  guestPreferences: !1
90
97
  });
91
- return this.http.acceptGuestFilters(r.filters), r;
98
+ return this.http.acceptGuestFilters(r.kinds), r;
92
99
  }
93
100
  async setContentFilters(t, n) {
94
101
  let r = this.http.captureChanges(), i = await e(this.http, "PUT", "/me/content-filters", {
@@ -99,7 +106,7 @@ var o = class {
99
106
  }
100
107
  history(t = {}, n) {
101
108
  return e(this.http, "GET", "/me/history", {
102
- query: a(t),
109
+ query: o(t),
103
110
  signal: n
104
111
  });
105
112
  }
@@ -107,6 +114,14 @@ var o = class {
107
114
  let n = this.http.captureChanges();
108
115
  await e(this.http, "DELETE", "/me/history", { query: t }), n({ type: "history.changed" });
109
116
  }
117
+ activityContexts(t, n, r = this.http.language()) {
118
+ let i = {
119
+ targets: [...t],
120
+ session: n,
121
+ language: r
122
+ };
123
+ return e(this.http, "POST", "/activity-contexts", { body: i });
124
+ }
110
125
  signals(t, n, r = {}) {
111
126
  let i = {
112
127
  session: n,
@@ -129,6 +144,6 @@ var o = class {
129
144
  }
130
145
  };
131
146
  //#endregion
132
- export { o as DiscoveryClient, r as popularQuery, a as query };
147
+ export { s as DiscoveryClient, i as popularBetween, r as popularQuery, o as query };
133
148
 
134
149
  //# sourceMappingURL=discovery.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.js","names":[],"sources":["../../src/client/discovery.ts"],"sourcesContent":["import type {\n ContentFilters,\n ContentFiltersUpdate,\n ContentPage,\n DailyReleases,\n ExposureInput,\n Exposures,\n RecentSearches,\n ReleaseDates,\n SignalInput,\n Signals,\n Typeahead,\n} from \"./generated/wire.js\";\nimport type { Http } from \"./http.js\";\nimport { call, routeURL } from \"./route.js\";\nimport { browserSearchHistory, type BrowserSearchHistory } from \"./browser-search-history.js\";\n\n/** A popularity period. */\nexport type Period = \"7d\" | \"30d\" | \"90d\" | \"365d\" | \"all\";\n/** A list's order; position orders by the first of `groups` (an ordered group). */\nexport type ListSort = \"newest\" | \"oldest\" | \"title\" | \"popular\" | `popular:${Period}` | \"top_rated\" | `top_rated:${Period}` | \"most_favorited\" | \"most_viewed\" | \"position\" | \"random\";\n/** How locked contents list: marked (lock, the default) or left out (hide). */\nexport type AccessMode = \"lock\" | \"hide\";\n\n/** Which contents a list holds. */\nexport interface ContentFilterQuery {\n kind?: string;\n /** Group ids every listed version is in. */\n groups?: readonly string[];\n /** Group ids no listed version is in. */\n exclude_groups?: readonly string[];\n /** Only contents this group owns: a channel's feed. */\n owner_group?: string;\n /** User ids: only contents one of them is credited on. */\n creators?: readonly string[];\n /** public (anyone opens it now) or the level it stays locked at. */\n access?: readonly (\"public\" | \"tier\" | \"group\" | \"ppv\" | \"group_ppv\")[];\n /** Only contents locked behind one of these tiers. */\n tiers?: readonly string[];\n /** Version traits every listed version has. */\n traits?: readonly string[];\n /** \"name:value\" of the kind's filterable attributes. */\n attribute?: readonly string[];\n /** Published on this day (YYYY-MM-DD) or month (YYYY-MM). */\n released_on?: string;\n /** Only contents published within this age window; independent of the popularity score window. */\n published_within?: Period;\n /** IANA time zone of released_on and release dates; UTC by default. */\n tz?: string;\n /** The signed-in reader's unseen, in-progress or completed contents. */\n view_status?: \"unseen\" | \"in_progress\" | \"completed\";\n}\n\n/** How a list reads to its reader. */\nexport interface ReaderQuery {\n /** Only content with a live version in this language. */\n language?: string;\n /** Titles, names and paths in this language, and each card's preferred version's; language by default. */\n display_language?: string;\n mode?: AccessMode;\n}\n\n/** The group kinds cards name; every kind with a page by default. */\nexport interface CardQuery {\n card_entities?: readonly string[];\n}\n\nexport interface PageQuery {\n cursor?: string;\n limit?: number;\n}\n\nexport type ContentQuery = ContentFilterQuery & ReaderQuery & CardQuery & PageQuery & { sort?: ListSort };\nexport type PopularPeriod = \"7d\" | \"90d\" | \"365d\" | \"all\";\nexport type PopularQuery = Omit<ContentQuery, \"sort\" | \"published_within\">;\n\n/** Match the popularity score window and publication age window. */\nexport function popularQuery(period: PopularPeriod = \"7d\", filters: PopularQuery = {}): ContentQuery {\n return { ...filters, sort: `popular:${period}`, published_within: period };\n}\n\nexport type SearchQuery = ContentFilterQuery &\n ReaderQuery &\n CardQuery &\n PageQuery & {\n q: string;\n sort?: \"relevance\" | ListSort;\n personalize?: boolean;\n /** Explicit legacy server-side anonymous history. Omit for public search without server history. */\n session?: string;\n };\nexport interface RecentSearchesQuery {\n /** Explicit legacy server-side anonymous history; no anonymous session is created by default. */\n session?: string;\n /** 10 by default, at most 100. */\n limit?: number;\n}\nexport type RailQuery = ContentFilterQuery & ReaderQuery & CardQuery & { limit?: number };\nexport type ReleaseDatesQuery = ContentFilterQuery & ReaderQuery & PageQuery & { by?: \"month\" | \"day\" };\n/** Complete daily metadata for one calendar window, without pagination. */\nexport type DailyReleasesQuery = Omit<ContentFilterQuery, \"released_on\" | \"tz\"> & ReaderQuery & CardQuery & { days?: number; time_zone?: string };\nexport type HistoryQuery = ReaderQuery & CardQuery & PageQuery & { kind?: string; status?: \"seen\" | \"in_progress\" | \"completed\" };\nexport interface TypeaheadQuery extends ReaderQuery {\n q: string;\n /** Only contents of this kind. */\n kind?: string;\n /** The group kinds suggested; every kind with a page by default. */\n entity_kinds?: readonly string[];\n /** Per section: 8 by default, at most 20. */\n limit?: number;\n}\n\n// Comma-joined list parameters; `attribute` repeats instead.\nconst LISTS = new Set([\"groups\", \"exclude_groups\", \"creators\", \"access\", \"tiers\", \"traits\", \"card_entities\", \"entity_kinds\"]);\n\n/** A query with its list parameters comma-joined, as discovery routes read them. */\nexport function query(q: object): Record<string, string | number | boolean | readonly string[] | undefined> {\n const out: Record<string, string | number | boolean | readonly string[] | undefined> = {};\n for (const [k, v] of Object.entries(q)) out[k] = LISTS.has(k) && Array.isArray(v) ? (v.length ? v.join(\",\") : undefined) : v;\n return out;\n}\n\n/**\n * Discovery: lists, search, typeahead, similar and recommended contents,\n * release dates, the reader's content filters and history, signals and exposures.\n */\nexport class DiscoveryClient {\n /** Local anonymous history, isolated from authenticated server history. */\n readonly browserSearches: BrowserSearchHistory;\n\n constructor(private readonly http: Http) {\n this.browserSearches = browserSearchHistory(http.mount(\"discovery\"));\n }\n\n /** Contents as cards, one per content item with the version that fits the reader. */\n list(q: ContentQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents\", { query: query(q) as never, signal });\n }\n /** A first page is kept among the reader's recent searches. */\n async search(q: SearchQuery, signal?: AbortSignal): Promise<ContentPage> {\n const emit = q.cursor ? null : this.http.captureChanges();\n const out: ContentPage = await call(this.http, \"GET\", \"/search\", { query: query(q) as never, signal });\n emit?.({ type: \"searches.changed\" });\n return out;\n }\n /** The reader's recent searches, newest first, since they last cleared them. */\n recentSearches(q: RecentSearchesQuery = {}, signal?: AbortSignal): Promise<RecentSearches> {\n return call(this.http, \"GET\", \"/me/searches\", { query: q, signal });\n }\n /** Clears one recent search, or all of them. */\n async clearSearches(o: { query?: string; session?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/searches\", { query: o });\n emit({ type: \"searches.changed\" });\n }\n /** Contents and groups while the reader types. */\n typeahead(q: TypeaheadQuery, signal?: AbortSignal): Promise<Typeahead> {\n return call(this.http, \"GET\", \"/typeahead\", { query: query(q) as never, signal });\n }\n /** Contents like one, of its kind unless q.kind names another. */\n similar(id: string, q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents/{id}/similar\", { params: { id }, query: query(q) as never, signal });\n }\n /** For the signed-in reader. */\n recommendations(q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/recommendations\", { query: query(q) as never, signal });\n }\n /** Per month or day, newest first: how many contents the reader's listing shows released then, and how many are free. */\n releaseDates(q: ReleaseDatesQuery = {}, signal?: AbortSignal): Promise<ReleaseDates> {\n return call(this.http, \"GET\", \"/release-dates\", { query: query(q) as never, signal });\n }\n /** Every visible card in each day; opening or expanding a day needs no further request. */\n releases(q: DailyReleasesQuery = {}, signal?: AbortSignal): Promise<DailyReleases> {\n return call(this.http, \"GET\", \"/releases\", { query: query(q) as never, signal });\n }\n /** Only the current domain's reader-toggleable entities, named in the requested language. */\n async contentFilters(language?: string, signal?: AbortSignal): Promise<ContentFilters> {\n const result = await this.http.request<ContentFilters>(routeURL(this.http, \"GET\", \"/content-filters\", { query: { language } }), { signal, guestPreferences: false });\n this.http.acceptGuestFilters(result.filters);\n return result;\n }\n /** Replaces this domain's reader settings; other domains' choices remain intact. */\n async setContentFilters(update: ContentFiltersUpdate, language?: string): Promise<ContentFilters> {\n const emit = this.http.captureChanges();\n const out: ContentFilters = await call(this.http, \"PUT\", \"/me/content-filters\", { query: { language }, body: update });\n emit({ type: \"contentFilters.changed\" });\n return out;\n }\n /** What the reader engaged with, most recent first. */\n history(q: HistoryQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/me/history\", { query: query(q) as never, signal });\n }\n /** Erases the reader's history: of one content item, one kind or everything. */\n async forgetHistory(o: { kind?: string; content_id?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/history\", { query: o });\n emit({ type: \"history.changed\" });\n }\n /** Views and clicks; signed out, of the session named. */\n signals(signals: SignalInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Signals = { session, signals };\n return call(this.http, \"POST\", \"/signals\", { body, keepalive: o.keepalive });\n }\n /** Result lists served, rendered or shown, for click attribution. */\n exposures(exposures: ExposureInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Exposures = { session, exposures };\n return call(this.http, \"POST\", \"/exposures\", { body, keepalive: o.keepalive });\n }\n}\n"],"mappings":";;;AA6EA,SAAgB,EAAa,IAAwB,MAAM,IAAwB,CAAC,GAAiB;CACnG,OAAO;EAAE,GAAG;EAAS,MAAM,WAAW;EAAU,kBAAkB;CAAO;AAC3E;AAkCA,IAAM,oBAAQ,IAAI,IAAI;CAAC;CAAU;CAAkB;CAAY;CAAU;CAAS;CAAU;CAAiB;AAAc,CAAC;AAG5H,SAAgB,EAAM,GAAsF;CAC1G,IAAM,IAAiF,CAAC;CACxF,KAAK,IAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,CAAC,GAAG,EAAI,KAAK,EAAM,IAAI,CAAC,KAAK,MAAM,QAAQ,CAAC,IAAK,EAAE,SAAS,EAAE,KAAK,GAAG,IAAI,KAAA,IAAa;CAC3H,OAAO;AACT;AAMA,IAAa,IAAb,MAA6B;CAIE;CAF7B;CAEA,YAAY,GAA6B;EACvC,AAD2B,KAAA,OAAA,GAC3B,KAAK,kBAAkB,EAAqB,EAAK,MAAM,WAAW,CAAC;CACrE;CAGA,KAAK,IAAkB,CAAC,GAAG,GAA4C;EACrE,OAAO,EAAK,KAAK,MAAM,OAAO,aAAa;GAAE,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;CACjF;CAEA,MAAM,OAAO,GAAgB,GAA4C;EACvE,IAAM,IAAO,EAAE,SAAS,OAAO,KAAK,KAAK,eAAe,GAClD,IAAmB,MAAM,EAAK,KAAK,MAAM,OAAO,WAAW;GAAE,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;EAErG,OADA,IAAO,EAAE,MAAM,mBAAmB,CAAC,GAC5B;CACT;CAEA,eAAe,IAAyB,CAAC,GAAG,GAA+C;EACzF,OAAO,EAAK,KAAK,MAAM,OAAO,gBAAgB;GAAE,OAAO;GAAG;EAAO,CAAC;CACpE;CAEA,MAAM,cAAc,IAA0C,CAAC,GAAkB;EAC/E,IAAM,IAAO,KAAK,KAAK,eAAe;EAEtC,AADA,MAAM,EAAK,KAAK,MAAM,UAAU,gBAAgB,EAAE,OAAO,EAAE,CAAC,GAC5D,EAAK,EAAE,MAAM,mBAAmB,CAAC;CACnC;CAEA,UAAU,GAAmB,GAA0C;EACrE,OAAO,EAAK,KAAK,MAAM,OAAO,cAAc;GAAE,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;CAClF;CAEA,QAAQ,GAAY,IAAe,CAAC,GAAG,GAA4C;EACjF,OAAO,EAAK,KAAK,MAAM,OAAO,0BAA0B;GAAE,QAAQ,EAAE,MAAG;GAAG,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;CAC9G;CAEA,gBAAgB,IAAe,CAAC,GAAG,GAA4C;EAC7E,OAAO,EAAK,KAAK,MAAM,OAAO,oBAAoB;GAAE,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;CACxF;CAEA,aAAa,IAAuB,CAAC,GAAG,GAA6C;EACnF,OAAO,EAAK,KAAK,MAAM,OAAO,kBAAkB;GAAE,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;CACtF;CAEA,SAAS,IAAwB,CAAC,GAAG,GAA8C;EACjF,OAAO,EAAK,KAAK,MAAM,OAAO,aAAa;GAAE,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;CACjF;CAEA,MAAM,eAAe,GAAmB,GAA+C;EACrF,IAAM,IAAS,MAAM,KAAK,KAAK,QAAwB,EAAS,KAAK,MAAM,OAAO,oBAAoB,EAAE,OAAO,EAAE,YAAS,EAAE,CAAC,GAAG;GAAE;GAAQ,kBAAkB;EAAM,CAAC;EAEnK,OADA,KAAK,KAAK,mBAAmB,EAAO,OAAO,GACpC;CACT;CAEA,MAAM,kBAAkB,GAA8B,GAA4C;EAChG,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAsB,MAAM,EAAK,KAAK,MAAM,OAAO,uBAAuB;GAAE,OAAO,EAAE,YAAS;GAAG,MAAM;EAAO,CAAC;EAErH,OADA,EAAK,EAAE,MAAM,yBAAyB,CAAC,GAChC;CACT;CAEA,QAAQ,IAAkB,CAAC,GAAG,GAA4C;EACxE,OAAO,EAAK,KAAK,MAAM,OAAO,eAAe;GAAE,OAAO,EAAM,CAAC;GAAY;EAAO,CAAC;CACnF;CAEA,MAAM,cAAc,IAA4C,CAAC,GAAkB;EACjF,IAAM,IAAO,KAAK,KAAK,eAAe;EAEtC,AADA,MAAM,EAAK,KAAK,MAAM,UAAU,eAAe,EAAE,OAAO,EAAE,CAAC,GAC3D,EAAK,EAAE,MAAM,kBAAkB,CAAC;CAClC;CAEA,QAAQ,GAAwB,GAAkB,IAA6B,CAAC,GAAkB;EAChG,IAAM,IAAgB;GAAE;GAAS;EAAQ;EACzC,OAAO,EAAK,KAAK,MAAM,QAAQ,YAAY;GAAE;GAAM,WAAW,EAAE;EAAU,CAAC;CAC7E;CAEA,UAAU,GAA4B,GAAkB,IAA6B,CAAC,GAAkB;EACtG,IAAM,IAAkB;GAAE;GAAS;EAAU;EAC7C,OAAO,EAAK,KAAK,MAAM,QAAQ,cAAc;GAAE;GAAM,WAAW,EAAE;EAAU,CAAC;CAC/E;AACF"}
1
+ {"version":3,"file":"discovery.js","names":[],"sources":["../../src/client/discovery.ts"],"sourcesContent":["import type {\n ActivityContexts,\n ActivityContextsInput,\n ActivityTarget,\n ContentFilters,\n ContentFiltersUpdate,\n ContentPage,\n DailyReleases,\n ExposureInput,\n Exposures,\n RecentSearches,\n ReleaseDates,\n SignalInput,\n Signals,\n Typeahead,\n} from \"./generated/wire.js\";\nimport type { Http } from \"./http.js\";\nimport { call, routeURL } from \"./route.js\";\nimport { browserSearchHistory, type BrowserSearchHistory } from \"./browser-search-history.js\";\n\n/** A popularity period. */\nexport type Period = \"7d\" | \"30d\" | \"90d\" | \"365d\" | \"all\";\n/** A list's order; position orders by the first of `groups` (an ordered group). */\nexport type ListSort = \"newest\" | \"oldest\" | \"title\" | \"popular\" | `popular:${Period}` | \"top_rated\" | `top_rated:${Period}` | \"most_favorited\" | \"most_viewed\" | \"position\" | \"random\";\n/** How locked contents list: marked (lock, the default) or left out (hide). */\nexport type AccessMode = \"lock\" | \"hide\";\n\n/** Which contents a list holds. */\nexport interface ContentFilterQuery {\n kind?: string;\n /** Group ids every listed version is in. */\n groups?: readonly string[];\n /** Group ids no listed version is in. */\n exclude_groups?: readonly string[];\n /** Only contents this group owns: a channel's feed. */\n owner_group?: string;\n /** User ids: only contents one of them is credited on. */\n creators?: readonly string[];\n /** public (anyone opens it now) or the level it stays locked at. */\n access?: readonly (\"public\" | \"tier\" | \"group\" | \"ppv\" | \"group_ppv\")[];\n /** Only contents locked behind one of these tiers. */\n tiers?: readonly string[];\n /** Version traits every listed version has. */\n traits?: readonly string[];\n /** \"name:value\" of the kind's filterable attributes. */\n attribute?: readonly string[];\n /** Published on this day (YYYY-MM-DD) or month (YYYY-MM). */\n released_on?: string;\n /** Only contents published within this age window; independent of the popularity score window. */\n published_within?: Period;\n /** IANA time zone of released_on and release dates; UTC by default. */\n tz?: string;\n /** The signed-in reader's unseen, in-progress or completed contents. */\n view_status?: \"unseen\" | \"in_progress\" | \"completed\";\n}\n\n/** How a list reads to its reader. */\nexport interface ReaderQuery {\n /** Only content with a live version in this language. */\n language?: string;\n /** Titles, names and paths in this language, and each card's preferred version's; language by default. */\n display_language?: string;\n mode?: AccessMode;\n}\n\n/** The group kinds cards name; every kind with a page by default. */\nexport interface CardQuery {\n card_entities?: readonly string[];\n}\n\nexport interface PageQuery {\n cursor?: string;\n limit?: number;\n}\n\nexport type ContentQuery = ContentFilterQuery & ReaderQuery & CardQuery & PageQuery & {\n sort?: ListSort;\n /** Inclusive UTC date (YYYY-MM-DD); requires activity_until, kind and sort=popular. */\n activity_from?: string;\n /** Exclusive UTC date (YYYY-MM-DD); requires activity_from, kind and sort=popular. */\n activity_until?: string;\n};\nexport type PopularPeriod = \"7d\" | \"90d\" | \"365d\" | \"all\";\nexport type PopularQuery = Omit<ContentQuery, \"sort\" | \"activity_from\" | \"activity_until\">;\n\n/** Rank activity in the selected window; publication age is an optional separate filter. */\nexport function popularQuery(period: PopularPeriod = \"7d\", filters: PopularQuery = {}): ContentQuery {\n return { ...filters, sort: `popular:${period}` };\n}\n\n/** Rank activity between two UTC dates: from inclusive, until exclusive. */\nexport function popularBetween(from: string, until: string, filters: PopularQuery & { kind: string }): ContentQuery {\n return { ...filters, sort: \"popular\", activity_from: from, activity_until: until };\n}\n\nexport type SearchQuery = ContentFilterQuery &\n ReaderQuery &\n CardQuery &\n PageQuery & {\n q: string;\n sort?: \"relevance\" | ListSort;\n personalize?: boolean;\n /** Explicit legacy server-side anonymous history. Omit for public search without server history. */\n session?: string;\n };\nexport interface RecentSearchesQuery {\n /** Explicit legacy server-side anonymous history; no anonymous session is created by default. */\n session?: string;\n /** 10 by default, at most 100. */\n limit?: number;\n}\nexport type RailQuery = ContentFilterQuery & ReaderQuery & CardQuery & { limit?: number };\nexport type ReleaseDatesQuery = ContentFilterQuery & ReaderQuery & PageQuery & { by?: \"month\" | \"day\" };\n/** Complete daily metadata for one calendar window, without pagination. */\nexport type DailyReleasesQuery = Omit<ContentFilterQuery, \"released_on\" | \"tz\"> & ReaderQuery & CardQuery & { days?: number; time_zone?: string };\nexport type HistoryQuery = ReaderQuery & CardQuery & PageQuery & { kind?: string; status?: \"seen\" | \"in_progress\" | \"completed\" };\nexport interface TypeaheadQuery extends ReaderQuery {\n q: string;\n /** Only contents of this kind. */\n kind?: string;\n /** The group kinds suggested; every kind with a page by default. */\n entity_kinds?: readonly string[];\n /** Per section: 8 by default, at most 20. */\n limit?: number;\n}\n\n// Comma-joined list parameters; `attribute` repeats instead.\nconst LISTS = new Set([\"groups\", \"exclude_groups\", \"creators\", \"access\", \"tiers\", \"traits\", \"card_entities\", \"entity_kinds\"]);\n\n/** A query with its list parameters comma-joined, as discovery routes read them. */\nexport function query(q: object): Record<string, string | number | boolean | readonly string[] | undefined> {\n const out: Record<string, string | number | boolean | readonly string[] | undefined> = {};\n for (const [k, v] of Object.entries(q)) out[k] = LISTS.has(k) && Array.isArray(v) ? (v.length ? v.join(\",\") : undefined) : v;\n return out;\n}\n\n/**\n * Discovery: lists, search, typeahead, similar and recommended contents,\n * release dates, the reader's content filters and history, signals and exposures.\n */\nexport class DiscoveryClient {\n /** Local anonymous history, isolated from authenticated server history. */\n readonly browserSearches: BrowserSearchHistory;\n\n constructor(private readonly http: Http) {\n this.browserSearches = browserSearchHistory(http.mount(\"discovery\"));\n }\n\n /** Contents as cards, one per content item with the version that fits the reader. */\n list(q: ContentQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents\", { query: query(q) as never, signal });\n }\n /** A first page is kept among the reader's recent searches. */\n async search(q: SearchQuery, signal?: AbortSignal): Promise<ContentPage> {\n const emit = q.cursor ? null : this.http.captureChanges();\n const out: ContentPage = await call(this.http, \"GET\", \"/search\", { query: query(q) as never, signal });\n emit?.({ type: \"searches.changed\" });\n return out;\n }\n /** The reader's recent searches, newest first, since they last cleared them. */\n recentSearches(q: RecentSearchesQuery = {}, signal?: AbortSignal): Promise<RecentSearches> {\n return call(this.http, \"GET\", \"/me/searches\", { query: q, signal });\n }\n /** Clears one recent search, or all of them. */\n async clearSearches(o: { query?: string; session?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/searches\", { query: o });\n emit({ type: \"searches.changed\" });\n }\n /** Contents and groups while the reader types. */\n typeahead(q: TypeaheadQuery, signal?: AbortSignal): Promise<Typeahead> {\n return call(this.http, \"GET\", \"/typeahead\", { query: query(q) as never, signal });\n }\n /** Contents like one, of its kind unless q.kind names another. */\n similar(id: string, q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents/{id}/similar\", { params: { id }, query: query(q) as never, signal });\n }\n /** For the signed-in reader. */\n recommendations(q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/recommendations\", { query: query(q) as never, signal });\n }\n /** Per month or day, newest first: how many contents the reader's listing shows released then, and how many are free. */\n releaseDates(q: ReleaseDatesQuery = {}, signal?: AbortSignal): Promise<ReleaseDates> {\n return call(this.http, \"GET\", \"/release-dates\", { query: query(q) as never, signal });\n }\n /** Every visible card in each day; opening or expanding a day needs no further request. */\n releases(q: DailyReleasesQuery = {}, signal?: AbortSignal): Promise<DailyReleases> {\n return call(this.http, \"GET\", \"/releases\", { query: query(q) as never, signal });\n }\n /** Only the current domain's reader-toggleable entities, named in the requested language. */\n async contentFilters(language?: string, signal?: AbortSignal): Promise<ContentFilters> {\n const result = await this.http.request<ContentFilters>(routeURL(this.http, \"GET\", \"/content-filters\", { query: { language } }), { signal, guestPreferences: false });\n this.http.acceptGuestFilters(result.kinds);\n return result;\n }\n /** Replaces this domain's reader settings; other domains' choices remain intact. */\n async setContentFilters(update: ContentFiltersUpdate, language?: string): Promise<ContentFilters> {\n const emit = this.http.captureChanges();\n const out: ContentFilters = await call(this.http, \"PUT\", \"/me/content-filters\", { query: { language }, body: update });\n emit({ type: \"contentFilters.changed\" });\n return out;\n }\n /** What the reader engaged with, most recent first. */\n history(q: HistoryQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/me/history\", { query: query(q) as never, signal });\n }\n /** Erases the reader's history: of one content item, one kind or everything. */\n async forgetHistory(o: { kind?: string; content_id?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/history\", { query: o });\n emit({ type: \"history.changed\" });\n }\n /** Capture server-owned access and eligibility at an observation's start. */\n activityContexts(targets: readonly ActivityTarget[], session?: string, language = this.http.language()): Promise<ActivityContexts> {\n const body: ActivityContextsInput = { targets: [...targets], session, language };\n return call(this.http, \"POST\", \"/activity-contexts\", { body });\n }\n /** Views and clicks; signed out, of the session named. */\n signals(signals: SignalInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Signals = { session, signals };\n return call(this.http, \"POST\", \"/signals\", { body, keepalive: o.keepalive });\n }\n /** Result lists served, rendered or shown, for click attribution. */\n exposures(exposures: ExposureInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Exposures = { session, exposures };\n return call(this.http, \"POST\", \"/exposures\", { body, keepalive: o.keepalive });\n }\n}\n"],"mappings":"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"}
@@ -1852,6 +1852,19 @@ export declare const CONTENTKIT_ROUTES: readonly [{
1852
1852
  };
1853
1853
  readonly errors: readonly [];
1854
1854
  readonly errorSets: readonly ["module:offers", "auth", "request"];
1855
+ }, {
1856
+ readonly method: "POST";
1857
+ readonly module: "discovery";
1858
+ readonly path: "/activity-contexts";
1859
+ readonly auth: "public";
1860
+ readonly permission: null;
1861
+ readonly query: readonly [];
1862
+ readonly request: "ActivityContextsInput";
1863
+ readonly responses: {
1864
+ readonly 200: "ActivityContexts";
1865
+ };
1866
+ readonly errors: readonly ["not_configured"];
1867
+ readonly errorSets: readonly ["module:discovery", "auth", "request"];
1855
1868
  }, {
1856
1869
  readonly method: "GET";
1857
1870
  readonly module: "discovery";
@@ -1871,7 +1884,7 @@ export declare const CONTENTKIT_ROUTES: readonly [{
1871
1884
  readonly path: "/contents";
1872
1885
  readonly auth: "public";
1873
1886
  readonly permission: null;
1874
- readonly query: readonly ["kind", "groups", "exclude_groups", "owner_group", "creators", "access", "tiers", "traits", "attribute", "released_on", "published_within", "tz", "view_status", "language", "display_language", "mode", "sort", "card_entities", "cursor", "limit"];
1887
+ readonly query: readonly ["kind", "groups", "exclude_groups", "owner_group", "creators", "access", "tiers", "traits", "attribute", "released_on", "published_within", "tz", "view_status", "language", "display_language", "mode", "sort", "card_entities", "activity_from", "activity_until", "cursor", "limit"];
1875
1888
  readonly request: null;
1876
1889
  readonly responses: {
1877
1890
  readonly 200: "ContentPage";
@@ -2573,6 +2573,22 @@ var e = {
2573
2573
  "request"
2574
2574
  ]
2575
2575
  },
2576
+ {
2577
+ method: "POST",
2578
+ module: "discovery",
2579
+ path: "/activity-contexts",
2580
+ auth: "public",
2581
+ permission: null,
2582
+ query: [],
2583
+ request: "ActivityContextsInput",
2584
+ responses: { 200: "ActivityContexts" },
2585
+ errors: ["not_configured"],
2586
+ errorSets: [
2587
+ "module:discovery",
2588
+ "auth",
2589
+ "request"
2590
+ ]
2591
+ },
2576
2592
  {
2577
2593
  method: "GET",
2578
2594
  module: "discovery",
@@ -2632,6 +2648,8 @@ var e = {
2632
2648
  "mode",
2633
2649
  "sort",
2634
2650
  "card_entities",
2651
+ "activity_from",
2652
+ "activity_until",
2635
2653
  "cursor",
2636
2654
  "limit"
2637
2655
  ],