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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +105 -43
  2. package/dist/client/beacons.d.ts +3 -13
  3. package/dist/client/beacons.js +4 -45
  4. package/dist/client/beacons.js.map +1 -1
  5. package/dist/client/client.d.ts +3 -0
  6. package/dist/client/client.js +45 -43
  7. package/dist/client/client.js.map +1 -1
  8. package/dist/client/content/favorites.d.ts +2 -0
  9. package/dist/client/content/favorites.js.map +1 -1
  10. package/dist/client/content/types.d.ts +3 -0
  11. package/dist/client/content/types.js.map +1 -1
  12. package/dist/client/discovery.d.ts +28 -3
  13. package/dist/client/discovery.js +22 -1
  14. package/dist/client/discovery.js.map +1 -1
  15. package/dist/client/engagement.d.ts +70 -0
  16. package/dist/client/engagement.js +191 -0
  17. package/dist/client/engagement.js.map +1 -0
  18. package/dist/client/generated/routes.d.ts +35 -9
  19. package/dist/client/generated/routes.js +59 -2
  20. package/dist/client/generated/routes.js.map +1 -1
  21. package/dist/client/generated/wire.d.ts +53 -12
  22. package/dist/client/generated/wire.js.map +1 -1
  23. package/dist/client/guest-reading-history.d.ts +40 -0
  24. package/dist/client/guest-reading-history.js +144 -0
  25. package/dist/client/guest-reading-history.js.map +1 -0
  26. package/dist/client/index.d.ts +4 -2
  27. package/dist/client/stats.d.ts +5 -5
  28. package/dist/client/stats.js.map +1 -1
  29. package/dist/client.js +24 -22
  30. package/dist/components/admin/content-manager.d.ts +1 -1
  31. package/dist/components/admin/content-manager.js +342 -302
  32. package/dist/components/admin/content-manager.js.map +1 -1
  33. package/dist/components/admin/page-review.js +106 -97
  34. package/dist/components/admin/page-review.js.map +1 -1
  35. package/dist/components/admin/quick-search.d.ts +14 -0
  36. package/dist/components/admin/quick-search.js +60 -0
  37. package/dist/components/admin/quick-search.js.map +1 -0
  38. package/dist/components/admin/reader-content-editor.d.ts +6 -0
  39. package/dist/components/admin/reader-content-editor.js +733 -0
  40. package/dist/components/admin/reader-content-editor.js.map +1 -0
  41. package/dist/components/admin/reader-manifest.js +120 -105
  42. package/dist/components/admin/reader-manifest.js.map +1 -1
  43. package/dist/components/admin/stats-dashboard.d.ts +4 -4
  44. package/dist/components/admin/stats-dashboard.js +132 -134
  45. package/dist/components/admin/stats-dashboard.js.map +1 -1
  46. package/dist/components/content/daily-releases.js +143 -191
  47. package/dist/components/content/daily-releases.js.map +1 -1
  48. package/dist/components/content/editor-save.d.ts +32 -0
  49. package/dist/components/content/editor-save.js +51 -0
  50. package/dist/components/content/editor-save.js.map +1 -0
  51. package/dist/components/content/editors.d.ts +17 -2
  52. package/dist/components/content/editors.js +359 -328
  53. package/dist/components/content/editors.js.map +1 -1
  54. package/dist/components/content/favorites.d.ts +6 -0
  55. package/dist/components/content/favorites.js +33 -0
  56. package/dist/components/content/favorites.js.map +1 -0
  57. package/dist/components/content/filter-dialog.js +19 -15
  58. package/dist/components/content/filter-dialog.js.map +1 -1
  59. package/dist/components/content/filters.d.ts +2 -0
  60. package/dist/components/content/filters.js +5 -1
  61. package/dist/components/content/filters.js.map +1 -1
  62. package/dist/components/content/grid.d.ts +34 -13
  63. package/dist/components/content/grid.js +300 -271
  64. package/dist/components/content/grid.js.map +1 -1
  65. package/dist/components/content/history.d.ts +6 -0
  66. package/dist/components/content/history.js +91 -0
  67. package/dist/components/content/history.js.map +1 -0
  68. package/dist/components/content/index.d.ts +1 -0
  69. package/dist/components/content/offer-fields.d.ts +1 -1
  70. package/dist/components/content/offer-fields.js +86 -64
  71. package/dist/components/content/offer-fields.js.map +1 -1
  72. package/dist/components/content/search.d.ts +11 -4
  73. package/dist/components/content/search.js +22 -11
  74. package/dist/components/content/search.js.map +1 -1
  75. package/dist/components/content/summary-tile.d.ts +14 -0
  76. package/dist/components/content/summary-tile.js +63 -0
  77. package/dist/components/content/summary-tile.js.map +1 -0
  78. package/dist/components/groups/memberships.d.ts +11 -1
  79. package/dist/components/groups/memberships.js +1 -1
  80. package/dist/components/groups/memberships.js.map +1 -1
  81. package/dist/components/media-folder-editor.d.ts +4 -0
  82. package/dist/components/media-folder-editor.js +178 -168
  83. package/dist/components/media-folder-editor.js.map +1 -1
  84. package/dist/components/video-mini-player.d.ts +4 -1
  85. package/dist/components/video-mini-player.js +77 -71
  86. package/dist/components/video-mini-player.js.map +1 -1
  87. package/dist/components/video-player.d.ts +4 -1
  88. package/dist/components/video-player.js +280 -276
  89. package/dist/components/video-player.js.map +1 -1
  90. package/dist/index.d.ts +1 -0
  91. package/dist/index.js +17 -14
  92. package/dist/locales/admin/de.js +33 -8
  93. package/dist/locales/admin/de.js.map +1 -1
  94. package/dist/locales/admin/en.d.ts +29 -4
  95. package/dist/locales/admin/en.js +33 -8
  96. package/dist/locales/admin/en.js.map +1 -1
  97. package/dist/locales/admin/es.js +33 -8
  98. package/dist/locales/admin/es.js.map +1 -1
  99. package/dist/locales/admin/ja.js +33 -8
  100. package/dist/locales/admin/ja.js.map +1 -1
  101. package/dist/locales/admin/ko.js +33 -8
  102. package/dist/locales/admin/ko.js.map +1 -1
  103. package/dist/locales/admin/zh.js +33 -8
  104. package/dist/locales/admin/zh.js.map +1 -1
  105. package/dist/locales/discovery/de.js +0 -1
  106. package/dist/locales/discovery/de.js.map +1 -1
  107. package/dist/locales/discovery/en.d.ts +0 -1
  108. package/dist/locales/discovery/en.js +0 -1
  109. package/dist/locales/discovery/en.js.map +1 -1
  110. package/dist/locales/discovery/es.js +0 -1
  111. package/dist/locales/discovery/es.js.map +1 -1
  112. package/dist/locales/discovery/ja.js +0 -1
  113. package/dist/locales/discovery/ja.js.map +1 -1
  114. package/dist/locales/discovery/ko.js +0 -1
  115. package/dist/locales/discovery/ko.js.map +1 -1
  116. package/dist/locales/discovery/zh.js +0 -1
  117. package/dist/locales/discovery/zh.js.map +1 -1
  118. package/dist/locales/en.d.ts +29 -5
  119. package/dist/react/discovery.d.ts +20 -17
  120. package/dist/react/discovery.js +273 -221
  121. package/dist/react/discovery.js.map +1 -1
  122. package/dist/react/index.d.ts +2 -1
  123. package/dist/react/page-review.d.ts +1 -1
  124. package/dist/react/page-review.js +6 -5
  125. package/dist/react/page-review.js.map +1 -1
  126. package/dist/react/resources.d.ts +3 -1
  127. package/dist/react/resources.js +4 -0
  128. package/dist/react/resources.js.map +1 -1
  129. package/dist/react/stats.d.ts +2 -2
  130. package/dist/react/stats.js.map +1 -1
  131. package/dist/react/use-draft.d.ts +8 -0
  132. package/dist/react/use-draft.js +35 -0
  133. package/dist/react/use-draft.js.map +1 -0
  134. package/dist/react/use-engagement.d.ts +26 -0
  135. package/dist/react/use-engagement.js +221 -0
  136. package/dist/react/use-engagement.js.map +1 -0
  137. package/dist/react/use-resource.d.ts +4 -3
  138. package/dist/react/use-resource.js +2 -1
  139. package/dist/react/use-resource.js.map +1 -1
  140. package/dist/react.js +12 -11
  141. package/dist/styles.css +1 -1
  142. package/dist/ui/dialog.d.ts +2 -1
  143. package/dist/ui/dialog.js +23 -20
  144. package/dist/ui/dialog.js.map +1 -1
  145. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../../src/client/content/types.ts"],"sourcesContent":["import type { CommentBan, CommentStanding, Comment, Content, Poll, ReactionCounts, ResolveInput } from \"../generated/wire.js\";\n\n/** What an interaction targets: a content item (optionally one of its versions) or a group. */\nexport interface Target {\n type: \"contents\" | \"entities\";\n id: string;\n /** One version of a content item. */\n version?: string;\n}\n\n/** The same target. */\nexport function sameTarget(a?: Target, b?: Target): boolean {\n return !!a && !!b && a.type === b.type && a.id === b.id && (a.version ?? \"\") === (b.version ?? \"\");\n}\n\n/** A reaction: like 1, dislike -1, none 0. */\nexport type Reaction = -1 | 0 | 1;\n\n/** The route verb that sets a like or dislike; clearing is DELETE …/reaction. */\nexport function reactionVerb(value: -1 | 1): \"like\" | \"dislike\" {\n return value === 1 ? \"like\" : \"dislike\";\n}\n\n/** Comment order: newest (the default), likes (most liked) or best (Wilson lower bound). */\nexport type Sort = \"newest\" | \"likes\" | \"best\";\n\n/** A ban route family: \"owner\" bans from the caller's own content, \"global\" from the whole site. */\nexport type BanScope = CommentStanding[\"ban_scopes\"][number];\n\n/** What the review queue holds: comments and content items. */\nexport type HeldKind = \"comment\" | \"content\";\n\n/** The moderation rules: banned words or IP bans. */\nexport type RuleSet = \"words\" | \"ips\";\n\n/** A reviewer's decision on a held item. */\nexport type Decision = Exclude<ResolveInput[\"decision\"], \"review\">;\n\n/** A content-module mutation made through the client. */\nexport type ContentChange =\n /** Created or edited: the item as the server now answers it. */\n | { type: \"content.saved\"; content: Content }\n /** A version changed: refetch the item. */\n | { type: \"content.changed\"; id: string }\n | { type: \"content.deleted\"; id: string }\n /** target is the commented item. */\n | { type: \"comment.created\"; target: Target; comment: Comment }\n | { type: \"comment.updated\"; comment: Comment }\n | { type: \"comment.deleted\"; id: string }\n | { type: \"comment.restored\"; id: string }\n | { type: \"comment.reacted\"; id: string; counts: ReactionCounts }\n | { type: \"reaction.changed\"; target: Target; counts: ReactionCounts }\n /** count is absent where the target's kind's locked view hides it. */\n | { type: \"favorite.changed\"; target: Target; favorited: boolean; count?: number }\n /** Created, updated, voted or answered: the poll as the server now answers it. */\n | { type: \"poll.created\"; poll: Poll }\n | { type: \"poll.updated\"; poll: Poll }\n /** An option or image changed: refetch the poll. */\n | { type: \"poll.changed\"; id: string }\n | { type: \"poll.deleted\"; id: string }\n | { type: \"ban.saved\"; scope: BanScope; ban: CommentBan }\n | { type: \"ban.lifted\"; scope: BanScope; user: string }\n | { type: \"moderation.resolved\"; kind: HeldKind; id: string; decision: Decision }\n /** Banned words or IP bans changed. */\n | { type: \"rules.changed\"; rules: RuleSet }\n /** A version's reader metadata changed: its reader plan reads again. */\n | { type: \"reader.changed\"; content: string; version: string }\n | { type: \"entities.changed\" }\n /** The reader's content filters changed: every list reads again. */\n | { type: \"contentFilters.changed\" }\n | { type: \"history.changed\" }\n /** A search ran or recent searches were cleared. */\n | { type: \"searches.changed\" }\n /** The reader's keys changed (a join, a purchase the host reports): reads of access re-run. */\n | { type: \"access.changed\" };\n"],"mappings":";AAWA,SAAgB,EAAW,GAAY,GAAqB;CAC1D,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,SAAS,EAAE,WAAW;AACjG;AAMA,SAAgB,EAAa,GAAmC;CAC9D,OAAO,MAAU,IAAI,SAAS;AAChC"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../../src/client/content/types.ts"],"sourcesContent":["import type { CommentBan, CommentStanding, Comment, Content, Poll, ReactionCounts, ResolveInput } from \"../generated/wire.js\";\n\n/** What an interaction targets: a content item (optionally one of its versions) or a group. */\nexport interface Target {\n type: \"contents\" | \"entities\";\n id: string;\n /** One version of a content item. */\n version?: string;\n}\n\n/** The same target. */\nexport function sameTarget(a?: Target, b?: Target): boolean {\n return !!a && !!b && a.type === b.type && a.id === b.id && (a.version ?? \"\") === (b.version ?? \"\");\n}\n\n/** A reaction: like 1, dislike -1, none 0. */\nexport type Reaction = -1 | 0 | 1;\n\n/** The route verb that sets a like or dislike; clearing is DELETE …/reaction. */\nexport function reactionVerb(value: -1 | 1): \"like\" | \"dislike\" {\n return value === 1 ? \"like\" : \"dislike\";\n}\n\n/** Comment order: newest (the default), likes (most liked) or best (Wilson lower bound). */\nexport type Sort = \"newest\" | \"likes\" | \"best\";\n\n/** A ban route family: \"owner\" bans from the caller's own content, \"global\" from the whole site. */\nexport type BanScope = CommentStanding[\"ban_scopes\"][number];\n\n/** What the review queue holds: comments and content items. */\nexport type HeldKind = \"comment\" | \"content\";\n\n/** The moderation rules: banned words or IP bans. */\nexport type RuleSet = \"words\" | \"ips\";\n\n/** A reviewer's decision on a held item. */\nexport type Decision = Exclude<ResolveInput[\"decision\"], \"review\">;\n\n/** A content-module mutation made through the client. */\nexport type ContentChange =\n /** Created or edited: the item as the server now answers it. */\n | { type: \"content.saved\"; content: Content }\n /** A version changed: refetch the item. */\n | { type: \"content.changed\"; id: string }\n | { type: \"content.deleted\"; id: string }\n /** target is the commented item. */\n | { type: \"comment.created\"; target: Target; comment: Comment }\n | { type: \"comment.updated\"; comment: Comment }\n | { type: \"comment.deleted\"; id: string }\n | { type: \"comment.restored\"; id: string }\n | { type: \"comment.reacted\"; id: string; counts: ReactionCounts }\n | { type: \"reaction.changed\"; target: Target; counts: ReactionCounts }\n /** count is absent where the target's kind's locked view hides it. */\n | { type: \"favorite.changed\"; target: Target; favorited: boolean; count?: number }\n /** Created, updated, voted or answered: the poll as the server now answers it. */\n | { type: \"poll.created\"; poll: Poll }\n | { type: \"poll.updated\"; poll: Poll }\n /** An option or image changed: refetch the poll. */\n | { type: \"poll.changed\"; id: string }\n | { type: \"poll.deleted\"; id: string }\n | { type: \"ban.saved\"; scope: BanScope; ban: CommentBan }\n | { type: \"ban.lifted\"; scope: BanScope; user: string }\n | { type: \"moderation.resolved\"; kind: HeldKind; id: string; decision: Decision }\n /** Banned words or IP bans changed. */\n | { type: \"rules.changed\"; rules: RuleSet }\n /** A version's reader metadata changed: its reader plan reads again. */\n | { type: \"reader.changed\"; content: string; version: string }\n | { type: \"entities.changed\" }\n /** The reader's content filters changed: every list reads again. */\n | { type: \"contentFilters.changed\" }\n | { type: \"history.changed\" }\n | { type: \"engagement.changed\"; ids: string[] }\n /** A search ran or recent searches were cleared. */\n | { type: \"searches.changed\" }\n /** The reader's keys changed (a join, a purchase the host reports): reads of access re-run. */\n | { type: \"access.changed\" };\n"],"mappings":";AAWA,SAAgB,EAAW,GAAY,GAAqB;CAC1D,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,SAAS,EAAE,WAAW;AACjG;AAMA,SAAgB,EAAa,GAAmC;CAC9D,OAAO,MAAU,IAAI,SAAS;AAChC"}
@@ -1,15 +1,17 @@
1
- import { ActivityContexts, ActivityTarget, ContentFilters, ContentFiltersUpdate, ContentPage, DailyReleases, ExposureInput, RecentSearches, ReleaseDates, SignalInput, Typeahead } from './generated/wire.js';
1
+ import { ActivityContexts, ActivityTarget, ContentFilters, ContentFiltersUpdate, ContentPage, DailyReleases, EngagementSnapshot, 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. */
5
5
  export type Period = "7d" | "30d" | "90d" | "365d" | "all";
6
6
  /** A list's order; position orders by the first of `groups` (an ordered group). */
7
- export type ListSort = "newest" | "oldest" | "title" | "popular" | `popular:${Period}` | "top_rated" | `top_rated:${Period}` | "most_favorited" | "most_viewed" | "position" | "random";
7
+ export type ListSort = "newest" | "oldest" | "title" | "popular" | `popular:${Period}` | "top_rated" | `top_rated:${Period}` | "most_favorited" | "position" | "random";
8
8
  /** How locked contents list: marked (lock, the default) or left out (hide). */
9
9
  export type AccessMode = "lock" | "hide";
10
10
  /** Which contents a list holds. */
11
11
  export interface ContentFilterQuery {
12
12
  kind?: string;
13
+ /** Public card hydration under current domain/access/preferences; at most 100. */
14
+ ids?: readonly string[];
13
15
  /** Group ids every listed version is in. */
14
16
  groups?: readonly string[];
15
17
  /** Group ids no listed version is in. */
@@ -52,6 +54,8 @@ export interface PageQuery {
52
54
  limit?: number;
53
55
  }
54
56
  export type ContentQuery = ContentFilterQuery & ReaderQuery & CardQuery & PageQuery & {
57
+ /** Selection-wide metadata; requested only on the first page. */
58
+ summary?: boolean;
55
59
  sort?: ListSort;
56
60
  /** Inclusive UTC date (YYYY-MM-DD); requires activity_until, kind and sort=popular. */
57
61
  activity_from?: string;
@@ -75,6 +79,7 @@ export type SearchQuery = ContentFilterQuery & ReaderQuery & CardQuery & PageQue
75
79
  q: string;
76
80
  sort?: "relevance" | ListSort;
77
81
  personalize?: boolean;
82
+ summary?: boolean;
78
83
  /** Explicit legacy server-side anonymous history. Omit for public search without server history. */
79
84
  session?: string;
80
85
  };
@@ -86,6 +91,7 @@ export interface RecentSearchesQuery {
86
91
  }
87
92
  export type RailQuery = ContentFilterQuery & ReaderQuery & CardQuery & {
88
93
  limit?: number;
94
+ summary?: boolean;
89
95
  };
90
96
  export type ReleaseDatesQuery = ContentFilterQuery & ReaderQuery & PageQuery & {
91
97
  by?: "month" | "day";
@@ -100,6 +106,7 @@ export type DailyReleasesQuery = Omit<ContentFilterQuery, "released_on" | "tz">
100
106
  export type HistoryQuery = ReaderQuery & CardQuery & PageQuery & {
101
107
  kind?: string;
102
108
  status?: "seen" | "in_progress" | "completed";
109
+ summary?: boolean;
103
110
  };
104
111
  export interface TypeaheadQuery extends ReaderQuery {
105
112
  q: string;
@@ -110,6 +117,17 @@ export interface TypeaheadQuery extends ReaderQuery {
110
117
  /** Per section: 8 by default, at most 20. */
111
118
  limit?: number;
112
119
  }
120
+ /** Private editor navigation. Each requested section must be chosen explicitly. */
121
+ export interface AdminSuggestionsQuery {
122
+ q: string;
123
+ display_language?: string;
124
+ content_kinds?: readonly string[];
125
+ entity_kinds?: readonly string[];
126
+ include_contents: boolean;
127
+ include_entities: boolean;
128
+ /** Per section: 8 by default, at most 20. */
129
+ limit?: number;
130
+ }
113
131
  /** A query with its list parameters comma-joined, as discovery routes read them. */
114
132
  export declare function query(q: object): Record<string, string | number | boolean | readonly string[] | undefined>;
115
133
  /**
@@ -134,6 +152,8 @@ export declare class DiscoveryClient {
134
152
  }): Promise<void>;
135
153
  /** Contents and groups while the reader types. */
136
154
  typeahead(q: TypeaheadQuery, signal?: AbortSignal): Promise<Typeahead>;
155
+ /** Current names of authorized draft/hidden content and entities; no public search fallback. */
156
+ adminSuggestions(q: AdminSuggestionsQuery, signal?: AbortSignal): Promise<Typeahead>;
137
157
  /** Contents like one, of its kind unless q.kind names another. */
138
158
  similar(id: string, q?: RailQuery, signal?: AbortSignal): Promise<ContentPage>;
139
159
  /** For the signed-in reader. */
@@ -155,7 +175,12 @@ export declare class DiscoveryClient {
155
175
  }): Promise<void>;
156
176
  /** Capture server-owned access and eligibility at an observation's start. */
157
177
  activityContexts(targets: readonly ActivityTarget[], session?: string, language?: string | undefined): Promise<ActivityContexts>;
158
- /** Views and clicks; signed out, of the session named. */
178
+ /** Cumulative measured attention. Successful checkpoints refresh history/progress caches. */
179
+ engagement(snapshots: EngagementSnapshot[], o?: {
180
+ session?: string;
181
+ keepalive?: boolean;
182
+ }): Promise<void>;
183
+ /** Clicks and explicit feedback; signed out, of the visitor named. */
159
184
  signals(signals: SignalInput[], session?: string, o?: {
160
185
  keepalive?: boolean;
161
186
  }): Promise<void>;
@@ -19,6 +19,7 @@ function i(e, t, n) {
19
19
  }, n);
20
20
  }
21
21
  var a = /* @__PURE__ */ new Set([
22
+ "ids",
22
23
  "groups",
23
24
  "exclude_groups",
24
25
  "creators",
@@ -26,7 +27,8 @@ var a = /* @__PURE__ */ new Set([
26
27
  "tiers",
27
28
  "traits",
28
29
  "card_entities",
29
- "entity_kinds"
30
+ "entity_kinds",
31
+ "content_kinds"
30
32
  ]);
31
33
  function o(e) {
32
34
  let t = {};
@@ -68,6 +70,12 @@ var s = class {
68
70
  signal: n
69
71
  });
70
72
  }
73
+ adminSuggestions(t, n) {
74
+ return e(this.http, "GET", "/manage/search-suggestions", {
75
+ query: o(t),
76
+ signal: n
77
+ });
78
+ }
71
79
  similar(t, n = {}, r) {
72
80
  return e(this.http, "GET", "/contents/{id}/similar", {
73
81
  params: { id: t },
@@ -125,6 +133,19 @@ var s = class {
125
133
  };
126
134
  return e(this.http, "POST", "/activity-contexts", { body: i });
127
135
  }
136
+ async engagement(t, n = {}) {
137
+ let r = this.http.captureChanges(), i = {
138
+ session: n.session,
139
+ snapshots: t
140
+ };
141
+ await e(this.http, "POST", "/engagement", {
142
+ body: i,
143
+ keepalive: n.keepalive
144
+ }), r({
145
+ type: "engagement.changed",
146
+ ids: [...new Set(t.map((e) => e.content_id))]
147
+ });
148
+ }
128
149
  signals(t, n, r = {}) {
129
150
  let i = {
130
151
  session: n,
@@ -1 +1 @@
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\";\n/** Whole UTC dates: from inclusive, until exclusive. */\nexport interface PopularDateRange {\n from: string;\n until: string;\n}\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 | PopularDateRange = \"7d\", filters: PopularQuery = {}): ContentQuery {\n return typeof period === \"string\"\n ? { ...filters, sort: `popular:${period}` }\n : { ...filters, sort: \"popular\", activity_from: period.from, activity_until: period.until };\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 popularQuery({ from, until }, filters);\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 & {\n days?: number;\n /** Inclusive YYYY-MM-DD in time_zone; today by default. Future dates are rejected. */\n end_date?: string;\n time_zone?: string;\n};\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 /** Atomically changes submitted shared choices; omitted settings 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, \"PATCH\", \"/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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"}
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 EngagementBatch,\n EngagementSnapshot,\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\" | \"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 /** Public card hydration under current domain/access/preferences; at most 100. */\n ids?: readonly 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 /** Selection-wide metadata; requested only on the first page. */\n summary?: boolean;\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\";\n/** Whole UTC dates: from inclusive, until exclusive. */\nexport interface PopularDateRange {\n from: string;\n until: string;\n}\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 | PopularDateRange = \"7d\", filters: PopularQuery = {}): ContentQuery {\n return typeof period === \"string\"\n ? { ...filters, sort: `popular:${period}` }\n : { ...filters, sort: \"popular\", activity_from: period.from, activity_until: period.until };\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 popularQuery({ from, until }, filters);\n}\n\nexport type SearchQuery = ContentFilterQuery &\n ReaderQuery &\n CardQuery &\n PageQuery & {\n q: string;\n sort?: \"relevance\" | ListSort;\n personalize?: boolean;\n summary?: 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; summary?: boolean };\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 & {\n days?: number;\n /** Inclusive YYYY-MM-DD in time_zone; today by default. Future dates are rejected. */\n end_date?: string;\n time_zone?: string;\n};\nexport type HistoryQuery = ReaderQuery & CardQuery & PageQuery & { kind?: string; status?: \"seen\" | \"in_progress\" | \"completed\"; summary?: boolean };\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/** Private editor navigation. Each requested section must be chosen explicitly. */\nexport interface AdminSuggestionsQuery {\n q: string;\n display_language?: string;\n content_kinds?: readonly string[];\n entity_kinds?: readonly string[];\n include_contents: boolean;\n include_entities: boolean;\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([\"ids\", \"groups\", \"exclude_groups\", \"creators\", \"access\", \"tiers\", \"traits\", \"card_entities\", \"entity_kinds\", \"content_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 /** Current names of authorized draft/hidden content and entities; no public search fallback. */\n adminSuggestions(q: AdminSuggestionsQuery, signal?: AbortSignal): Promise<Typeahead> {\n return call(this.http, \"GET\", \"/manage/search-suggestions\", { 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 /** Atomically changes submitted shared choices; omitted settings 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, \"PATCH\", \"/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 /** Cumulative measured attention. Successful checkpoints refresh history/progress caches. */\n async engagement(snapshots: EngagementSnapshot[], o: { session?: string; keepalive?: boolean } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n const body: EngagementBatch = { session: o.session, snapshots };\n await call(this.http, \"POST\", \"/engagement\", { body, keepalive: o.keepalive });\n emit({ type: \"engagement.changed\", ids: [...new Set(snapshots.map(s => s.content_id))] });\n }\n /** Clicks and explicit feedback; signed out, of the visitor 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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"}
@@ -0,0 +1,70 @@
1
+ import { ActivityContext, CoverageRange, EngagementSnapshot } from './generated/wire.js';
2
+ export interface VisiblePage {
3
+ /** Zero-based index in the version's reading order. */
4
+ index: number;
5
+ /** The part actually on screen, in 0..10000 basis points of page height. */
6
+ ranges: readonly CoverageRange[];
7
+ }
8
+ export interface ReadingObservation {
9
+ pages?: readonly VisiblePage[];
10
+ video?: {
11
+ positionMs: number;
12
+ playing: boolean;
13
+ seeking?: boolean;
14
+ };
15
+ resume?: string;
16
+ }
17
+ export interface EngagementTrackerOptions {
18
+ contentId: string;
19
+ versionId?: string;
20
+ /** Acquired when attention begins, never by preloading or opening a route. */
21
+ begin: () => Promise<ActivityContext | null>;
22
+ submit: (snapshot: EngagementSnapshot, options: {
23
+ keepalive: boolean;
24
+ }) => Promise<void>;
25
+ onSnapshot?: (snapshot: EngagementSnapshot) => void;
26
+ idleAfter?: number;
27
+ checkpointInterval?: number;
28
+ now?: () => number;
29
+ id?: () => string;
30
+ }
31
+ /**
32
+ * One content visit, represented by cumulative, bounded attention snapshots.
33
+ * The clock and transport are injected so ownership and browser lifecycle stay
34
+ * outside the accounting. Repeated snapshots replace a revision, not add views.
35
+ */
36
+ export declare class EngagementTracker {
37
+ private readonly options;
38
+ readonly visitId: string;
39
+ private readonly now;
40
+ private readonly id;
41
+ private readonly idleAfter;
42
+ private readonly interval;
43
+ private last;
44
+ private lastActivity;
45
+ private attentive;
46
+ private observation;
47
+ private segment?;
48
+ private pending;
49
+ private tail;
50
+ private stopped;
51
+ private keepaliveRequested;
52
+ constructor(options: EngagementTrackerOptions);
53
+ /** Genuine input extends the reading window; a timer or preload does not. */
54
+ activity(): void;
55
+ attention(attentive: boolean): void;
56
+ observe(observation: ReadingObservation): void;
57
+ /** Tick even while the geometry is unchanged, so idle and checkpoint bounds hold. */
58
+ tick(): void;
59
+ /** Send accumulated attention; failed packets retain their exact revision for retry. */
60
+ flush(keepalive?: boolean): Promise<void>;
61
+ /** Account changes discard old ownership instead of sending under new credentials. */
62
+ discard(): void;
63
+ private startObservedSegment;
64
+ private receipt;
65
+ private createSegment;
66
+ private advance;
67
+ private record;
68
+ private capture;
69
+ private send;
70
+ }
@@ -0,0 +1,191 @@
1
+ //#region src/client/engagement.ts
2
+ var e = 12e4, t = () => typeof performance > "u" ? Date.now() : performance.now(), n = () => crypto.randomUUID(), r = (e, t, n = Infinity) => e !== void 0 && Number.isFinite(e) ? Math.max(1e3, Math.min(n, Math.floor(e))) : t;
3
+ function i(e, t = Infinity) {
4
+ let n = e.filter((e) => Number.isFinite(e.from) && Number.isFinite(e.to)).map(({ from: e, to: n }) => ({
5
+ from: Math.max(0, Math.round(e)),
6
+ to: Math.min(t, Math.round(n))
7
+ })).filter(({ from: e, to: t }) => t > e).sort((e, t) => e.from - t.from), r = [];
8
+ for (let e of n) {
9
+ let t = r.at(-1);
10
+ t && e.from <= t.to ? t.to = Math.max(t.to, e.to) : r.push({ ...e });
11
+ }
12
+ return r;
13
+ }
14
+ var a = class {
15
+ options;
16
+ visitId;
17
+ now;
18
+ id;
19
+ idleAfter;
20
+ interval;
21
+ last;
22
+ lastActivity;
23
+ attentive = !1;
24
+ observation = {};
25
+ segment;
26
+ pending = [];
27
+ tail = Promise.resolve();
28
+ stopped = !1;
29
+ keepaliveRequested = !1;
30
+ constructor(i) {
31
+ this.options = i, this.now = i.now ?? t, this.id = i.id ?? n, this.visitId = this.id(), this.idleAfter = r(i.idleAfter, 6e4), this.interval = r(i.checkpointInterval, e, e), this.last = this.lastActivity = this.now();
32
+ }
33
+ activity() {
34
+ this.advance(this.now()), this.lastActivity = this.now(), this.startObservedSegment();
35
+ }
36
+ attention(e) {
37
+ this.advance(this.now()), this.attentive = e, this.startObservedSegment();
38
+ }
39
+ observe(e) {
40
+ let t = this.now();
41
+ this.advance(t, e.video);
42
+ let n = 512, r = /* @__PURE__ */ new Map();
43
+ for (let t of e.pages ?? []) {
44
+ if (!Number.isSafeInteger(t.index) || t.index < 0 || r.size >= 128 || !n) continue;
45
+ let e = i(t.ranges, 1e4).slice(0, Math.min(16, n));
46
+ e.length && (r.set(t.index, {
47
+ index: t.index,
48
+ ranges: e
49
+ }), n -= e.length);
50
+ }
51
+ this.observation = {
52
+ ...e,
53
+ pages: [...r.values()]
54
+ }, this.startObservedSegment(), e.resume !== void 0 && this.segment && (this.segment.resume = e.resume, this.segment.changed = !0);
55
+ }
56
+ tick() {
57
+ this.advance(this.now()), this.startObservedSegment();
58
+ }
59
+ flush(e = !1) {
60
+ return this.stopped ? Promise.resolve() : (this.advance(this.now()), this.capture(), this.send(e));
61
+ }
62
+ discard() {
63
+ this.stopped = !0, this.segment = void 0, this.pending = [], this.observation = {};
64
+ }
65
+ startObservedSegment() {
66
+ let e = this.now();
67
+ !this.stopped && this.attentive && (this.observation.video?.playing || this.observation.pages?.length && e < this.lastActivity + this.idleAfter) && (this.segment ??= this.createSegment(e));
68
+ }
69
+ receipt() {
70
+ try {
71
+ return this.options.begin().catch(() => null);
72
+ } catch {
73
+ return Promise.resolve(null);
74
+ }
75
+ }
76
+ createSegment(e) {
77
+ return {
78
+ id: this.id(),
79
+ start: e,
80
+ active: 0,
81
+ pages: /* @__PURE__ */ new Map(),
82
+ watched: [],
83
+ revision: 0,
84
+ changed: !1,
85
+ resume: this.observation.resume,
86
+ receipt: this.receipt()
87
+ };
88
+ }
89
+ advance(e, t) {
90
+ if (this.stopped) return;
91
+ let n = this.last;
92
+ if (this.last = Math.max(n, e), e - n > 5e3) {
93
+ this.capture(), this.segment = void 0, this.send(!1);
94
+ return;
95
+ }
96
+ this.record(n, e, t, e), this.segment && e >= this.segment.start + this.interval && (this.capture(), this.segment = void 0, this.send(!1));
97
+ }
98
+ record(e, t, n, r) {
99
+ let a = this.observation.pages ?? [], o = this.observation.video, s = r - e, c = n && o ? n.positionMs - o.positionMs : 0, l = o?.playing && !o.seeking && n && !n.seeking && c > 0 && s > 0 && c <= s * 4 + 250;
100
+ if (!this.attentive || !a.length && !l) return;
101
+ let u = l ? t : Math.min(t, this.lastActivity + this.idleAfter), d = e;
102
+ for (; u > d;) {
103
+ this.segment ??= this.createSegment(d);
104
+ let t = Math.min(u, this.segment.start + this.interval), n = t - d;
105
+ if (n <= 0) {
106
+ this.capture(), this.segment = void 0, this.send(!1);
107
+ continue;
108
+ }
109
+ let r = l && o ? {
110
+ from: o.positionMs + c * (d - e) / s,
111
+ to: o.positionMs + c * (t - e) / s
112
+ } : void 0, f = new Map(this.segment.pages);
113
+ for (let e of a) f.set(e.index, {
114
+ index: e.index,
115
+ active_ms: 0,
116
+ ranges: i([...f.get(e.index)?.ranges ?? [], ...e.ranges], 1e4)
117
+ });
118
+ let p = r ? i([...this.segment.watched, r]) : this.segment.watched;
119
+ if (this.segment.active > 0 && (f.size > 128 || [...f.values()].some((e) => e.ranges.length > 16) || [...f.values()].reduce((e, t) => e + t.ranges.length, 0) > 512 || p.length > 256)) {
120
+ this.capture(), this.segment = void 0, this.send(!1);
121
+ continue;
122
+ }
123
+ if (this.segment.active += n, this.segment.changed = !0, this.segment.resume = this.observation.resume, a.length) {
124
+ let e = n / a.length;
125
+ for (let t of a) {
126
+ let n = this.segment.pages.get(t.index);
127
+ n || this.segment.pages.set(t.index, n = {
128
+ index: t.index,
129
+ ranges: [],
130
+ active_ms: 0
131
+ }), n.active_ms += e, n.ranges = i([...n.ranges, ...t.ranges], 1e4);
132
+ }
133
+ }
134
+ this.segment.watched = p, d = t, d >= this.segment.start + this.interval && (this.capture(), this.segment = void 0, this.send(!1));
135
+ }
136
+ }
137
+ capture() {
138
+ let e = this.segment;
139
+ if (!e?.changed || e.active < 1) return;
140
+ let t = [...e.pages.values()].filter((e) => e.active_ms >= 1).sort((e, t) => e.index - t.index).map((e) => ({
141
+ ...e,
142
+ active_ms: Math.floor(e.active_ms),
143
+ ranges: e.ranges.map((e) => ({ ...e }))
144
+ })), n = e.watched.map(({ from: e, to: t }) => ({
145
+ from_ms: e,
146
+ to_ms: t
147
+ }));
148
+ if (!t.length && !n.length) return;
149
+ e.changed = !1;
150
+ let r = {
151
+ content_id: this.options.contentId,
152
+ version_id: this.options.versionId,
153
+ visit_id: this.visitId,
154
+ segment_id: e.id,
155
+ revision: ++e.revision,
156
+ activity_token: "",
157
+ active_ms: Math.floor(e.active),
158
+ ...e.resume === void 0 ? {} : { resume: e.resume },
159
+ ...t.length ? { pages: t } : {},
160
+ ...n.length ? { watched: n } : {}
161
+ };
162
+ this.options.onSnapshot?.(r), this.pending = this.pending.filter((t) => t.snapshot.segment_id !== e.id), this.pending.push({
163
+ snapshot: r,
164
+ receipt: e.receipt
165
+ }), this.pending.length > 100 && this.pending.shift();
166
+ }
167
+ send(e) {
168
+ return this.keepaliveRequested ||= e, this.tail = this.tail.then(async () => {
169
+ for (let t of [...this.pending]) {
170
+ if (this.stopped) return;
171
+ let n = await t.receipt;
172
+ if (this.stopped) return;
173
+ if (!n) {
174
+ this.pending = this.pending.filter((e) => e !== t);
175
+ continue;
176
+ }
177
+ try {
178
+ await this.options.submit({
179
+ ...t.snapshot,
180
+ activity_token: n.token
181
+ }, { keepalive: e || this.keepaliveRequested }), this.pending = this.pending.filter((e) => e !== t);
182
+ } catch {}
183
+ }
184
+ this.pending.length || (this.keepaliveRequested = !1);
185
+ }), this.tail;
186
+ }
187
+ };
188
+ //#endregion
189
+ export { a as EngagementTracker };
190
+
191
+ //# sourceMappingURL=engagement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engagement.js","names":[],"sources":["../../src/client/engagement.ts"],"sourcesContent":["import type { ActivityContext, CoverageRange, EngagementSnapshot, PageEngagement, WatchedRange } from \"./generated/wire.js\";\n\nexport interface VisiblePage {\n /** Zero-based index in the version's reading order. */\n index: number;\n /** The part actually on screen, in 0..10000 basis points of page height. */\n ranges: readonly CoverageRange[];\n}\n\nexport interface ReadingObservation {\n pages?: readonly VisiblePage[];\n video?: { positionMs: number; playing: boolean; seeking?: boolean };\n resume?: string;\n}\n\nexport interface EngagementTrackerOptions {\n contentId: string;\n versionId?: string;\n /** Acquired when attention begins, never by preloading or opening a route. */\n begin: () => Promise<ActivityContext | null>;\n submit: (snapshot: EngagementSnapshot, options: { keepalive: boolean }) => Promise<void>;\n onSnapshot?: (snapshot: EngagementSnapshot) => void;\n idleAfter?: number;\n checkpointInterval?: number;\n now?: () => number;\n id?: () => string;\n}\n\nconst MAX_SEGMENT = 120_000;\nconst MAX_PENDING = 100;\nconst clock = () => typeof performance === \"undefined\" ? Date.now() : performance.now();\nconst randomId = () => crypto.randomUUID();\nconst durationOption = (value: number | undefined, fallback: number, max = Infinity) =>\n value !== undefined && Number.isFinite(value) ? Math.max(1000, Math.min(max, Math.floor(value))) : fallback;\n\n/** Coalesce coverage, so looking at the same page or video interval never inflates completion. */\nfunction union(ranges: readonly CoverageRange[], max = Infinity): CoverageRange[] {\n const ordered = ranges.filter((range) => Number.isFinite(range.from) && Number.isFinite(range.to))\n .map(({ from, to }) => ({ from: Math.max(0, Math.round(from)), to: Math.min(max, Math.round(to)) }))\n .filter(({ from, to }) => to > from).sort((a, b) => a.from - b.from);\n const merged: CoverageRange[] = [];\n for (const range of ordered) {\n const last = merged.at(-1);\n if (last && range.from <= last.to) last.to = Math.max(last.to, range.to);\n else merged.push({ ...range });\n }\n return merged;\n}\n\ninterface Segment {\n id: string;\n start: number;\n active: number;\n pages: Map<number, PageEngagement>;\n watched: CoverageRange[];\n revision: number;\n changed: boolean;\n resume?: string;\n receipt: Promise<ActivityContext | null>;\n}\n\n/**\n * One content visit, represented by cumulative, bounded attention snapshots.\n * The clock and transport are injected so ownership and browser lifecycle stay\n * outside the accounting. Repeated snapshots replace a revision, not add views.\n */\nexport class EngagementTracker {\n readonly visitId: string;\n private readonly now: () => number;\n private readonly id: () => string;\n private readonly idleAfter: number;\n private readonly interval: number;\n private last: number;\n private lastActivity: number;\n private attentive = false;\n private observation: ReadingObservation = {};\n private segment?: Segment;\n private pending: { snapshot: EngagementSnapshot; receipt: Promise<ActivityContext | null> }[] = [];\n private tail: Promise<void> = Promise.resolve();\n private stopped = false;\n private keepaliveRequested = false;\n\n constructor(private readonly options: EngagementTrackerOptions) {\n this.now = options.now ?? clock;\n this.id = options.id ?? randomId;\n this.visitId = this.id();\n this.idleAfter = durationOption(options.idleAfter, 60_000);\n this.interval = durationOption(options.checkpointInterval, MAX_SEGMENT, MAX_SEGMENT);\n this.last = this.lastActivity = this.now();\n }\n\n /** Genuine input extends the reading window; a timer or preload does not. */\n activity(): void {\n this.advance(this.now());\n this.lastActivity = this.now();\n this.startObservedSegment();\n }\n\n attention(attentive: boolean): void {\n this.advance(this.now());\n this.attentive = attentive;\n this.startObservedSegment();\n }\n\n observe(observation: ReadingObservation): void {\n const at = this.now();\n this.advance(at, observation.video);\n let remaining = 512;\n const pages = new Map<number, VisiblePage>();\n for (const page of observation.pages ?? []) {\n if (!Number.isSafeInteger(page.index) || page.index < 0 || pages.size >= 128 || !remaining) continue;\n const ranges = union(page.ranges, 10_000).slice(0, Math.min(16, remaining));\n if (!ranges.length) continue;\n pages.set(page.index, { index: page.index, ranges });\n remaining -= ranges.length;\n }\n this.observation = { ...observation, pages: [...pages.values()] };\n this.startObservedSegment();\n if (observation.resume !== undefined && this.segment) {\n this.segment.resume = observation.resume;\n this.segment.changed = true;\n }\n }\n\n /** Tick even while the geometry is unchanged, so idle and checkpoint bounds hold. */\n tick(): void { this.advance(this.now()); this.startObservedSegment(); }\n\n /** Send accumulated attention; failed packets retain their exact revision for retry. */\n flush(keepalive = false): Promise<void> {\n if (this.stopped) return Promise.resolve();\n this.advance(this.now());\n this.capture();\n return this.send(keepalive);\n }\n\n /** Account changes discard old ownership instead of sending under new credentials. */\n discard(): void {\n this.stopped = true;\n this.segment = undefined;\n this.pending = [];\n this.observation = {};\n }\n\n private startObservedSegment(): void {\n const at = this.now();\n if (!this.stopped && this.attentive && (this.observation.video?.playing\n || (this.observation.pages?.length && at < this.lastActivity + this.idleAfter))) this.segment ??= this.createSegment(at);\n }\n\n private receipt(): Promise<ActivityContext | null> {\n try { return this.options.begin().catch(() => null); }\n catch { return Promise.resolve(null); }\n }\n\n private createSegment(start: number): Segment {\n return {\n id: this.id(), start, active: 0, pages: new Map(), watched: [], revision: 0, changed: false,\n resume: this.observation.resume,\n // Capture immediately; a later checkpoint must not rebind old attention\n // to a new access level. An unavailable receipt drops remote telemetry.\n receipt: this.receipt(),\n };\n }\n\n private advance(at: number, video?: ReadingObservation[\"video\"]): void {\n if (this.stopped) return;\n const previous = this.last;\n this.last = Math.max(previous, at);\n if (at - previous > 5000) {\n // A suspended tab or stalled timer cannot tell us what stayed visible or\n // which access cohort applied. Never backfill it under a new receipt.\n this.capture(); this.segment = undefined; void this.send(false);\n return;\n }\n this.record(previous, at, video, at);\n if (this.segment && at >= this.segment.start + this.interval) {\n this.capture(); this.segment = undefined; void this.send(false);\n }\n }\n\n private record(from: number, to: number, video: ReadingObservation[\"video\"], sampledAt: number): void {\n const pages = this.observation.pages ?? [];\n const previousVideo = this.observation.video;\n const duration = sampledAt - from;\n const delta = video && previousVideo ? video.positionMs - previousVideo.positionMs : 0;\n const playing = previousVideo?.playing && !previousVideo.seeking && video && !video.seeking\n && delta > 0 && duration > 0 && delta <= duration * 4 + 250;\n if (!this.attentive || (!pages.length && !playing)) return;\n const end = playing ? to : Math.min(to, this.lastActivity + this.idleAfter);\n let start = from;\n while (end > start) {\n this.segment ??= this.createSegment(start);\n const boundary = Math.min(end, this.segment.start + this.interval);\n const elapsed = boundary - start;\n if (elapsed <= 0) {\n this.capture(); this.segment = undefined; void this.send(false);\n continue;\n }\n // Real playback can be faster than 1x; discontinuous seeks are not watched.\n const watched = playing && previousVideo ? {\n from: previousVideo.positionMs + delta * (start - from) / duration,\n to: previousVideo.positionMs + delta * (boundary - from) / duration,\n } : undefined;\n const nextPages = new Map(this.segment.pages);\n for (const page of pages) nextPages.set(page.index, { index: page.index, active_ms: 0,\n ranges: union([...(nextPages.get(page.index)?.ranges ?? []), ...page.ranges], 10_000) });\n const nextWatched = watched ? union([...this.segment.watched, watched]) : this.segment.watched;\n if (this.segment.active > 0 && (nextPages.size > 128 || [...nextPages.values()].some((page) => page.ranges.length > 16)\n || [...nextPages.values()].reduce((total, page) => total + page.ranges.length, 0) > 512 || nextWatched.length > 256)) {\n this.capture(); this.segment = undefined; void this.send(false);\n continue;\n }\n this.segment.active += elapsed;\n this.segment.changed = true;\n this.segment.resume = this.observation.resume;\n if (pages.length) {\n const share = elapsed / pages.length;\n for (const page of pages) {\n let row = this.segment.pages.get(page.index);\n if (!row) this.segment.pages.set(page.index, (row = { index: page.index, ranges: [], active_ms: 0 }));\n row.active_ms += share;\n row.ranges = union([...row.ranges, ...page.ranges], 10_000);\n }\n }\n this.segment.watched = nextWatched;\n start = boundary;\n if (start >= this.segment.start + this.interval) {\n this.capture(); this.segment = undefined; void this.send(false);\n }\n }\n }\n\n private capture(): void {\n const segment = this.segment;\n if (!segment?.changed || segment.active < 1) return;\n const pages = [...segment.pages.values()].filter((page) => page.active_ms >= 1)\n .sort((a, b) => a.index - b.index).map((page) => ({ ...page, active_ms: Math.floor(page.active_ms), ranges: page.ranges.map((range) => ({ ...range })) }));\n const watched: WatchedRange[] = segment.watched.map(({ from, to }) => ({ from_ms: from, to_ms: to }));\n if (!pages.length && !watched.length) return;\n segment.changed = false;\n const snapshot: EngagementSnapshot = {\n content_id: this.options.contentId, version_id: this.options.versionId,\n visit_id: this.visitId, segment_id: segment.id, revision: ++segment.revision,\n activity_token: \"\", active_ms: Math.floor(segment.active),\n ...(segment.resume !== undefined ? { resume: segment.resume } : {}),\n ...(pages.length ? { pages } : {}), ...(watched.length ? { watched } : {}),\n };\n this.options.onSnapshot?.(snapshot);\n // A newer cumulative revision makes an unsent earlier revision unnecessary.\n this.pending = this.pending.filter((entry) => entry.snapshot.segment_id !== segment.id);\n this.pending.push({ snapshot, receipt: segment.receipt });\n if (this.pending.length > MAX_PENDING) this.pending.shift();\n }\n\n private send(keepalive: boolean): Promise<void> {\n this.keepaliveRequested ||= keepalive;\n this.tail = this.tail.then(async () => {\n for (const entry of [...this.pending]) {\n if (this.stopped) return;\n const context = await entry.receipt;\n if (this.stopped) return;\n if (!context) { this.pending = this.pending.filter((item) => item !== entry); continue; }\n try {\n await this.options.submit({ ...entry.snapshot, activity_token: context.token }, { keepalive: keepalive || this.keepaliveRequested });\n this.pending = this.pending.filter((item) => item !== entry);\n } catch {\n // A later checkpoint, online event or exit can retry this same revision.\n }\n }\n if (!this.pending.length) this.keepaliveRequested = false;\n });\n return this.tail;\n }\n}\n"],"mappings":"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"}