@atproto/bsky 0.0.282 → 0.1.0

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 (138) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/api/app/bsky/feed/getFeed.d.ts +8 -0
  3. package/dist/api/app/bsky/feed/getFeed.d.ts.map +1 -1
  4. package/dist/api/app/bsky/feed/getFeed.js +17 -0
  5. package/dist/api/app/bsky/feed/getFeed.js.map +1 -1
  6. package/dist/api/app/bsky/feed/getListFeed.d.ts +2 -1
  7. package/dist/api/app/bsky/feed/getListFeed.d.ts.map +1 -1
  8. package/dist/api/app/bsky/feed/getListFeed.js +17 -4
  9. package/dist/api/app/bsky/feed/getListFeed.js.map +1 -1
  10. package/dist/api/app/bsky/feed/getTimeline.d.ts +2 -1
  11. package/dist/api/app/bsky/feed/getTimeline.d.ts.map +1 -1
  12. package/dist/api/app/bsky/feed/getTimeline.js +17 -4
  13. package/dist/api/app/bsky/feed/getTimeline.js.map +1 -1
  14. package/dist/api/app/bsky/notification/constants.d.ts +37 -0
  15. package/dist/api/app/bsky/notification/constants.d.ts.map +1 -0
  16. package/dist/api/app/bsky/notification/constants.js +24 -0
  17. package/dist/api/app/bsky/notification/constants.js.map +1 -0
  18. package/dist/api/app/bsky/notification/getGroupedNotifications.d.ts +5 -0
  19. package/dist/api/app/bsky/notification/getGroupedNotifications.d.ts.map +1 -0
  20. package/dist/api/app/bsky/notification/getGroupedNotifications.js +349 -0
  21. package/dist/api/app/bsky/notification/getGroupedNotifications.js.map +1 -0
  22. package/dist/api/app/bsky/notification/grouping/algoGravity.d.ts +4 -0
  23. package/dist/api/app/bsky/notification/grouping/algoGravity.d.ts.map +1 -0
  24. package/dist/api/app/bsky/notification/grouping/algoGravity.js +141 -0
  25. package/dist/api/app/bsky/notification/grouping/algoGravity.js.map +1 -0
  26. package/dist/api/app/bsky/notification/grouping/algoLookback.d.ts +4 -0
  27. package/dist/api/app/bsky/notification/grouping/algoLookback.d.ts.map +1 -0
  28. package/dist/api/app/bsky/notification/grouping/algoLookback.js +61 -0
  29. package/dist/api/app/bsky/notification/grouping/algoLookback.js.map +1 -0
  30. package/dist/api/app/bsky/notification/grouping/grouping.d.ts +41 -0
  31. package/dist/api/app/bsky/notification/grouping/grouping.d.ts.map +1 -0
  32. package/dist/api/app/bsky/notification/grouping/grouping.js +54 -0
  33. package/dist/api/app/bsky/notification/grouping/grouping.js.map +1 -0
  34. package/dist/api/app/bsky/notification/grouping/shared.d.ts +8 -0
  35. package/dist/api/app/bsky/notification/grouping/shared.d.ts.map +1 -0
  36. package/dist/api/app/bsky/notification/grouping/shared.js +20 -0
  37. package/dist/api/app/bsky/notification/grouping/shared.js.map +1 -0
  38. package/dist/api/app/bsky/notification/grouping/spotlight.d.ts +3 -0
  39. package/dist/api/app/bsky/notification/grouping/spotlight.d.ts.map +1 -0
  40. package/dist/api/app/bsky/notification/grouping/spotlight.js +114 -0
  41. package/dist/api/app/bsky/notification/grouping/spotlight.js.map +1 -0
  42. package/dist/api/app/bsky/notification/listNotifications.d.ts +0 -7
  43. package/dist/api/app/bsky/notification/listNotifications.d.ts.map +1 -1
  44. package/dist/api/app/bsky/notification/listNotifications.js +7 -59
  45. package/dist/api/app/bsky/notification/listNotifications.js.map +1 -1
  46. package/dist/api/app/bsky/notification/updateSeen.d.ts.map +1 -1
  47. package/dist/api/app/bsky/notification/updateSeen.js +4 -3
  48. package/dist/api/app/bsky/notification/updateSeen.js.map +1 -1
  49. package/dist/api/app/bsky/notification/util.d.ts +8 -0
  50. package/dist/api/app/bsky/notification/util.d.ts.map +1 -1
  51. package/dist/api/app/bsky/notification/util.js +55 -0
  52. package/dist/api/app/bsky/notification/util.js.map +1 -1
  53. package/dist/api/index.d.ts.map +1 -1
  54. package/dist/api/index.js +2 -0
  55. package/dist/api/index.js.map +1 -1
  56. package/dist/api/util.d.ts +5 -0
  57. package/dist/api/util.d.ts.map +1 -1
  58. package/dist/api/util.js +4 -1
  59. package/dist/api/util.js.map +1 -1
  60. package/dist/config.d.ts +7 -0
  61. package/dist/config.d.ts.map +1 -1
  62. package/dist/config.js +19 -0
  63. package/dist/config.js.map +1 -1
  64. package/dist/data-plane/server/db/pagination.d.ts +7 -0
  65. package/dist/data-plane/server/db/pagination.d.ts.map +1 -1
  66. package/dist/data-plane/server/db/pagination.js +18 -2
  67. package/dist/data-plane/server/db/pagination.js.map +1 -1
  68. package/dist/data-plane/server/db/tables/notification.d.ts +2 -1
  69. package/dist/data-plane/server/db/tables/notification.d.ts.map +1 -1
  70. package/dist/data-plane/server/db/tables/notification.js.map +1 -1
  71. package/dist/data-plane/server/indexing/index.d.ts +2 -0
  72. package/dist/data-plane/server/indexing/index.d.ts.map +1 -1
  73. package/dist/data-plane/server/indexing/plugins/follow.d.ts +6 -9
  74. package/dist/data-plane/server/indexing/plugins/follow.d.ts.map +1 -1
  75. package/dist/data-plane/server/indexing/plugins/follow.js +13 -2
  76. package/dist/data-plane/server/indexing/plugins/follow.js.map +1 -1
  77. package/dist/data-plane/server/indexing/plugins/like.d.ts.map +1 -1
  78. package/dist/data-plane/server/indexing/plugins/like.js +3 -2
  79. package/dist/data-plane/server/indexing/plugins/like.js.map +1 -1
  80. package/dist/data-plane/server/indexing/plugins/post.d.ts.map +1 -1
  81. package/dist/data-plane/server/indexing/plugins/post.js +5 -4
  82. package/dist/data-plane/server/indexing/plugins/post.js.map +1 -1
  83. package/dist/data-plane/server/indexing/plugins/profile.d.ts.map +1 -1
  84. package/dist/data-plane/server/indexing/plugins/profile.js +2 -1
  85. package/dist/data-plane/server/indexing/plugins/profile.js.map +1 -1
  86. package/dist/data-plane/server/indexing/plugins/repost.d.ts.map +1 -1
  87. package/dist/data-plane/server/indexing/plugins/repost.js +3 -2
  88. package/dist/data-plane/server/indexing/plugins/repost.js.map +1 -1
  89. package/dist/data-plane/server/indexing/plugins/verification.d.ts.map +1 -1
  90. package/dist/data-plane/server/indexing/plugins/verification.js +3 -2
  91. package/dist/data-plane/server/indexing/plugins/verification.js.map +1 -1
  92. package/dist/data-plane/server/routes/feeds.d.ts +1 -1
  93. package/dist/data-plane/server/routes/feeds.d.ts.map +1 -1
  94. package/dist/data-plane/server/routes/feeds.js +35 -7
  95. package/dist/data-plane/server/routes/feeds.js.map +1 -1
  96. package/dist/data-plane/server/routes/notifs.d.ts.map +1 -1
  97. package/dist/data-plane/server/routes/notifs.js +67 -2
  98. package/dist/data-plane/server/routes/notifs.js.map +1 -1
  99. package/dist/hydration/hydrator.d.ts +2 -0
  100. package/dist/hydration/hydrator.d.ts.map +1 -1
  101. package/dist/hydration/hydrator.js +69 -1
  102. package/dist/hydration/hydrator.js.map +1 -1
  103. package/dist/lexicons/app/bsky/authManageNotifications.defs.d.ts.map +1 -1
  104. package/dist/lexicons/app/bsky/authManageNotifications.defs.js +1 -0
  105. package/dist/lexicons/app/bsky/authManageNotifications.defs.js.map +1 -1
  106. package/dist/lexicons/app/bsky/feed/getListFeed.defs.d.ts +4 -0
  107. package/dist/lexicons/app/bsky/feed/getListFeed.defs.d.ts.map +1 -1
  108. package/dist/lexicons/app/bsky/feed/getListFeed.defs.js +2 -0
  109. package/dist/lexicons/app/bsky/feed/getListFeed.defs.js.map +1 -1
  110. package/dist/lexicons/app/bsky/feed/getTimeline.defs.d.ts +4 -0
  111. package/dist/lexicons/app/bsky/feed/getTimeline.defs.d.ts.map +1 -1
  112. package/dist/lexicons/app/bsky/feed/getTimeline.defs.js +2 -0
  113. package/dist/lexicons/app/bsky/feed/getTimeline.defs.js.map +1 -1
  114. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.d.ts +3 -0
  115. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.d.ts.map +1 -0
  116. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.d.ts +299 -0
  117. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.d.ts.map +1 -0
  118. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.js +231 -0
  119. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.defs.js.map +1 -0
  120. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.js +6 -0
  121. package/dist/lexicons/app/bsky/notification/getGroupedNotifications.js.map +1 -0
  122. package/dist/lexicons/app/bsky/notification.d.ts +1 -0
  123. package/dist/lexicons/app/bsky/notification.d.ts.map +1 -1
  124. package/dist/lexicons/app/bsky/notification.js +1 -0
  125. package/dist/lexicons/app/bsky/notification.js.map +1 -1
  126. package/dist/proto/bsky_connect.d.ts +10 -1
  127. package/dist/proto/bsky_connect.d.ts.map +1 -1
  128. package/dist/proto/bsky_connect.js +10 -1
  129. package/dist/proto/bsky_connect.js.map +1 -1
  130. package/dist/proto/bsky_pb.d.ts +120 -25
  131. package/dist/proto/bsky_pb.d.ts.map +1 -1
  132. package/dist/proto/bsky_pb.js +166 -27
  133. package/dist/proto/bsky_pb.js.map +1 -1
  134. package/dist/views/index.d.ts +3 -0
  135. package/dist/views/index.d.ts.map +1 -1
  136. package/dist/views/index.js +133 -0
  137. package/dist/views/index.js.map +1 -1
  138. package/package.json +14 -14
@@ -0,0 +1,349 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mapDefined } from '@atproto/common';
3
+ import { asDatetimeString, } from '@atproto/lex';
4
+ import { AtUri } from '@atproto/syntax';
5
+ import { app } from '../../../../lexicons/index.js';
6
+ import { createPipeline, } from '../../../../pipeline.js';
7
+ import { NotificationFeed } from '../../../../proto/bsky_pb.js';
8
+ import { uriToDid } from '../../../../util/uris.js';
9
+ import { resHeaders } from '../../../util.js';
10
+ import { NOTIFICATION_REASON } from './constants.js';
11
+ import { buildGroups, parseRawNotification, } from './grouping/grouping.js';
12
+ import { compareNotificationGroupsNewestFirst, isNotificationRead, } from './grouping/shared.js';
13
+ import { buildSpotlight } from './grouping/spotlight.js';
14
+ import { delayCursor, shouldFilterForNeedsReview, shouldFilterHiddenThreadTag, shouldFilterReplyByThreadgate, } from './util.js';
15
+ const MIN_RAW_LIMIT = 300;
16
+ const MAX_RAW_ITEMS = 3000;
17
+ const MAX_READS = 3;
18
+ const MAX_GROUP_ITEMS_WITH_RELATED_VIEWS = 10;
19
+ export const getNextRawLimit = (itemsFetched, groupsBuilt, groupsNeeded) => {
20
+ // How many raw items we still can fetch until hit the max.
21
+ const remainingItems = MAX_RAW_ITEMS - itemsFetched;
22
+ if (remainingItems <= 0)
23
+ return 0;
24
+ // Defensive fallback to avoid a division by 0 later.
25
+ // This is for the scenario where fetched items but couldn't build any groups with them
26
+ // (let's say all notifications are missing data and are filtered out, shouldn't happen in practice).
27
+ if (groupsBuilt === 0)
28
+ return Math.min(MIN_RAW_LIMIT, remainingItems);
29
+ // How many groups still to be built - note that we might still fit items into existing groups.
30
+ const groupsMissing = Math.max(0, groupsNeeded - groupsBuilt);
31
+ const averagePerGroup = itemsFetched / groupsBuilt;
32
+ // Estimate additional raw notifications from the average group size.
33
+ const estimatedRawItems = Math.ceil(averagePerGroup * groupsMissing);
34
+ // Keep the estimate above the minimum batch and within the total raw-item budget.
35
+ return Math.min(remainingItems, Math.max(MIN_RAW_LIMIT, estimatedRawItems));
36
+ };
37
+ export default function (server, ctx) {
38
+ const getGroupedNotifications = createPipeline(skeleton, hydration, rules, presentation);
39
+ server.add(app.bsky.notification.getGroupedNotifications, {
40
+ auth: ctx.authVerifier.standard,
41
+ handler: async ({ params, auth, req }) => {
42
+ const viewer = auth.credentials.iss;
43
+ const features = ctx.featureGatesClient.scope(ctx.featureGatesClient.parseUserContextFromHandler({ viewer, req }));
44
+ const labelers = ctx.reqLabelers(req);
45
+ const hydrateCtx = await ctx.hydrator.createContext({
46
+ labelers,
47
+ viewer,
48
+ features,
49
+ });
50
+ const lastSeenRes = await ctx.hydrator.dataplane.getNotificationSeen({
51
+ actorDid: viewer,
52
+ });
53
+ const seenAt = lastSeenRes.timestamp
54
+ ? asDatetimeString(lastSeenRes.timestamp.toJson())
55
+ : undefined;
56
+ const body = await getGroupedNotifications({ ...params, hydrateCtx, seenAt }, ctx);
57
+ return {
58
+ encoding: 'application/json',
59
+ headers: resHeaders({ labelers: hydrateCtx.labelers }),
60
+ body,
61
+ };
62
+ },
63
+ });
64
+ }
65
+ const skeleton = async (input) => {
66
+ const { ctx, params } = input;
67
+ const now = Date.now();
68
+ const feed = normalizeFeed(params.feed);
69
+ const seenAt = params.seenAt ? Date.parse(params.seenAt) : undefined;
70
+ const delayedCursor = delayCursor(undefined, ctx.cfg.notificationsDelayMs);
71
+ let cursor = params.cursor;
72
+ if (!cursor || Date.parse(cursor) > Date.parse(delayedCursor)) {
73
+ cursor = delayedCursor;
74
+ }
75
+ const items = [];
76
+ let groups = [];
77
+ let nextCursor;
78
+ let itemsFetched = 0;
79
+ let rawLimit = MIN_RAW_LIMIT;
80
+ for (let read = 0; read < MAX_READS; read++) {
81
+ const response = await ctx.hydrator.dataplane.getNotificationsV2({
82
+ actorDid: params.hydrateCtx.viewer,
83
+ feed,
84
+ cursor,
85
+ limit: rawLimit,
86
+ });
87
+ itemsFetched += response.notifications.length;
88
+ for (const notification of response.notifications) {
89
+ const notif = parseRawNotification(notification);
90
+ if (!notif)
91
+ continue;
92
+ const uri = notif.uri;
93
+ const actorDid = uriToDid(uri);
94
+ const indexedAt = notif.indexedAt;
95
+ // Hashing keeps IDs within the lexicon's length limit for long record URIs.
96
+ const id = createHash('sha256')
97
+ .update(`${indexedAt}\0${uri}`)
98
+ .digest('base64url');
99
+ let groupingKey = notif.reasonSubject || uri;
100
+ if (notif.reason === NOTIFICATION_REASON.FOLLOW) {
101
+ groupingKey = NOTIFICATION_REASON.FOLLOW;
102
+ }
103
+ if (notif.reason === NOTIFICATION_REASON.SUBSCRIBED_POST) {
104
+ groupingKey = actorDid;
105
+ }
106
+ items.push({
107
+ raw: notif,
108
+ id,
109
+ groupingKey,
110
+ actorDid,
111
+ });
112
+ }
113
+ const groupingResult = buildGroups(items, params.limit, now, seenAt, ctx.cfg.notificationsV2Algorithm, feed);
114
+ groups = groupingResult.groups;
115
+ // After fetching a page from the dataplane and attempting to group, 2 situations can happen:
116
+ // 1. The first unconsumed notification would create an additional group beyond params.limit.
117
+ // result.cursor points before that notification; use it so the next request processes it.
118
+ // 2. Every fetched notification was consumed into the groups.
119
+ // result.cursor is undefined, so use response.cursor to fetch the next dataplane page.
120
+ nextCursor = groupingResult.cursor ?? (response.cursor || undefined);
121
+ const groupingFoundUnconsumedNotification = groupingResult.cursor !== undefined;
122
+ const dataplaneHasMoreNotifications = !!response.cursor;
123
+ const shouldContinueBuildingPage = !groupingFoundUnconsumedNotification && dataplaneHasMoreNotifications;
124
+ if (!shouldContinueBuildingPage)
125
+ break;
126
+ cursor = response.cursor;
127
+ rawLimit = getNextRawLimit(itemsFetched, groups.length, params.limit);
128
+ if (rawLimit === 0)
129
+ break;
130
+ }
131
+ return buildSpotlight(items, groups, nextCursor, params.limit, seenAt);
132
+ };
133
+ const hydration = async (input) => {
134
+ const { ctx, params, skeleton } = input;
135
+ const notifs = skeleton.groups.flatMap((group) => group.items.map((item) => item.raw));
136
+ return ctx.hydrator.hydrateGroupedNotifications(notifs, params.hydrateCtx);
137
+ };
138
+ const rules = (input) => {
139
+ const { ctx, params, skeleton, hydration } = input;
140
+ const seenAt = params.seenAt ? Date.parse(params.seenAt) : undefined;
141
+ const groups = mapDefined(skeleton.groups, (group) => {
142
+ const items = group.items.filter((item) => {
143
+ const notif = item.raw;
144
+ const uri = notif.uri;
145
+ const did = item.actorDid;
146
+ if (ctx.views.viewerBlockExists(did, hydration) ||
147
+ ctx.views.viewerMuteExists(did, hydration)) {
148
+ return false;
149
+ }
150
+ if (shouldFilterReplyByThreadgate(notif.reason, uri, params.hydrateCtx.viewer, hydration, ctx.views)) {
151
+ return false;
152
+ }
153
+ if (shouldFilterHiddenThreadTag(notif.reason, uri, did, hydration, ctx.cfg.threadTagsHide)) {
154
+ return false;
155
+ }
156
+ if (shouldFilterForNeedsReview(notif.reason, did, uri, hydration, ctx.views)) {
157
+ return false;
158
+ }
159
+ // After hydration, notificationAvailable checks whether each notification’s required records exist.
160
+ // If its actor, post, repost, follow, or starter pack is missing or blocked, rules removes that notification from the returned group.
161
+ return notificationAvailable(notif, hydration, ctx.views);
162
+ });
163
+ const newest = items[0];
164
+ const oldest = items.at(-1);
165
+ // If it has no more elements, drop this group.
166
+ if (!newest || !oldest)
167
+ return;
168
+ // Filtering may remove notifications from this group; build a new group with only the remaining items and their updated metadata.
169
+ return {
170
+ ...group,
171
+ id: newest.id,
172
+ items,
173
+ actorDids: items.map((item) => item.actorDid),
174
+ itemCount: items.length,
175
+ indexedAt: newest.raw.indexedAt,
176
+ firstIndexedAt: oldest.raw.indexedAt,
177
+ isRead: isNotificationRead(newest.raw.indexedAt, seenAt),
178
+ };
179
+ });
180
+ groups.sort(compareNotificationGroupsNewestFirst);
181
+ return { ...skeleton, groups };
182
+ };
183
+ // Checks that the notification’s actor and required records exist and referenced posts aren’t viewer-blocked.
184
+ const notificationAvailable = (notif, state, views) => {
185
+ const uri = notif.uri;
186
+ const actor = state.actors?.get(uriToDid(uri));
187
+ if (!actor)
188
+ return false;
189
+ const postAvailable = (postUri) => !!state.posts?.get(postUri) &&
190
+ !!state.actors?.get(uriToDid(postUri)) &&
191
+ !views.viewerBlockExists(uriToDid(postUri), state);
192
+ const starterPackAvailable = (starterPackUri) => !!state.starterPacks?.get(starterPackUri) &&
193
+ !!state.actors?.get(uriToDid(starterPackUri));
194
+ switch (notif.reason) {
195
+ case NOTIFICATION_REASON.LIKE: {
196
+ const subjectUri = notif.reasonSubject;
197
+ if (!state.likes?.get(uri))
198
+ return false;
199
+ if (new AtUri(subjectUri).collection === app.bsky.feed.generator.$type) {
200
+ return (!!state.feedgens?.get(subjectUri) &&
201
+ !!state.actors?.get(uriToDid(subjectUri)));
202
+ }
203
+ return postAvailable(subjectUri);
204
+ }
205
+ case NOTIFICATION_REASON.REPOST:
206
+ return !!state.reposts?.get(uri) && postAvailable(notif.reasonSubject);
207
+ case NOTIFICATION_REASON.LIKE_VIA_REPOST:
208
+ case NOTIFICATION_REASON.REPOST_VIA_REPOST: {
209
+ const recordInfo = notif.reason === NOTIFICATION_REASON.LIKE_VIA_REPOST
210
+ ? state.likes?.get(uri)
211
+ : state.reposts?.get(uri);
212
+ const postUri = state.reposts?.get(notif.reasonSubject)?.record.subject
213
+ .uri;
214
+ return !!recordInfo && !!postUri && postAvailable(postUri);
215
+ }
216
+ case NOTIFICATION_REASON.FOLLOW:
217
+ case NOTIFICATION_REASON.FOLLOW_BACK:
218
+ return !!state.follows?.get(uri);
219
+ case NOTIFICATION_REASON.REPLY:
220
+ return (postAvailable(uri) && !!state.posts?.get(uri)?.record.reply?.parent.uri);
221
+ case NOTIFICATION_REASON.QUOTE:
222
+ case NOTIFICATION_REASON.MENTION:
223
+ case NOTIFICATION_REASON.SUBSCRIBED_POST:
224
+ return postAvailable(uri);
225
+ case NOTIFICATION_REASON.VERIFIED:
226
+ case NOTIFICATION_REASON.UNVERIFIED:
227
+ // Both notifications survive deletion of the verification record.
228
+ return true;
229
+ case NOTIFICATION_REASON.STARTERPACK_JOINED:
230
+ return (!!actor.profile &&
231
+ !!actor.profileCid &&
232
+ starterPackAvailable(notif.reasonSubject));
233
+ case NOTIFICATION_REASON.CONTACT_MATCH:
234
+ return !!actor.profile && !!actor.profileCid;
235
+ default:
236
+ return false;
237
+ }
238
+ };
239
+ const presentation = (input) => {
240
+ const { ctx, params, skeleton, hydration } = input;
241
+ const defs = app.bsky.notification.getGroupedNotifications;
242
+ const groups = mapDefined(skeleton.groups, (group) => ctx.views.notificationGroup(group, hydration));
243
+ const profileDids = new Set();
244
+ const recordUris = new Set();
245
+ for (const { kind } of groups) {
246
+ if (defs.likeGroup.$isTypeOf(kind) ||
247
+ defs.repostGroup.$isTypeOf(kind) ||
248
+ defs.likeViaRepostGroup.$isTypeOf(kind) ||
249
+ defs.repostViaRepostGroup.$isTypeOf(kind)) {
250
+ recordUris.add(kind.post);
251
+ for (const item of truncateRelatedViewsItems(kind.items)) {
252
+ profileDids.add(item.actor);
253
+ }
254
+ }
255
+ else if (defs.multiPostLikeGroup.$isTypeOf(kind)) {
256
+ profileDids.add(kind.actor);
257
+ for (const item of truncateRelatedViewsItems(kind.items)) {
258
+ recordUris.add(item.post);
259
+ }
260
+ }
261
+ else if (defs.generatorLikeGroup.$isTypeOf(kind)) {
262
+ recordUris.add(kind.generator);
263
+ for (const item of truncateRelatedViewsItems(kind.items)) {
264
+ profileDids.add(item.actor);
265
+ }
266
+ }
267
+ else if (defs.followGroup.$isTypeOf(kind)) {
268
+ for (const item of truncateRelatedViewsItems(kind.items)) {
269
+ profileDids.add(item.actor);
270
+ if (item.starterPack)
271
+ recordUris.add(item.starterPack);
272
+ }
273
+ }
274
+ else if (defs.subscribedPostGroup.$isTypeOf(kind)) {
275
+ for (const item of truncateRelatedViewsItems(kind.items)) {
276
+ profileDids.add(item.actor);
277
+ recordUris.add(item.post);
278
+ }
279
+ }
280
+ else if (defs.replyNotification.$isTypeOf(kind) ||
281
+ defs.quoteNotification.$isTypeOf(kind) ||
282
+ defs.mentionNotification.$isTypeOf(kind)) {
283
+ recordUris.add(kind.post);
284
+ if (kind.parent)
285
+ recordUris.add(kind.parent);
286
+ }
287
+ else if (defs.followBackNotification.$isTypeOf(kind) ||
288
+ defs.starterPackJoinedNotification.$isTypeOf(kind)) {
289
+ profileDids.add(kind.actor);
290
+ if (kind.starterPack)
291
+ recordUris.add(kind.starterPack);
292
+ }
293
+ else if (defs.verifiedNotification.$isTypeOf(kind) ||
294
+ defs.unverifiedNotification.$isTypeOf(kind) ||
295
+ defs.contactMatchNotification.$isTypeOf(kind)) {
296
+ profileDids.add(kind.actor);
297
+ }
298
+ }
299
+ return {
300
+ groups,
301
+ relatedViews: [
302
+ ...mapDefined([...profileDids], (did) => {
303
+ const view = ctx.views.profileDetailed(did, hydration);
304
+ if (!view)
305
+ return;
306
+ return app.bsky.actor.defs.profileViewDetailed.$build(view);
307
+ }),
308
+ ...mapDefined([...recordUris], (uri) => {
309
+ const collection = new AtUri(uri).collection;
310
+ if (collection === app.bsky.feed.post.$type) {
311
+ const view = ctx.views.maybePost(uri, hydration);
312
+ if (!view)
313
+ return;
314
+ return view;
315
+ }
316
+ else if (collection === app.bsky.graph.starterpack.$type) {
317
+ const view = ctx.views.starterPack(uri, hydration);
318
+ if (!view)
319
+ return;
320
+ return app.bsky.graph.defs.starterPackView.$build(view);
321
+ }
322
+ else if (collection === app.bsky.feed.generator.$type) {
323
+ const view = ctx.views.feedGenerator(uri, hydration);
324
+ if (!view)
325
+ return;
326
+ return app.bsky.feed.defs.generatorView.$build(view);
327
+ }
328
+ }),
329
+ ],
330
+ cursor: skeleton.cursor,
331
+ seenAt: params.seenAt,
332
+ };
333
+ };
334
+ const truncateRelatedViewsItems = (items) => items.slice(0, MAX_GROUP_ITEMS_WITH_RELATED_VIEWS);
335
+ const normalizeFeed = (feed) => {
336
+ switch (feed) {
337
+ case 'people-i-follow':
338
+ return NotificationFeed.PEOPLE_I_FOLLOW;
339
+ case 'conversations':
340
+ return NotificationFeed.CONVERSATIONS;
341
+ case 'followers':
342
+ return NotificationFeed.FOLLOWERS;
343
+ case 'activity':
344
+ return NotificationFeed.ACTIVITY;
345
+ default:
346
+ return NotificationFeed.ALL;
347
+ }
348
+ };
349
+ //# sourceMappingURL=getGroupedNotifications.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getGroupedNotifications.js","sourceRoot":"","sources":["../../../../../src/api/app/bsky/notification/getGroupedNotifications.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto'\nimport { mapDefined } from '@atproto/common'\nimport {\n type AtUriString,\n type DatetimeString,\n type DidString,\n asDatetimeString,\n} from '@atproto/lex'\nimport { AtUri } from '@atproto/syntax'\nimport type { Server } from '@atproto/xrpc-server'\nimport type { ServerConfig } from '../../../../config.js'\nimport type { AppContext } from '../../../../context.js'\nimport type {\n HydrateCtxWithViewer,\n HydrationState,\n Hydrator,\n} from '../../../../hydration/hydrator.js'\nimport { app } from '../../../../lexicons/index.js'\nimport {\n type HydrationFnInput,\n type PresentationFnInput,\n type RulesFnInput,\n type SkeletonFnInput,\n createPipeline,\n} from '../../../../pipeline.js'\nimport { NotificationFeed } from '../../../../proto/bsky_pb.js'\nimport { uriToDid } from '../../../../util/uris.js'\nimport type { Views } from '../../../../views/index.js'\nimport { resHeaders } from '../../../util.js'\nimport { NOTIFICATION_REASON } from './constants.js'\nimport {\n type NotificationGroup,\n type NotificationItem,\n type RawNotification,\n buildGroups,\n parseRawNotification,\n} from './grouping/grouping.js'\nimport {\n compareNotificationGroupsNewestFirst,\n isNotificationRead,\n} from './grouping/shared.js'\nimport { buildSpotlight } from './grouping/spotlight.js'\nimport {\n delayCursor,\n shouldFilterForNeedsReview,\n shouldFilterHiddenThreadTag,\n shouldFilterReplyByThreadgate,\n} from './util.js'\n\nconst MIN_RAW_LIMIT = 300\nconst MAX_RAW_ITEMS = 3000\nconst MAX_READS = 3\nconst MAX_GROUP_ITEMS_WITH_RELATED_VIEWS = 10\n\nexport const getNextRawLimit = (\n itemsFetched: number,\n groupsBuilt: number,\n groupsNeeded: number,\n): number => {\n // How many raw items we still can fetch until hit the max.\n const remainingItems = MAX_RAW_ITEMS - itemsFetched\n if (remainingItems <= 0) return 0\n\n // Defensive fallback to avoid a division by 0 later.\n // This is for the scenario where fetched items but couldn't build any groups with them\n // (let's say all notifications are missing data and are filtered out, shouldn't happen in practice).\n if (groupsBuilt === 0) return Math.min(MIN_RAW_LIMIT, remainingItems)\n\n // How many groups still to be built - note that we might still fit items into existing groups.\n const groupsMissing = Math.max(0, groupsNeeded - groupsBuilt)\n\n const averagePerGroup = itemsFetched / groupsBuilt\n\n // Estimate additional raw notifications from the average group size.\n const estimatedRawItems = Math.ceil(averagePerGroup * groupsMissing)\n\n // Keep the estimate above the minimum batch and within the total raw-item budget.\n return Math.min(remainingItems, Math.max(MIN_RAW_LIMIT, estimatedRawItems))\n}\n\nexport default function (server: Server, ctx: AppContext) {\n const getGroupedNotifications = createPipeline(\n skeleton,\n hydration,\n rules,\n presentation,\n )\n server.add(app.bsky.notification.getGroupedNotifications, {\n auth: ctx.authVerifier.standard,\n handler: async ({ params, auth, req }) => {\n const viewer = auth.credentials.iss\n const features = ctx.featureGatesClient.scope(\n ctx.featureGatesClient.parseUserContextFromHandler({ viewer, req }),\n )\n const labelers = ctx.reqLabelers(req)\n const hydrateCtx = await ctx.hydrator.createContext({\n labelers,\n viewer,\n features,\n })\n const lastSeenRes = await ctx.hydrator.dataplane.getNotificationSeen({\n actorDid: viewer,\n })\n const seenAt = lastSeenRes.timestamp\n ? asDatetimeString(lastSeenRes.timestamp.toJson() as string)\n : undefined\n const body = await getGroupedNotifications(\n { ...params, hydrateCtx, seenAt },\n ctx,\n )\n return {\n encoding: 'application/json',\n headers: resHeaders({ labelers: hydrateCtx.labelers }),\n body,\n }\n },\n })\n}\n\nconst skeleton = async (\n input: SkeletonFnInput<Context, Params>,\n): Promise<SkeletonState> => {\n const { ctx, params } = input\n const now = Date.now()\n const feed = normalizeFeed(params.feed)\n const seenAt = params.seenAt ? Date.parse(params.seenAt) : undefined\n const delayedCursor = delayCursor(undefined, ctx.cfg.notificationsDelayMs)\n let cursor = params.cursor\n if (!cursor || Date.parse(cursor) > Date.parse(delayedCursor)) {\n cursor = delayedCursor\n }\n\n const items: NotificationItem[] = []\n let groups: NotificationGroup[] = []\n let nextCursor: string | undefined\n let itemsFetched = 0\n let rawLimit = MIN_RAW_LIMIT\n\n for (let read = 0; read < MAX_READS; read++) {\n const response = await ctx.hydrator.dataplane.getNotificationsV2({\n actorDid: params.hydrateCtx.viewer,\n feed,\n cursor,\n limit: rawLimit,\n })\n itemsFetched += response.notifications.length\n\n for (const notification of response.notifications) {\n const notif = parseRawNotification(notification)\n if (!notif) continue\n const uri = notif.uri\n const actorDid = uriToDid(uri)\n const indexedAt = notif.indexedAt\n // Hashing keeps IDs within the lexicon's length limit for long record URIs.\n const id = createHash('sha256')\n .update(`${indexedAt}\\0${uri}`)\n .digest('base64url')\n\n let groupingKey: string = notif.reasonSubject || uri\n if (notif.reason === NOTIFICATION_REASON.FOLLOW) {\n groupingKey = NOTIFICATION_REASON.FOLLOW\n }\n if (notif.reason === NOTIFICATION_REASON.SUBSCRIBED_POST) {\n groupingKey = actorDid\n }\n\n items.push({\n raw: notif,\n id,\n groupingKey,\n actorDid,\n })\n }\n\n const groupingResult = buildGroups(\n items,\n params.limit,\n now,\n seenAt,\n ctx.cfg.notificationsV2Algorithm,\n feed,\n )\n groups = groupingResult.groups\n\n // After fetching a page from the dataplane and attempting to group, 2 situations can happen:\n // 1. The first unconsumed notification would create an additional group beyond params.limit.\n // result.cursor points before that notification; use it so the next request processes it.\n // 2. Every fetched notification was consumed into the groups.\n // result.cursor is undefined, so use response.cursor to fetch the next dataplane page.\n nextCursor = groupingResult.cursor ?? (response.cursor || undefined)\n\n const groupingFoundUnconsumedNotification =\n groupingResult.cursor !== undefined\n const dataplaneHasMoreNotifications = !!response.cursor\n const shouldContinueBuildingPage =\n !groupingFoundUnconsumedNotification && dataplaneHasMoreNotifications\n if (!shouldContinueBuildingPage) break\n\n cursor = response.cursor\n rawLimit = getNextRawLimit(itemsFetched, groups.length, params.limit)\n if (rawLimit === 0) break\n }\n\n return buildSpotlight(items, groups, nextCursor, params.limit, seenAt)\n}\n\nconst hydration = async (\n input: HydrationFnInput<Context, Params, SkeletonState>,\n) => {\n const { ctx, params, skeleton } = input\n const notifs = skeleton.groups.flatMap((group) =>\n group.items.map((item) => item.raw),\n )\n return ctx.hydrator.hydrateGroupedNotifications(notifs, params.hydrateCtx)\n}\n\nconst rules = (\n input: RulesFnInput<Context, Params, SkeletonState>,\n): SkeletonState => {\n const { ctx, params, skeleton, hydration } = input\n const seenAt = params.seenAt ? Date.parse(params.seenAt) : undefined\n const groups = mapDefined(\n skeleton.groups,\n (group): NotificationGroup | undefined => {\n const items = group.items.filter((item) => {\n const notif = item.raw\n const uri = notif.uri\n const did = item.actorDid\n if (\n ctx.views.viewerBlockExists(did, hydration) ||\n ctx.views.viewerMuteExists(did, hydration)\n ) {\n return false\n }\n if (\n shouldFilterReplyByThreadgate(\n notif.reason,\n uri,\n params.hydrateCtx.viewer,\n hydration,\n ctx.views,\n )\n ) {\n return false\n }\n if (\n shouldFilterHiddenThreadTag(\n notif.reason,\n uri,\n did,\n hydration,\n ctx.cfg.threadTagsHide,\n )\n ) {\n return false\n }\n if (\n shouldFilterForNeedsReview(\n notif.reason,\n did,\n uri,\n hydration,\n ctx.views,\n )\n ) {\n return false\n }\n\n // After hydration, notificationAvailable checks whether each notification’s required records exist.\n // If its actor, post, repost, follow, or starter pack is missing or blocked, rules removes that notification from the returned group.\n return notificationAvailable(notif, hydration, ctx.views)\n })\n\n const newest = items[0]\n const oldest = items.at(-1)\n\n // If it has no more elements, drop this group.\n if (!newest || !oldest) return\n\n // Filtering may remove notifications from this group; build a new group with only the remaining items and their updated metadata.\n return {\n ...group,\n id: newest.id,\n items,\n actorDids: items.map((item) => item.actorDid),\n itemCount: items.length,\n indexedAt: newest.raw.indexedAt,\n firstIndexedAt: oldest.raw.indexedAt,\n isRead: isNotificationRead(newest.raw.indexedAt, seenAt),\n }\n },\n )\n\n groups.sort(compareNotificationGroupsNewestFirst)\n\n return { ...skeleton, groups }\n}\n\n// Checks that the notification’s actor and required records exist and referenced posts aren’t viewer-blocked.\nconst notificationAvailable = (\n notif: RawNotification,\n state: HydrationState,\n views: Views,\n): boolean => {\n const uri = notif.uri\n const actor = state.actors?.get(uriToDid(uri))\n if (!actor) return false\n\n const postAvailable = (postUri: AtUriString) =>\n !!state.posts?.get(postUri) &&\n !!state.actors?.get(uriToDid(postUri)) &&\n !views.viewerBlockExists(uriToDid(postUri), state)\n\n const starterPackAvailable = (starterPackUri: AtUriString) =>\n !!state.starterPacks?.get(starterPackUri) &&\n !!state.actors?.get(uriToDid(starterPackUri))\n\n switch (notif.reason) {\n case NOTIFICATION_REASON.LIKE: {\n const subjectUri = notif.reasonSubject\n if (!state.likes?.get(uri)) return false\n if (new AtUri(subjectUri).collection === app.bsky.feed.generator.$type) {\n return (\n !!state.feedgens?.get(subjectUri) &&\n !!state.actors?.get(uriToDid(subjectUri))\n )\n }\n return postAvailable(subjectUri)\n }\n case NOTIFICATION_REASON.REPOST:\n return !!state.reposts?.get(uri) && postAvailable(notif.reasonSubject)\n case NOTIFICATION_REASON.LIKE_VIA_REPOST:\n case NOTIFICATION_REASON.REPOST_VIA_REPOST: {\n const recordInfo =\n notif.reason === NOTIFICATION_REASON.LIKE_VIA_REPOST\n ? state.likes?.get(uri)\n : state.reposts?.get(uri)\n const postUri = state.reposts?.get(notif.reasonSubject)?.record.subject\n .uri\n return !!recordInfo && !!postUri && postAvailable(postUri)\n }\n case NOTIFICATION_REASON.FOLLOW:\n case NOTIFICATION_REASON.FOLLOW_BACK:\n return !!state.follows?.get(uri)\n case NOTIFICATION_REASON.REPLY:\n return (\n postAvailable(uri) && !!state.posts?.get(uri)?.record.reply?.parent.uri\n )\n case NOTIFICATION_REASON.QUOTE:\n case NOTIFICATION_REASON.MENTION:\n case NOTIFICATION_REASON.SUBSCRIBED_POST:\n return postAvailable(uri)\n case NOTIFICATION_REASON.VERIFIED:\n case NOTIFICATION_REASON.UNVERIFIED:\n // Both notifications survive deletion of the verification record.\n return true\n case NOTIFICATION_REASON.STARTERPACK_JOINED:\n return (\n !!actor.profile &&\n !!actor.profileCid &&\n starterPackAvailable(notif.reasonSubject)\n )\n case NOTIFICATION_REASON.CONTACT_MATCH:\n return !!actor.profile && !!actor.profileCid\n default:\n return false\n }\n}\n\nconst presentation = (\n input: PresentationFnInput<Context, Params, SkeletonState>,\n): app.bsky.notification.getGroupedNotifications.$OutputBody => {\n const { ctx, params, skeleton, hydration } = input\n const defs = app.bsky.notification.getGroupedNotifications\n const groups = mapDefined(skeleton.groups, (group) =>\n ctx.views.notificationGroup(group, hydration),\n )\n const profileDids = new Set<DidString>()\n const recordUris = new Set<AtUriString>()\n for (const { kind } of groups) {\n if (\n defs.likeGroup.$isTypeOf(kind) ||\n defs.repostGroup.$isTypeOf(kind) ||\n defs.likeViaRepostGroup.$isTypeOf(kind) ||\n defs.repostViaRepostGroup.$isTypeOf(kind)\n ) {\n recordUris.add(kind.post)\n for (const item of truncateRelatedViewsItems(kind.items)) {\n profileDids.add(item.actor)\n }\n } else if (defs.multiPostLikeGroup.$isTypeOf(kind)) {\n profileDids.add(kind.actor)\n for (const item of truncateRelatedViewsItems(kind.items)) {\n recordUris.add(item.post)\n }\n } else if (defs.generatorLikeGroup.$isTypeOf(kind)) {\n recordUris.add(kind.generator)\n for (const item of truncateRelatedViewsItems(kind.items)) {\n profileDids.add(item.actor)\n }\n } else if (defs.followGroup.$isTypeOf(kind)) {\n for (const item of truncateRelatedViewsItems(kind.items)) {\n profileDids.add(item.actor)\n if (item.starterPack) recordUris.add(item.starterPack)\n }\n } else if (defs.subscribedPostGroup.$isTypeOf(kind)) {\n for (const item of truncateRelatedViewsItems(kind.items)) {\n profileDids.add(item.actor)\n recordUris.add(item.post)\n }\n } else if (\n defs.replyNotification.$isTypeOf(kind) ||\n defs.quoteNotification.$isTypeOf(kind) ||\n defs.mentionNotification.$isTypeOf(kind)\n ) {\n recordUris.add(kind.post)\n if (kind.parent) recordUris.add(kind.parent)\n } else if (\n defs.followBackNotification.$isTypeOf(kind) ||\n defs.starterPackJoinedNotification.$isTypeOf(kind)\n ) {\n profileDids.add(kind.actor)\n if (kind.starterPack) recordUris.add(kind.starterPack)\n } else if (\n defs.verifiedNotification.$isTypeOf(kind) ||\n defs.unverifiedNotification.$isTypeOf(kind) ||\n defs.contactMatchNotification.$isTypeOf(kind)\n ) {\n profileDids.add(kind.actor)\n }\n }\n return {\n groups,\n relatedViews: [\n ...mapDefined([...profileDids], (did) => {\n const view = ctx.views.profileDetailed(did, hydration)\n if (!view) return\n return app.bsky.actor.defs.profileViewDetailed.$build(view)\n }),\n ...mapDefined([...recordUris], (uri) => {\n const collection = new AtUri(uri).collection\n if (collection === app.bsky.feed.post.$type) {\n const view = ctx.views.maybePost(uri, hydration)\n if (!view) return\n return view\n } else if (collection === app.bsky.graph.starterpack.$type) {\n const view = ctx.views.starterPack(uri, hydration)\n if (!view) return\n return app.bsky.graph.defs.starterPackView.$build(view)\n } else if (collection === app.bsky.feed.generator.$type) {\n const view = ctx.views.feedGenerator(uri, hydration)\n if (!view) return\n return app.bsky.feed.defs.generatorView.$build(view)\n }\n }),\n ],\n cursor: skeleton.cursor,\n seenAt: params.seenAt,\n }\n}\n\nconst truncateRelatedViewsItems = <T extends unknown[]>(\n items: T,\n): T[number][] => items.slice(0, MAX_GROUP_ITEMS_WITH_RELATED_VIEWS)\n\nconst normalizeFeed = (feed: string): NotificationFeed => {\n switch (feed) {\n case 'people-i-follow':\n return NotificationFeed.PEOPLE_I_FOLLOW\n case 'conversations':\n return NotificationFeed.CONVERSATIONS\n case 'followers':\n return NotificationFeed.FOLLOWERS\n case 'activity':\n return NotificationFeed.ACTIVITY\n default:\n return NotificationFeed.ALL\n }\n}\n\ntype Context = {\n hydrator: Hydrator\n views: Views\n cfg: ServerConfig\n}\n\ntype Params = app.bsky.notification.getGroupedNotifications.$Params & {\n hydrateCtx: HydrateCtxWithViewer\n seenAt?: DatetimeString\n}\n\ntype SkeletonState = {\n groups: NotificationGroup[]\n cursor?: string\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import { NotificationFeed } from '../../../../../proto/bsky_pb.js';
2
+ import type { GroupingResult, NotificationItem } from './grouping.js';
3
+ export declare const buildAlgoGravityGroups: (items: NotificationItem[], limit: number, now: number, seenAt?: number, feed?: NotificationFeed) => GroupingResult;
4
+ //# sourceMappingURL=algoGravity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"algoGravity.d.ts","sourceRoot":"","sources":["../../../../../../src/api/app/bsky/notification/grouping/algoGravity.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAA;AAClE,OAAO,KAAK,EACV,cAAc,EAEd,gBAAgB,EACjB,MAAM,eAAe,CAAA;AA2CtB,eAAO,MAAM,sBAAsB,UAC1B,gBAAgB,EAAE,SAClB,MAAM,OACR,MAAM,WACF,MAAM,8BAEd,cAwDF,CAAA"}
@@ -0,0 +1,141 @@
1
+ import { HOUR, MINUTE, SECOND } from '@atproto/common';
2
+ import { NotificationFeed } from '../../../../../proto/bsky_pb.js';
3
+ import { MAX_GROUP_SIZE, canGroupNotification, compareNotificationGroupsNewestFirst, isNotificationRead, } from './shared.js';
4
+ const PARAMS = {
5
+ zones: [
6
+ // @NOTE: Keep in ascending ageMs, null last.
7
+ { ageMs: 30 * MINUTE, gravity: 3, attractionSpanMs: 30 * MINUTE },
8
+ { ageMs: HOUR, gravity: 2.2, attractionSpanMs: HOUR },
9
+ { ageMs: 8 * HOUR, gravity: 1, attractionSpanMs: 12 * HOUR },
10
+ { ageMs: null, gravity: 0.5, attractionSpanMs: 24 * HOUR },
11
+ ],
12
+ attractionCoefficient: 0.9,
13
+ notificationWeight: 0.005,
14
+ };
15
+ export const buildAlgoGravityGroups = (items, limit, now, seenAt, feed = NotificationFeed.ALL) => {
16
+ const zones = PARAMS.zones;
17
+ const chains = new Map();
18
+ let groupCount = 0;
19
+ for (const [itemIndex, item] of items.entries()) {
20
+ const canGroup = canGroupNotification(item.raw.reason, feed);
21
+ const key = JSON.stringify([
22
+ item.raw.reason,
23
+ item.groupingKey,
24
+ canGroup ? undefined : item.id,
25
+ ]);
26
+ const candidateGroups = chains.get(key) ?? [];
27
+ const active = candidateGroups.at(-1);
28
+ let nextCandidateGroups;
29
+ if (!active || active.closed) {
30
+ nextCandidateGroups = [
31
+ ...candidateGroups,
32
+ createCandidateGroup([item], now, zones),
33
+ ];
34
+ }
35
+ else {
36
+ const extended = appendItem(active, item, PARAMS.attractionCoefficient);
37
+ const stableCandidateGroups = splitUntilStable(extended, PARAMS.notificationWeight, PARAMS.attractionCoefficient, now, zones).map((candidateGroup) => candidateGroup.items.length === MAX_GROUP_SIZE
38
+ ? { ...candidateGroup, closed: true }
39
+ : candidateGroup);
40
+ nextCandidateGroups = [
41
+ ...candidateGroups.slice(0, -1),
42
+ ...stableCandidateGroups,
43
+ ];
44
+ }
45
+ const nextGroupCount = groupCount - candidateGroups.length + nextCandidateGroups.length;
46
+ if (nextGroupCount > limit) {
47
+ return {
48
+ groups: toGroups(chains, seenAt),
49
+ cursor: items[itemIndex - 1]?.raw.indexedAt,
50
+ };
51
+ }
52
+ chains.set(key, nextCandidateGroups);
53
+ groupCount = nextGroupCount;
54
+ }
55
+ return {
56
+ groups: toGroups(chains, seenAt),
57
+ };
58
+ };
59
+ const splitUntilStable = (candidateGroup, notificationWeight, attractionCoefficient, now, zones) => {
60
+ if (candidateGroup.items.length < 2 ||
61
+ candidateGroup.occupiedSeconds *
62
+ notificationWeight *
63
+ candidateGroup.zone.gravity <=
64
+ candidateGroup.weakestAttraction) {
65
+ return [candidateGroup];
66
+ }
67
+ // Resolve equal weakest bindings by cutting nearest the segment midpoint.
68
+ const splitIndex = midpointIndex(candidateGroup.weakestBindingIndexes, candidateGroup.items.length);
69
+ const newer = createCandidateGroup(candidateGroup.items.slice(0, splitIndex + 1), now, zones, attractionCoefficient);
70
+ const older = createCandidateGroup(candidateGroup.items.slice(splitIndex + 1), now, zones, attractionCoefficient);
71
+ const newerCandidateGroups = splitUntilStable(newer, notificationWeight, attractionCoefficient, now, zones).map((part) => ({ ...part, closed: true }));
72
+ const olderCandidateGroups = splitUntilStable(older, notificationWeight, attractionCoefficient, now, zones);
73
+ return [...newerCandidateGroups, ...olderCandidateGroups];
74
+ };
75
+ const createCandidateGroup = (items, now, zones, attractionCoefficient = PARAMS.attractionCoefficient) => {
76
+ const newest = items[0];
77
+ const age = Math.max(0, now - Date.parse(newest.raw.indexedAt));
78
+ const zone = zones.find(({ ageMs: maxAgeMs }) => maxAgeMs === null || age <= maxAgeMs);
79
+ const candidateGroup = {
80
+ items,
81
+ occupiedSeconds: new Set(items.map(({ raw }) => Math.floor(Date.parse(raw.indexedAt) / SECOND))).size,
82
+ zone,
83
+ weakestAttraction: Number.POSITIVE_INFINITY,
84
+ weakestBindingIndexes: [],
85
+ closed: false,
86
+ };
87
+ for (let index = 0; index < items.length - 1; index++) {
88
+ const gap = Date.parse(items[index].raw.indexedAt) -
89
+ Date.parse(items[index + 1].raw.indexedAt);
90
+ addBindingAttraction(candidateGroup, index, linearAttraction(gap, attractionCoefficient, candidateGroup.zone.attractionSpanMs));
91
+ }
92
+ return candidateGroup;
93
+ };
94
+ const appendItem = (candidateGroup, item, attractionCoefficient) => {
95
+ const newestBindingIndex = candidateGroup.items.length - 1;
96
+ const previousTime = Date.parse(candidateGroup.items.at(-1).raw.indexedAt);
97
+ const itemTime = Date.parse(item.raw.indexedAt);
98
+ const gap = previousTime - itemTime;
99
+ const extended = {
100
+ ...candidateGroup,
101
+ items: [...candidateGroup.items, item],
102
+ occupiedSeconds: candidateGroup.occupiedSeconds +
103
+ Number(Math.floor(previousTime / SECOND) !== Math.floor(itemTime / SECOND)),
104
+ weakestBindingIndexes: [...candidateGroup.weakestBindingIndexes],
105
+ };
106
+ addBindingAttraction(extended, newestBindingIndex, linearAttraction(gap, attractionCoefficient, candidateGroup.zone.attractionSpanMs));
107
+ return extended;
108
+ };
109
+ const addBindingAttraction = (candidateGroup, index, attraction) => {
110
+ if (attraction < candidateGroup.weakestAttraction) {
111
+ candidateGroup.weakestAttraction = attraction;
112
+ candidateGroup.weakestBindingIndexes = [index];
113
+ }
114
+ else if (attraction === candidateGroup.weakestAttraction) {
115
+ candidateGroup.weakestBindingIndexes.push(index);
116
+ }
117
+ };
118
+ const midpointIndex = (indexes, itemCount) => {
119
+ const midpoint = (itemCount - 2) / 2;
120
+ return indexes.reduce((best, index) => Math.abs(index - midpoint) < Math.abs(best - midpoint) ? index : best);
121
+ };
122
+ const linearAttraction = (gapMs, attractionCoefficient, attractionSpanMs) => attractionCoefficient * Math.max(0, 1 - Math.max(0, gapMs) / attractionSpanMs);
123
+ const toGroups = (chains, seenAt) => [...chains.values()]
124
+ .flatMap((candidateGroups) => candidateGroups)
125
+ .map((candidateGroup) => {
126
+ const newest = candidateGroup.items[0];
127
+ const oldest = candidateGroup.items.at(-1);
128
+ return {
129
+ id: newest.id,
130
+ kind: newest.raw.reason,
131
+ groupingKey: newest.groupingKey,
132
+ actorDids: candidateGroup.items.map(({ actorDid }) => actorDid),
133
+ itemCount: candidateGroup.items.length,
134
+ indexedAt: newest.raw.indexedAt,
135
+ firstIndexedAt: oldest.raw.indexedAt,
136
+ isRead: isNotificationRead(newest.raw.indexedAt, seenAt),
137
+ items: candidateGroup.items,
138
+ };
139
+ })
140
+ .sort(compareNotificationGroupsNewestFirst);
141
+ //# sourceMappingURL=algoGravity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"algoGravity.js","sourceRoot":"","sources":["../../../../../../src/api/app/bsky/notification/grouping/algoGravity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAA;AAMlE,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,oCAAoC,EACpC,kBAAkB,GACnB,MAAM,aAAa,CAAA;AAcpB,MAAM,MAAM,GAAW;IACrB,KAAK,EAAE;QACL,6CAA6C;QAC7C,EAAE,KAAK,EAAE,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,gBAAgB,EAAE,EAAE,GAAG,MAAM,EAAE;QACjE,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE;QACrD,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,gBAAgB,EAAE,EAAE,GAAG,IAAI,EAAE;QAC5D,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,gBAAgB,EAAE,EAAE,GAAG,IAAI,EAAE;KAC3D;IACD,qBAAqB,EAAE,GAAG;IAC1B,kBAAkB,EAAE,KAAK;CAC1B,CAAA;AAaD,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACpC,KAAyB,EACzB,KAAa,EACb,GAAW,EACX,MAAe,EACf,IAAI,GAAG,gBAAgB,CAAC,GAAG,EACX,EAAE;IAClB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAA;IAC1B,MAAM,MAAM,GAAG,IAAI,GAAG,EAA4B,CAAA;IAClD,IAAI,UAAU,GAAG,CAAC,CAAA;IAElB,KAAK,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAChD,MAAM,QAAQ,GAAG,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;QAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;YACzB,IAAI,CAAC,GAAG,CAAC,MAAM;YACf,IAAI,CAAC,WAAW;YAChB,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE;SAC/B,CAAC,CAAA;QACF,MAAM,eAAe,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAA;QAC7C,MAAM,MAAM,GAAG,eAAe,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;QACrC,IAAI,mBAAqC,CAAA;QAEzC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAC7B,mBAAmB,GAAG;gBACpB,GAAG,eAAe;gBAClB,oBAAoB,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC;aACzC,CAAA;QACH,CAAC;aAAM,CAAC;YACN,MAAM,QAAQ,GAAG,UAAU,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,qBAAqB,CAAC,CAAA;YACvE,MAAM,qBAAqB,GAAG,gBAAgB,CAC5C,QAAQ,EACR,MAAM,CAAC,kBAAkB,EACzB,MAAM,CAAC,qBAAqB,EAC5B,GAAG,EACH,KAAK,CACN,CAAC,GAAG,CAAC,CAAC,cAAc,EAAE,EAAE,CACvB,cAAc,CAAC,KAAK,CAAC,MAAM,KAAK,cAAc;gBAC5C,CAAC,CAAC,EAAE,GAAG,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE;gBACrC,CAAC,CAAC,cAAc,CACnB,CAAA;YACD,mBAAmB,GAAG;gBACpB,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC/B,GAAG,qBAAqB;aACzB,CAAA;QACH,CAAC;QAED,MAAM,cAAc,GAClB,UAAU,GAAG,eAAe,CAAC,MAAM,GAAG,mBAAmB,CAAC,MAAM,CAAA;QAClE,IAAI,cAAc,GAAG,KAAK,EAAE,CAAC;YAC3B,OAAO;gBACL,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAChC,MAAM,EAAE,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,SAAS;aAC5C,CAAA;QACH,CAAC;QAED,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAA;QACpC,UAAU,GAAG,cAAc,CAAA;IAC7B,CAAC;IAED,OAAO;QACL,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACjC,CAAA;AACH,CAAC,CAAA;AAED,MAAM,gBAAgB,GAAG,CACvB,cAA8B,EAC9B,kBAA0B,EAC1B,qBAA6B,EAC7B,GAAW,EACX,KAAa,EACK,EAAE;IACpB,IACE,cAAc,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;QAC/B,cAAc,CAAC,eAAe;YAC5B,kBAAkB;YAClB,cAAc,CAAC,IAAI,CAAC,OAAO;YAC3B,cAAc,CAAC,iBAAiB,EAClC,CAAC;QACD,OAAO,CAAC,cAAc,CAAC,CAAA;IACzB,CAAC;IAED,0EAA0E;IAC1E,MAAM,UAAU,GAAG,aAAa,CAC9B,cAAc,CAAC,qBAAqB,EACpC,cAAc,CAAC,KAAK,CAAC,MAAM,CAC5B,CAAA;IACD,MAAM,KAAK,GAAG,oBAAoB,CAChC,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,GAAG,CAAC,CAAC,EAC7C,GAAG,EACH,KAAK,EACL,qBAAqB,CACtB,CAAA;IACD,MAAM,KAAK,GAAG,oBAAoB,CAChC,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,EAC1C,GAAG,EACH,KAAK,EACL,qBAAqB,CACtB,CAAA;IACD,MAAM,oBAAoB,GAAG,gBAAgB,CAC3C,KAAK,EACL,kBAAkB,EAClB,qBAAqB,EACrB,GAAG,EACH,KAAK,CACN,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAC5C,MAAM,oBAAoB,GAAG,gBAAgB,CAC3C,KAAK,EACL,kBAAkB,EAClB,qBAAqB,EACrB,GAAG,EACH,KAAK,CACN,CAAA;IACD,OAAO,CAAC,GAAG,oBAAoB,EAAE,GAAG,oBAAoB,CAAC,CAAA;AAC3D,CAAC,CAAA;AAED,MAAM,oBAAoB,GAAG,CAC3B,KAAyB,EACzB,GAAW,EACX,KAAa,EACb,qBAAqB,GAAG,MAAM,CAAC,qBAAqB,EACpC,EAAE;IAClB,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAE,CAAA;IACxB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAA;IAC/D,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CACrB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,CAC7D,CAAA;IACF,MAAM,cAAc,GAAmB;QACrC,KAAK;QACL,eAAe,EAAE,IAAI,GAAG,CACtB,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,CAAC,CACvE,CAAC,IAAI;QACN,IAAI;QACJ,iBAAiB,EAAE,MAAM,CAAC,iBAAiB;QAC3C,qBAAqB,EAAE,EAAE;QACzB,MAAM,EAAE,KAAK;KACd,CAAA;IAED,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;QACtD,MAAM,GAAG,GACP,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAE,CAAC,GAAG,CAAC,SAAS,CAAC;YACvC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAC7C,oBAAoB,CAClB,cAAc,EACd,KAAK,EACL,gBAAgB,CACd,GAAG,EACH,qBAAqB,EACrB,cAAc,CAAC,IAAI,CAAC,gBAAgB,CACrC,CACF,CAAA;IACH,CAAC;IAED,OAAO,cAAc,CAAA;AACvB,CAAC,CAAA;AAED,MAAM,UAAU,GAAG,CACjB,cAA8B,EAC9B,IAAsB,EACtB,qBAA6B,EACb,EAAE;IAClB,MAAM,kBAAkB,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;IAC1D,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IAC3E,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IAC/C,MAAM,GAAG,GAAG,YAAY,GAAG,QAAQ,CAAA;IACnC,MAAM,QAAQ,GAAmB;QAC/B,GAAG,cAAc;QACjB,KAAK,EAAE,CAAC,GAAG,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC;QACtC,eAAe,EACb,cAAc,CAAC,eAAe;YAC9B,MAAM,CACJ,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC,CACpE;QACH,qBAAqB,EAAE,CAAC,GAAG,cAAc,CAAC,qBAAqB,CAAC;KACjE,CAAA;IACD,oBAAoB,CAClB,QAAQ,EACR,kBAAkB,EAClB,gBAAgB,CACd,GAAG,EACH,qBAAqB,EACrB,cAAc,CAAC,IAAI,CAAC,gBAAgB,CACrC,CACF,CAAA;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED,MAAM,oBAAoB,GAAG,CAC3B,cAA8B,EAC9B,KAAa,EACb,UAAkB,EAClB,EAAE;IACF,IAAI,UAAU,GAAG,cAAc,CAAC,iBAAiB,EAAE,CAAC;QAClD,cAAc,CAAC,iBAAiB,GAAG,UAAU,CAAA;QAC7C,cAAc,CAAC,qBAAqB,GAAG,CAAC,KAAK,CAAC,CAAA;IAChD,CAAC;SAAM,IAAI,UAAU,KAAK,cAAc,CAAC,iBAAiB,EAAE,CAAC;QAC3D,cAAc,CAAC,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAClD,CAAC;AACH,CAAC,CAAA;AAED,MAAM,aAAa,GAAG,CAAC,OAAiB,EAAE,SAAiB,EAAU,EAAE;IACrE,MAAM,QAAQ,GAAG,CAAC,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;IACpC,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CACpC,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,QAAQ,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CACtE,CAAA;AACH,CAAC,CAAA;AAED,MAAM,gBAAgB,GAAG,CACvB,KAAa,EACb,qBAA6B,EAC7B,gBAAwB,EACxB,EAAE,CACF,qBAAqB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,gBAAgB,CAAC,CAAA;AAEhF,MAAM,QAAQ,GAAG,CACf,MAAqC,EACrC,MAA0B,EACL,EAAE,CACvB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;KACjB,OAAO,CAAC,CAAC,eAAe,EAAE,EAAE,CAAC,eAAe,CAAC;KAC7C,GAAG,CAAC,CAAC,cAAc,EAAE,EAAE;IACtB,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC,CAAE,CAAA;IACvC,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,CAAA;IAC3C,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM;QACvB,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,SAAS,EAAE,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC;QAC/D,SAAS,EAAE,cAAc,CAAC,KAAK,CAAC,MAAM;QACtC,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS;QAC/B,cAAc,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS;QACpC,MAAM,EAAE,kBAAkB,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC;QACxD,KAAK,EAAE,cAAc,CAAC,KAAK;KAC5B,CAAA;AACH,CAAC,CAAC;KACD,IAAI,CAAC,oCAAoC,CAAC,CAAA","sourcesContent":["import { HOUR, MINUTE, SECOND } from '@atproto/common'\nimport { NotificationFeed } from '../../../../../proto/bsky_pb.js'\nimport type {\n GroupingResult,\n NotificationGroup,\n NotificationItem,\n} from './grouping.js'\nimport {\n MAX_GROUP_SIZE,\n canGroupNotification,\n compareNotificationGroupsNewestFirst,\n isNotificationRead,\n} from './shared.js'\n\ntype Zone = {\n ageMs: number | null\n gravity: number\n attractionSpanMs: number\n}\n\ntype Params = {\n zones: Zone[]\n attractionCoefficient: number\n notificationWeight: number\n}\n\nconst PARAMS: Params = {\n zones: [\n // @NOTE: Keep in ascending ageMs, null last.\n { ageMs: 30 * MINUTE, gravity: 3, attractionSpanMs: 30 * MINUTE },\n { ageMs: HOUR, gravity: 2.2, attractionSpanMs: HOUR },\n { ageMs: 8 * HOUR, gravity: 1, attractionSpanMs: 12 * HOUR },\n { ageMs: null, gravity: 0.5, attractionSpanMs: 24 * HOUR },\n ],\n attractionCoefficient: 0.9,\n notificationWeight: 0.005,\n}\n\n// A notification group while Gravity is building it: holds its notifications and\n// the measurements used to decide whether to split it or accept more notifications.\ntype CandidateGroup = {\n items: NotificationItem[]\n occupiedSeconds: number\n zone: Zone\n weakestAttraction: number\n weakestBindingIndexes: number[]\n closed: boolean\n}\n\nexport const buildAlgoGravityGroups = (\n items: NotificationItem[],\n limit: number,\n now: number,\n seenAt?: number,\n feed = NotificationFeed.ALL,\n): GroupingResult => {\n const zones = PARAMS.zones\n const chains = new Map<string, CandidateGroup[]>()\n let groupCount = 0\n\n for (const [itemIndex, item] of items.entries()) {\n const canGroup = canGroupNotification(item.raw.reason, feed)\n const key = JSON.stringify([\n item.raw.reason,\n item.groupingKey,\n canGroup ? undefined : item.id,\n ])\n const candidateGroups = chains.get(key) ?? []\n const active = candidateGroups.at(-1)\n let nextCandidateGroups: CandidateGroup[]\n\n if (!active || active.closed) {\n nextCandidateGroups = [\n ...candidateGroups,\n createCandidateGroup([item], now, zones),\n ]\n } else {\n const extended = appendItem(active, item, PARAMS.attractionCoefficient)\n const stableCandidateGroups = splitUntilStable(\n extended,\n PARAMS.notificationWeight,\n PARAMS.attractionCoefficient,\n now,\n zones,\n ).map((candidateGroup) =>\n candidateGroup.items.length === MAX_GROUP_SIZE\n ? { ...candidateGroup, closed: true }\n : candidateGroup,\n )\n nextCandidateGroups = [\n ...candidateGroups.slice(0, -1),\n ...stableCandidateGroups,\n ]\n }\n\n const nextGroupCount =\n groupCount - candidateGroups.length + nextCandidateGroups.length\n if (nextGroupCount > limit) {\n return {\n groups: toGroups(chains, seenAt),\n cursor: items[itemIndex - 1]?.raw.indexedAt,\n }\n }\n\n chains.set(key, nextCandidateGroups)\n groupCount = nextGroupCount\n }\n\n return {\n groups: toGroups(chains, seenAt),\n }\n}\n\nconst splitUntilStable = (\n candidateGroup: CandidateGroup,\n notificationWeight: number,\n attractionCoefficient: number,\n now: number,\n zones: Zone[],\n): CandidateGroup[] => {\n if (\n candidateGroup.items.length < 2 ||\n candidateGroup.occupiedSeconds *\n notificationWeight *\n candidateGroup.zone.gravity <=\n candidateGroup.weakestAttraction\n ) {\n return [candidateGroup]\n }\n\n // Resolve equal weakest bindings by cutting nearest the segment midpoint.\n const splitIndex = midpointIndex(\n candidateGroup.weakestBindingIndexes,\n candidateGroup.items.length,\n )\n const newer = createCandidateGroup(\n candidateGroup.items.slice(0, splitIndex + 1),\n now,\n zones,\n attractionCoefficient,\n )\n const older = createCandidateGroup(\n candidateGroup.items.slice(splitIndex + 1),\n now,\n zones,\n attractionCoefficient,\n )\n const newerCandidateGroups = splitUntilStable(\n newer,\n notificationWeight,\n attractionCoefficient,\n now,\n zones,\n ).map((part) => ({ ...part, closed: true }))\n const olderCandidateGroups = splitUntilStable(\n older,\n notificationWeight,\n attractionCoefficient,\n now,\n zones,\n )\n return [...newerCandidateGroups, ...olderCandidateGroups]\n}\n\nconst createCandidateGroup = (\n items: NotificationItem[],\n now: number,\n zones: Zone[],\n attractionCoefficient = PARAMS.attractionCoefficient,\n): CandidateGroup => {\n const newest = items[0]!\n const age = Math.max(0, now - Date.parse(newest.raw.indexedAt))\n const zone = zones.find(\n ({ ageMs: maxAgeMs }) => maxAgeMs === null || age <= maxAgeMs,\n )!\n const candidateGroup: CandidateGroup = {\n items,\n occupiedSeconds: new Set(\n items.map(({ raw }) => Math.floor(Date.parse(raw.indexedAt) / SECOND)),\n ).size,\n zone,\n weakestAttraction: Number.POSITIVE_INFINITY,\n weakestBindingIndexes: [],\n closed: false,\n }\n\n for (let index = 0; index < items.length - 1; index++) {\n const gap =\n Date.parse(items[index]!.raw.indexedAt) -\n Date.parse(items[index + 1]!.raw.indexedAt)\n addBindingAttraction(\n candidateGroup,\n index,\n linearAttraction(\n gap,\n attractionCoefficient,\n candidateGroup.zone.attractionSpanMs,\n ),\n )\n }\n\n return candidateGroup\n}\n\nconst appendItem = (\n candidateGroup: CandidateGroup,\n item: NotificationItem,\n attractionCoefficient: number,\n): CandidateGroup => {\n const newestBindingIndex = candidateGroup.items.length - 1\n const previousTime = Date.parse(candidateGroup.items.at(-1)!.raw.indexedAt)\n const itemTime = Date.parse(item.raw.indexedAt)\n const gap = previousTime - itemTime\n const extended: CandidateGroup = {\n ...candidateGroup,\n items: [...candidateGroup.items, item],\n occupiedSeconds:\n candidateGroup.occupiedSeconds +\n Number(\n Math.floor(previousTime / SECOND) !== Math.floor(itemTime / SECOND),\n ),\n weakestBindingIndexes: [...candidateGroup.weakestBindingIndexes],\n }\n addBindingAttraction(\n extended,\n newestBindingIndex,\n linearAttraction(\n gap,\n attractionCoefficient,\n candidateGroup.zone.attractionSpanMs,\n ),\n )\n return extended\n}\n\nconst addBindingAttraction = (\n candidateGroup: CandidateGroup,\n index: number,\n attraction: number,\n) => {\n if (attraction < candidateGroup.weakestAttraction) {\n candidateGroup.weakestAttraction = attraction\n candidateGroup.weakestBindingIndexes = [index]\n } else if (attraction === candidateGroup.weakestAttraction) {\n candidateGroup.weakestBindingIndexes.push(index)\n }\n}\n\nconst midpointIndex = (indexes: number[], itemCount: number): number => {\n const midpoint = (itemCount - 2) / 2\n return indexes.reduce((best, index) =>\n Math.abs(index - midpoint) < Math.abs(best - midpoint) ? index : best,\n )\n}\n\nconst linearAttraction = (\n gapMs: number,\n attractionCoefficient: number,\n attractionSpanMs: number,\n) =>\n attractionCoefficient * Math.max(0, 1 - Math.max(0, gapMs) / attractionSpanMs)\n\nconst toGroups = (\n chains: Map<string, CandidateGroup[]>,\n seenAt: number | undefined,\n): NotificationGroup[] =>\n [...chains.values()]\n .flatMap((candidateGroups) => candidateGroups)\n .map((candidateGroup) => {\n const newest = candidateGroup.items[0]!\n const oldest = candidateGroup.items.at(-1)!\n return {\n id: newest.id,\n kind: newest.raw.reason,\n groupingKey: newest.groupingKey,\n actorDids: candidateGroup.items.map(({ actorDid }) => actorDid),\n itemCount: candidateGroup.items.length,\n indexedAt: newest.raw.indexedAt,\n firstIndexedAt: oldest.raw.indexedAt,\n isRead: isNotificationRead(newest.raw.indexedAt, seenAt),\n items: candidateGroup.items,\n }\n })\n .sort(compareNotificationGroupsNewestFirst)\n"]}
@@ -0,0 +1,4 @@
1
+ import { NotificationFeed } from '../../../../../proto/bsky_pb.js';
2
+ import type { GroupingResult, NotificationItem } from './grouping.js';
3
+ export declare const buildAlgoLookbackGroups: (items: NotificationItem[], limit: number, now: number, seenAt?: number, feed?: NotificationFeed) => GroupingResult;
4
+ //# sourceMappingURL=algoLookback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"algoLookback.d.ts","sourceRoot":"","sources":["../../../../../../src/api/app/bsky/notification/grouping/algoLookback.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAA;AAClE,OAAO,KAAK,EACV,cAAc,EAEd,gBAAgB,EACjB,MAAM,eAAe,CAAA;AA0BtB,eAAO,MAAM,uBAAuB,UAC3B,gBAAgB,EAAE,SAClB,MAAM,OACR,MAAM,WACF,MAAM,8BAEd,cAyDF,CAAA"}