@atproto/bsky 0.0.16 → 0.0.18

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 (110) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cache/read-through.d.ts +30 -0
  3. package/dist/config.d.ts +18 -0
  4. package/dist/context.d.ts +6 -6
  5. package/dist/daemon/config.d.ts +15 -0
  6. package/dist/daemon/context.d.ts +15 -0
  7. package/dist/daemon/index.d.ts +23 -0
  8. package/dist/daemon/logger.d.ts +3 -0
  9. package/dist/daemon/notifications.d.ts +18 -0
  10. package/dist/daemon/services.d.ts +11 -0
  11. package/dist/db/database-schema.d.ts +1 -2
  12. package/dist/db/index.js +16 -1
  13. package/dist/db/index.js.map +3 -3
  14. package/dist/db/migrations/20231205T000257238Z-remove-did-cache.d.ts +3 -0
  15. package/dist/db/migrations/index.d.ts +1 -0
  16. package/dist/did-cache.d.ts +10 -7
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.js +1921 -938
  19. package/dist/index.js.map +3 -3
  20. package/dist/indexer/context.d.ts +2 -0
  21. package/dist/indexer/index.d.ts +1 -0
  22. package/dist/lexicon/index.d.ts +12 -0
  23. package/dist/lexicon/lexicons.d.ts +134 -0
  24. package/dist/lexicon/types/com/atproto/admin/deleteAccount.d.ts +25 -0
  25. package/dist/lexicon/types/com/atproto/temp/importRepo.d.ts +32 -0
  26. package/dist/lexicon/types/com/atproto/temp/pushBlob.d.ts +25 -0
  27. package/dist/lexicon/types/com/atproto/temp/transferAccount.d.ts +42 -0
  28. package/dist/logger.d.ts +1 -0
  29. package/dist/redis.d.ts +10 -1
  30. package/dist/services/actor/index.d.ts +18 -4
  31. package/dist/services/actor/views.d.ts +5 -7
  32. package/dist/services/feed/index.d.ts +6 -4
  33. package/dist/services/feed/views.d.ts +5 -4
  34. package/dist/services/index.d.ts +3 -7
  35. package/dist/services/label/index.d.ts +10 -4
  36. package/dist/services/moderation/index.d.ts +0 -1
  37. package/dist/services/types.d.ts +3 -0
  38. package/dist/services/util/notification.d.ts +5 -0
  39. package/dist/services/util/post.d.ts +6 -6
  40. package/dist/util/retry.d.ts +1 -6
  41. package/package.json +6 -6
  42. package/src/api/app/bsky/actor/searchActorsTypeahead.ts +1 -1
  43. package/src/cache/read-through.ts +151 -0
  44. package/src/config.ts +90 -1
  45. package/src/context.ts +7 -7
  46. package/src/daemon/config.ts +60 -0
  47. package/src/daemon/context.ts +27 -0
  48. package/src/daemon/index.ts +78 -0
  49. package/src/daemon/logger.ts +6 -0
  50. package/src/daemon/notifications.ts +54 -0
  51. package/src/daemon/services.ts +22 -0
  52. package/src/db/database-schema.ts +0 -2
  53. package/src/db/migrations/20231205T000257238Z-remove-did-cache.ts +14 -0
  54. package/src/db/migrations/index.ts +1 -0
  55. package/src/did-cache.ts +33 -56
  56. package/src/feed-gen/index.ts +0 -4
  57. package/src/index.ts +55 -16
  58. package/src/indexer/context.ts +5 -0
  59. package/src/indexer/index.ts +10 -7
  60. package/src/lexicon/index.ts +50 -0
  61. package/src/lexicon/lexicons.ts +156 -0
  62. package/src/lexicon/types/com/atproto/admin/deleteAccount.ts +38 -0
  63. package/src/lexicon/types/com/atproto/temp/importRepo.ts +45 -0
  64. package/src/lexicon/types/com/atproto/temp/pushBlob.ts +39 -0
  65. package/src/lexicon/types/com/atproto/temp/transferAccount.ts +62 -0
  66. package/src/logger.ts +2 -0
  67. package/src/redis.ts +43 -3
  68. package/src/services/actor/index.ts +55 -7
  69. package/src/services/actor/views.ts +16 -13
  70. package/src/services/feed/index.ts +27 -13
  71. package/src/services/feed/views.ts +20 -10
  72. package/src/services/index.ts +14 -14
  73. package/src/services/indexing/index.ts +7 -10
  74. package/src/services/indexing/plugins/post.ts +13 -0
  75. package/src/services/label/index.ts +66 -22
  76. package/src/services/moderation/index.ts +1 -1
  77. package/src/services/moderation/status.ts +1 -4
  78. package/src/services/types.ts +4 -0
  79. package/src/services/util/notification.ts +70 -0
  80. package/src/util/retry.ts +1 -44
  81. package/tests/admin/get-repo.test.ts +5 -3
  82. package/tests/admin/moderation.test.ts +2 -2
  83. package/tests/admin/repo-search.test.ts +1 -0
  84. package/tests/algos/hot-classic.test.ts +1 -2
  85. package/tests/auth.test.ts +1 -1
  86. package/tests/auto-moderator/labeler.test.ts +19 -20
  87. package/tests/auto-moderator/takedowns.test.ts +16 -10
  88. package/tests/blob-resolver.test.ts +4 -2
  89. package/tests/daemon.test.ts +191 -0
  90. package/tests/did-cache.test.ts +20 -5
  91. package/tests/handle-invalidation.test.ts +1 -5
  92. package/tests/indexing.test.ts +20 -13
  93. package/tests/redis-cache.test.ts +231 -0
  94. package/tests/seeds/basic.ts +3 -0
  95. package/tests/subscription/repo.test.ts +4 -7
  96. package/tests/views/profile.test.ts +0 -1
  97. package/tests/views/thread.test.ts +73 -78
  98. package/tests/views/threadgating.test.ts +38 -0
  99. package/dist/db/tables/did-cache.d.ts +0 -10
  100. package/dist/feed-gen/best-of-follows.d.ts +0 -29
  101. package/dist/feed-gen/whats-hot.d.ts +0 -29
  102. package/dist/feed-gen/with-friends.d.ts +0 -3
  103. package/dist/label-cache.d.ts +0 -19
  104. package/src/db/tables/did-cache.ts +0 -13
  105. package/src/feed-gen/best-of-follows.ts +0 -77
  106. package/src/feed-gen/whats-hot.ts +0 -101
  107. package/src/feed-gen/with-friends.ts +0 -43
  108. package/src/label-cache.ts +0 -90
  109. package/tests/algos/whats-hot.test.ts +0 -118
  110. package/tests/algos/with-friends.test.ts +0 -145
@@ -1,4 +1,5 @@
1
1
  import { mapDefined } from '@atproto/common'
2
+ import { AtUri } from '@atproto/syntax'
2
3
  import { Database } from '../../db'
3
4
  import {
4
5
  FeedViewPost,
@@ -37,26 +38,35 @@ import {
37
38
  } from './types'
38
39
  import { Labels, getSelfLabels } from '../label'
39
40
  import { ImageUriBuilder } from '../../image/uri'
40
- import { LabelCache } from '../../label-cache'
41
41
  import { ActorInfoMap, ActorService } from '../actor'
42
42
  import { ListInfoMap, GraphService } from '../graph'
43
- import { AtUri } from '@atproto/syntax'
43
+ import { FromDb } from '../types'
44
44
  import { parseThreadGate } from './util'
45
45
 
46
46
  export class FeedViews {
47
+ services: {
48
+ actor: ActorService
49
+ graph: GraphService
50
+ }
51
+
47
52
  constructor(
48
53
  public db: Database,
49
54
  public imgUriBuilder: ImageUriBuilder,
50
- public labelCache: LabelCache,
51
- ) {}
52
-
53
- static creator(imgUriBuilder: ImageUriBuilder, labelCache: LabelCache) {
54
- return (db: Database) => new FeedViews(db, imgUriBuilder, labelCache)
55
+ private actor: FromDb<ActorService>,
56
+ private graph: FromDb<GraphService>,
57
+ ) {
58
+ this.services = {
59
+ actor: actor(this.db),
60
+ graph: graph(this.db),
61
+ }
55
62
  }
56
63
 
57
- services = {
58
- actor: ActorService.creator(this.imgUriBuilder, this.labelCache)(this.db),
59
- graph: GraphService.creator(this.imgUriBuilder)(this.db),
64
+ static creator(
65
+ imgUriBuilder: ImageUriBuilder,
66
+ actor: FromDb<ActorService>,
67
+ graph: FromDb<GraphService>,
68
+ ) {
69
+ return (db: Database) => new FeedViews(db, imgUriBuilder, actor, graph)
60
70
  }
61
71
 
62
72
  formatFeedGeneratorView(
@@ -1,25 +1,29 @@
1
- import { Database, PrimaryDatabase } from '../db'
2
1
  import { ImageUriBuilder } from '../image/uri'
3
2
  import { ActorService } from './actor'
4
3
  import { FeedService } from './feed'
5
4
  import { GraphService } from './graph'
6
5
  import { ModerationService } from './moderation'
7
- import { LabelService } from './label'
6
+ import { LabelCacheOpts, LabelService } from './label'
8
7
  import { ImageInvalidator } from '../image/invalidator'
9
- import { LabelCache } from '../label-cache'
8
+ import { FromDb, FromDbPrimary } from './types'
10
9
 
11
10
  export function createServices(resources: {
12
11
  imgUriBuilder: ImageUriBuilder
13
12
  imgInvalidator: ImageInvalidator
14
- labelCache: LabelCache
13
+ labelCacheOpts: LabelCacheOpts
15
14
  }): Services {
16
- const { imgUriBuilder, imgInvalidator, labelCache } = resources
15
+ const { imgUriBuilder, imgInvalidator, labelCacheOpts } = resources
16
+ const label = LabelService.creator(labelCacheOpts)
17
+ const graph = GraphService.creator(imgUriBuilder)
18
+ const actor = ActorService.creator(imgUriBuilder, graph, label)
19
+ const moderation = ModerationService.creator(imgUriBuilder, imgInvalidator)
20
+ const feed = FeedService.creator(imgUriBuilder, actor, label, graph)
17
21
  return {
18
- actor: ActorService.creator(imgUriBuilder, labelCache),
19
- feed: FeedService.creator(imgUriBuilder, labelCache),
20
- graph: GraphService.creator(imgUriBuilder),
21
- moderation: ModerationService.creator(imgUriBuilder, imgInvalidator),
22
- label: LabelService.creator(labelCache),
22
+ actor,
23
+ feed,
24
+ moderation,
25
+ graph,
26
+ label,
23
27
  }
24
28
  }
25
29
 
@@ -30,7 +34,3 @@ export type Services = {
30
34
  moderation: FromDbPrimary<ModerationService>
31
35
  label: FromDb<LabelService>
32
36
  }
33
-
34
- type FromDb<T> = (db: Database) => T
35
-
36
- type FromDbPrimary<T> = (db: PrimaryDatabase) => T
@@ -7,6 +7,7 @@ import {
7
7
  verifyRepo,
8
8
  Commit,
9
9
  VerifiedRepo,
10
+ getAndParseRecord,
10
11
  } from '@atproto/repo'
11
12
  import { AtUri } from '@atproto/syntax'
12
13
  import { IdResolver, getPds } from '@atproto/identity'
@@ -201,10 +202,11 @@ export class IndexingService {
201
202
  if (op.op === 'delete') {
202
203
  await this.deleteRecord(uri)
203
204
  } else {
205
+ const parsed = await getAndParseRecord(blocks, cid)
204
206
  await this.indexRecord(
205
207
  uri,
206
208
  cid,
207
- op.value,
209
+ parsed.record,
208
210
  op.op === 'create' ? WriteOpAction.Create : WriteOpAction.Update,
209
211
  now,
210
212
  )
@@ -389,19 +391,15 @@ type UriAndCid = {
389
391
  cid: CID
390
392
  }
391
393
 
392
- type RecordDescript = UriAndCid & {
393
- value: unknown
394
- }
395
-
396
394
  type IndexOp =
397
395
  | ({
398
396
  op: 'create' | 'update'
399
- } & RecordDescript)
397
+ } & UriAndCid)
400
398
  | ({ op: 'delete' } & UriAndCid)
401
399
 
402
400
  const findDiffFromCheckout = (
403
401
  curr: Record<string, UriAndCid>,
404
- checkout: Record<string, RecordDescript>,
402
+ checkout: Record<string, UriAndCid>,
405
403
  ): IndexOp[] => {
406
404
  const ops: IndexOp[] = []
407
405
  for (const uri of Object.keys(checkout)) {
@@ -428,14 +426,13 @@ const findDiffFromCheckout = (
428
426
  const formatCheckout = (
429
427
  did: string,
430
428
  verifiedRepo: VerifiedRepo,
431
- ): Record<string, RecordDescript> => {
432
- const records: Record<string, RecordDescript> = {}
429
+ ): Record<string, UriAndCid> => {
430
+ const records: Record<string, UriAndCid> = {}
433
431
  for (const create of verifiedRepo.creates) {
434
432
  const uri = AtUri.make(did, create.collection, create.rkey)
435
433
  records[uri.toString()] = {
436
434
  uri,
437
435
  cid: create.cid,
438
- value: create.record,
439
436
  }
440
437
  }
441
438
  return records
@@ -112,6 +112,7 @@ const insertFn = async (
112
112
  obj.reply,
113
113
  )
114
114
  if (invalidReplyRoot || violatesThreadGate) {
115
+ Object.assign(insertedPost, { invalidReplyRoot, violatesThreadGate })
115
116
  await db
116
117
  .updateTable('post')
117
118
  .where('uri', '=', post.uri)
@@ -241,6 +242,13 @@ const notifsForInsert = (obj: IndexedPost) => {
241
242
  }
242
243
  }
243
244
 
245
+ if (obj.post.violatesThreadGate) {
246
+ // don't generate reply notifications when post violates threadgate
247
+ return notifs
248
+ }
249
+
250
+ // reply notifications
251
+
244
252
  for (const ancestor of obj.ancestors ?? []) {
245
253
  if (ancestor.uri === obj.post.uri) continue // no need to notify for own post
246
254
  if (ancestor.height < REPLY_NOTIF_DEPTH) {
@@ -353,6 +361,11 @@ const updateAggregates = async (db: DatabaseSchema, postIdx: IndexedPost) => {
353
361
  replyCount: db
354
362
  .selectFrom('post')
355
363
  .where('post.replyParent', '=', postIdx.post.replyParent)
364
+ .where((qb) =>
365
+ qb
366
+ .where('post.violatesThreadGate', 'is', null)
367
+ .orWhere('post.violatesThreadGate', '=', false),
368
+ )
356
369
  .select(countAll.as('count')),
357
370
  })
358
371
  .onConflict((oc) =>
@@ -3,15 +3,36 @@ import { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'
3
3
  import { Database } from '../../db'
4
4
  import { Label, isSelfLabels } from '../../lexicon/types/com/atproto/label/defs'
5
5
  import { ids } from '../../lexicon/lexicons'
6
- import { LabelCache } from '../../label-cache'
6
+ import { ReadThroughCache } from '../../cache/read-through'
7
+ import { Redis } from '../../redis'
7
8
 
8
9
  export type Labels = Record<string, Label[]>
9
10
 
11
+ export type LabelCacheOpts = {
12
+ redis: Redis
13
+ staleTTL: number
14
+ maxTTL: number
15
+ }
16
+
10
17
  export class LabelService {
11
- constructor(public db: Database, public cache: LabelCache | null) {}
18
+ public cache: ReadThroughCache<Label[]> | null
12
19
 
13
- static creator(cache: LabelCache | null) {
14
- return (db: Database) => new LabelService(db, cache)
20
+ constructor(public db: Database, cacheOpts: LabelCacheOpts | null) {
21
+ if (cacheOpts) {
22
+ this.cache = new ReadThroughCache(cacheOpts.redis, {
23
+ ...cacheOpts,
24
+ fetchMethod: async (subject: string) => {
25
+ const res = await fetchLabelsForSubjects(db, [subject])
26
+ return res[subject] ?? []
27
+ },
28
+ fetchManyMethod: (subjects: string[]) =>
29
+ fetchLabelsForSubjects(db, subjects),
30
+ })
31
+ }
32
+ }
33
+
34
+ static creator(cacheOpts: LabelCacheOpts | null) {
35
+ return (db: Database) => new LabelService(db, cacheOpts)
15
36
  }
16
37
 
17
38
  async formatAndCreate(
@@ -72,24 +93,19 @@ export class LabelService {
72
93
  },
73
94
  ): Promise<Labels> {
74
95
  if (subjects.length < 1) return {}
75
- const res =
76
- this.cache === null || opts?.skipCache
77
- ? await this.db.db
78
- .selectFrom('label')
79
- .where('label.uri', 'in', subjects)
80
- .if(!opts?.includeNeg, (qb) => qb.where('neg', '=', false))
81
- .selectAll()
82
- .execute()
83
- : this.cache.forSubjects(subjects, opts?.includeNeg)
84
- return res.reduce((acc, cur) => {
85
- acc[cur.uri] ??= []
86
- acc[cur.uri].push({
87
- ...cur,
88
- cid: cur.cid === '' ? undefined : cur.cid,
89
- neg: cur.neg,
90
- })
91
- return acc
92
- }, {} as Labels)
96
+ const res = this.cache
97
+ ? await this.cache.getMany(subjects, { revalidate: opts?.skipCache })
98
+ : await fetchLabelsForSubjects(this.db, subjects)
99
+
100
+ if (opts?.includeNeg) {
101
+ return res
102
+ }
103
+
104
+ const noNegs: Labels = {}
105
+ for (const [key, val] of Object.entries(res)) {
106
+ noNegs[key] = val.filter((label) => !label.neg)
107
+ }
108
+ return noNegs
93
109
  }
94
110
 
95
111
  // gets labels for any record. when did is present, combine labels for both did & profile record.
@@ -171,3 +187,31 @@ export function getSelfLabels(details: {
171
187
  return { src, uri, cid, val, cts, neg: false }
172
188
  })
173
189
  }
190
+
191
+ const fetchLabelsForSubjects = async (
192
+ db: Database,
193
+ subjects: string[],
194
+ ): Promise<Record<string, Label[]>> => {
195
+ if (subjects.length === 0) {
196
+ return {}
197
+ }
198
+ const res = await db.db
199
+ .selectFrom('label')
200
+ .where('label.uri', 'in', subjects)
201
+ .selectAll()
202
+ .execute()
203
+ const labelMap = res.reduce((acc, cur) => {
204
+ acc[cur.uri] ??= []
205
+ acc[cur.uri].push({
206
+ ...cur,
207
+ cid: cur.cid === '' ? undefined : cur.cid,
208
+ neg: cur.neg,
209
+ })
210
+ return acc
211
+ }, {} as Record<string, Label[]>)
212
+ // ensure we cache negatives
213
+ for (const subject of subjects) {
214
+ labelMap[subject] ??= []
215
+ }
216
+ return labelMap
217
+ }
@@ -639,7 +639,7 @@ export class ModerationService {
639
639
  const { did, recordPath } = getStatusIdentifierFromSubject(
640
640
  'did' in subject ? subject.did : subject.uri,
641
641
  )
642
- let builder = this.db.db
642
+ const builder = this.db.db
643
643
  .selectFrom('moderation_subject_status')
644
644
  .where('did', '=', did)
645
645
  .where('recordPath', '=', recordPath || '')
@@ -2,10 +2,7 @@
2
2
 
3
3
  import { AtUri } from '@atproto/syntax'
4
4
  import { PrimaryDatabase } from '../../db'
5
- import {
6
- ModerationEvent,
7
- ModerationSubjectStatus,
8
- } from '../../db/tables/moderation'
5
+ import { ModerationSubjectStatus } from '../../db/tables/moderation'
9
6
  import {
10
7
  REVIEWOPEN,
11
8
  REVIEWCLOSED,
@@ -0,0 +1,4 @@
1
+ import { Database, PrimaryDatabase } from '../db'
2
+
3
+ export type FromDb<T> = (db: Database) => T
4
+ export type FromDbPrimary<T> = (db: PrimaryDatabase) => T
@@ -0,0 +1,70 @@
1
+ import { sql } from 'kysely'
2
+ import { countAll } from '../../db/util'
3
+ import { PrimaryDatabase } from '../../db'
4
+
5
+ // i.e. 30 days before the last time the user checked their notifs
6
+ export const BEFORE_LAST_SEEN_DAYS = 30
7
+ // i.e. 180 days before the latest unread notification
8
+ export const BEFORE_LATEST_UNREAD_DAYS = 180
9
+ // don't consider culling unreads until they hit this threshold, and then enforce beforeLatestUnreadThresholdDays
10
+ export const UNREAD_KEPT_COUNT = 500
11
+
12
+ export const tidyNotifications = async (db: PrimaryDatabase, did: string) => {
13
+ const stats = await db.db
14
+ .selectFrom('notification')
15
+ .select([
16
+ sql<0 | 1>`("sortAt" < "lastSeenNotifs")`.as('read'),
17
+ countAll.as('count'),
18
+ sql<string>`min("sortAt")`.as('earliestAt'),
19
+ sql<string>`max("sortAt")`.as('latestAt'),
20
+ sql<string>`max("lastSeenNotifs")`.as('lastSeenAt'),
21
+ ])
22
+ .leftJoin('actor_state', 'actor_state.did', 'notification.did')
23
+ .where('notification.did', '=', did)
24
+ .groupBy(sql`1`) // group by read (i.e. 1st column)
25
+ .execute()
26
+ const readStats = stats.find((stat) => stat.read)
27
+ const unreadStats = stats.find((stat) => !stat.read)
28
+ let readCutoffAt: Date | undefined
29
+ let unreadCutoffAt: Date | undefined
30
+ if (readStats) {
31
+ readCutoffAt = addDays(
32
+ new Date(readStats.lastSeenAt),
33
+ -BEFORE_LAST_SEEN_DAYS,
34
+ )
35
+ }
36
+ if (unreadStats && unreadStats.count > UNREAD_KEPT_COUNT) {
37
+ unreadCutoffAt = addDays(
38
+ new Date(unreadStats.latestAt),
39
+ -BEFORE_LATEST_UNREAD_DAYS,
40
+ )
41
+ }
42
+ // take most recent of read/unread cutoffs
43
+ const cutoffAt = greatest(readCutoffAt, unreadCutoffAt)
44
+ if (cutoffAt) {
45
+ // skip delete if it wont catch any notifications
46
+ const earliestAt = least(readStats?.earliestAt, unreadStats?.earliestAt)
47
+ if (earliestAt && earliestAt < cutoffAt.toISOString()) {
48
+ await db.db
49
+ .deleteFrom('notification')
50
+ .where('did', '=', did)
51
+ .where('sortAt', '<', cutoffAt.toISOString())
52
+ .execute()
53
+ }
54
+ }
55
+ }
56
+
57
+ const addDays = (date: Date, days: number) => {
58
+ date.setDate(date.getDate() + days)
59
+ return date
60
+ }
61
+
62
+ const least = <T extends Ordered>(a: T | undefined, b: T | undefined) => {
63
+ return a !== undefined && (b === undefined || a < b) ? a : b
64
+ }
65
+
66
+ const greatest = <T extends Ordered>(a: T | undefined, b: T | undefined) => {
67
+ return a !== undefined && (b === undefined || a > b) ? a : b
68
+ }
69
+
70
+ type Ordered = string | number | Date
package/src/util/retry.ts CHANGED
@@ -1,26 +1,6 @@
1
1
  import { AxiosError } from 'axios'
2
- import { wait } from '@atproto/common'
3
2
  import { XRPCError, ResponseType } from '@atproto/xrpc'
4
-
5
- export async function retry<T>(
6
- fn: () => Promise<T>,
7
- opts: RetryOptions = {},
8
- ): Promise<T> {
9
- const { max = 3, retryable = () => true } = opts
10
- let retries = 0
11
- let doneError: unknown
12
- while (!doneError) {
13
- try {
14
- if (retries) await backoff(retries)
15
- return await fn()
16
- } catch (err) {
17
- const willRetry = retries < max && retryable(err)
18
- if (!willRetry) doneError = err
19
- retries += 1
20
- }
21
- }
22
- throw doneError
23
- }
3
+ import { RetryOptions, retry } from '@atproto/common'
24
4
 
25
5
  export async function retryHttp<T>(
26
6
  fn: () => Promise<T>,
@@ -44,26 +24,3 @@ export function retryableHttp(err: unknown) {
44
24
  const retryableHttpStatusCodes = new Set([
45
25
  408, 425, 429, 500, 502, 503, 504, 522, 524,
46
26
  ])
47
-
48
- type RetryOptions = {
49
- max?: number
50
- retryable?: (err: unknown) => boolean
51
- }
52
-
53
- // Waits exponential backoff with max and jitter: ~50, ~100, ~200, ~400, ~800, ~1000, ~1000, ...
54
- async function backoff(n: number, multiplier = 50, max = 1000) {
55
- const exponentialMs = Math.pow(2, n) * multiplier
56
- const ms = Math.min(exponentialMs, max)
57
- await wait(jitter(ms))
58
- }
59
-
60
- // Adds randomness +/-15% of value
61
- function jitter(value: number) {
62
- const delta = value * 0.15
63
- return value + randomRange(-delta, delta)
64
- }
65
-
66
- function randomRange(from: number, to: number) {
67
- const rand = Math.random() * (to - from)
68
- return rand + from
69
- }
@@ -97,9 +97,11 @@ describe('admin get repo view', () => {
97
97
  expect(beforeEmailVerification.emailConfirmedAt).toBeUndefined()
98
98
  const timestampBeforeVerification = Date.now()
99
99
  const bobsAccount = sc.accounts[sc.dids.bob]
100
- const verificationToken = await network.pds.ctx.services
101
- .account(network.pds.ctx.db)
102
- .createEmailToken(sc.dids.bob, 'confirm_email')
100
+ const verificationToken =
101
+ await network.pds.ctx.accountManager.createEmailToken(
102
+ sc.dids.bob,
103
+ 'confirm_email',
104
+ )
103
105
  await agent.api.com.atproto.server.confirmEmail(
104
106
  { email: bobsAccount.email, token: verificationToken },
105
107
  {
@@ -387,7 +387,7 @@ describe('moderation', () => {
387
387
  createLabelVals: ['nsfw'],
388
388
  negateLabelVals: [],
389
389
  })
390
- const { data: takedownAction } = await emitModEvent({
390
+ await emitModEvent({
391
391
  $type: 'com.atproto.admin.defs#modEventTakedown',
392
392
  })
393
393
 
@@ -779,7 +779,7 @@ describe('moderation', () => {
779
779
  negateLabelVals: ModEventLabel['negateLabelVals']
780
780
  },
781
781
  ) {
782
- const { createLabelVals, negateLabelVals, ...rest } = opts
782
+ const { createLabelVals, negateLabelVals } = opts
783
783
  const result = await agent.api.com.atproto.admin.emitModerationEvent(
784
784
  {
785
785
  event: {
@@ -17,6 +17,7 @@ describe('admin repo search view', () => {
17
17
  sc = network.getSeedClient()
18
18
  await usersBulkSeed(sc)
19
19
  headers = network.pds.adminAuthHeaders()
20
+ await network.processAll()
20
21
  })
21
22
 
22
23
  afterAll(async () => {
@@ -31,7 +31,6 @@ describe('algo hot-classic', () => {
31
31
  alice = sc.dids.alice
32
32
  bob = sc.dids.bob
33
33
  await network.processAll()
34
- await network.bsky.processAll()
35
34
  })
36
35
 
37
36
  afterAll(async () => {
@@ -59,7 +58,7 @@ describe('algo hot-classic', () => {
59
58
  await sc.like(sc.dids[name], two.ref)
60
59
  await sc.like(sc.dids[name], three.ref)
61
60
  }
62
- await network.bsky.processAll()
61
+ await network.processAll()
63
62
 
64
63
  const res = await agent.api.app.bsky.feed.getFeed(
65
64
  { feed: feedUri },
@@ -36,7 +36,7 @@ describe('auth', () => {
36
36
  { headers: { authorization: `Bearer ${jwt}` } },
37
37
  )
38
38
  }
39
- const origSigningKey = network.pds.ctx.repoSigningKey
39
+ const origSigningKey = await network.pds.ctx.actorStore.keypair(issuer)
40
40
  const newSigningKey = await Secp256k1Keypair.create({ exportable: true })
41
41
  // confirm original signing key works
42
42
  await expect(attemptWithKey(origSigningKey)).resolves.toBeDefined()
@@ -40,26 +40,25 @@ describe('labeler', () => {
40
40
  await usersSeed(sc)
41
41
  await network.processAll()
42
42
  alice = sc.dids.alice
43
- const repoSvc = pdsCtx.services.repo(pdsCtx.db)
44
- const storeBlob = async (bytes: Uint8Array) => {
45
- const blobRef = await repoSvc.blobs.addUntetheredBlob(
46
- alice,
47
- 'image/jpeg',
48
- Readable.from([bytes], { objectMode: false }),
49
- )
50
- const preparedBlobRef = {
51
- cid: blobRef.ref,
52
- mimeType: 'image/jpeg',
53
- constraints: {},
54
- }
55
- await repoSvc.blobs.verifyBlobAndMakePermanent(alice, preparedBlobRef)
56
- await repoSvc.blobs.associateBlob(
57
- preparedBlobRef,
58
- postUri(),
59
- TID.nextStr(),
60
- alice,
61
- )
62
- return blobRef
43
+ const storeBlob = (bytes: Uint8Array) => {
44
+ return pdsCtx.actorStore.transact(alice, async (store) => {
45
+ const blobRef = await store.repo.blob.addUntetheredBlob(
46
+ 'image/jpeg',
47
+ Readable.from([bytes], { objectMode: false }),
48
+ )
49
+ const preparedBlobRef = {
50
+ cid: blobRef.ref,
51
+ mimeType: 'image/jpeg',
52
+ constraints: {},
53
+ }
54
+ await store.repo.blob.verifyBlobAndMakePermanent(preparedBlobRef)
55
+ await store.repo.blob.associateBlob(
56
+ preparedBlobRef,
57
+ postUri(),
58
+ TID.nextStr(),
59
+ )
60
+ return blobRef
61
+ })
63
62
  }
64
63
  const bytes1 = new Uint8Array([1, 2, 3, 4])
65
64
  const bytes2 = new Uint8Array([5, 6, 7, 8])
@@ -106,11 +106,15 @@ describe('takedowner', () => {
106
106
  .executeTakeFirst()
107
107
  expect(record?.takedownId).toBeGreaterThan(0)
108
108
 
109
- const recordPds = await network.pds.ctx.db.db
110
- .selectFrom('record')
111
- .where('uri', '=', post.ref.uriStr)
112
- .select('takedownRef')
113
- .executeTakeFirst()
109
+ const recordPds = await network.pds.ctx.actorStore.read(
110
+ post.ref.uri.hostname,
111
+ (store) =>
112
+ store.db.db
113
+ .selectFrom('record')
114
+ .where('uri', '=', post.ref.uriStr)
115
+ .select('takedownRef')
116
+ .executeTakeFirst(),
117
+ )
114
118
  expect(recordPds?.takedownRef).toEqual(takedownEvent.id.toString())
115
119
 
116
120
  expect(testInvalidator.invalidated.length).toBe(1)
@@ -162,11 +166,13 @@ describe('takedowner', () => {
162
166
  .executeTakeFirst()
163
167
  expect(record?.takedownId).toBeGreaterThan(0)
164
168
 
165
- const recordPds = await network.pds.ctx.db.db
166
- .selectFrom('record')
167
- .where('uri', '=', res.data.uri)
168
- .select('takedownRef')
169
- .executeTakeFirst()
169
+ const recordPds = await network.pds.ctx.actorStore.read(alice, (store) =>
170
+ store.db.db
171
+ .selectFrom('record')
172
+ .where('uri', '=', res.data.uri)
173
+ .select('takedownRef')
174
+ .executeTakeFirst(),
175
+ )
170
176
  expect(recordPds?.takedownRef).toEqual(takedownEvent.id.toString())
171
177
 
172
178
  expect(testInvalidator.invalidated.length).toBe(2)
@@ -77,8 +77,10 @@ describe('blob resolver', () => {
77
77
  })
78
78
 
79
79
  it('fails on blob with bad signature check.', async () => {
80
- await network.pds.ctx.blobstore.delete(fileCid)
81
- await network.pds.ctx.blobstore.putPermanent(fileCid, randomBytes(100))
80
+ await network.pds.ctx.blobstore(fileDid).delete(fileCid)
81
+ await network.pds.ctx
82
+ .blobstore(fileDid)
83
+ .putPermanent(fileCid, randomBytes(100))
82
84
  const tryGetBlob = client.get(`/blob/${fileDid}/${fileCid.toString()}`)
83
85
  await expect(tryGetBlob).rejects.toThrow(
84
86
  'maxContentLength size of -1 exceeded',