@atproto/bsky 0.0.124 → 0.0.126

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 (117) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/api/app/bsky/notification/listNotifications.d.ts +7 -0
  3. package/dist/api/app/bsky/notification/listNotifications.d.ts.map +1 -1
  4. package/dist/api/app/bsky/notification/listNotifications.js +21 -5
  5. package/dist/api/app/bsky/notification/listNotifications.js.map +1 -1
  6. package/dist/config.d.ts +6 -0
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +24 -15
  9. package/dist/config.js.map +1 -1
  10. package/dist/context.d.ts +6 -1
  11. package/dist/context.d.ts.map +1 -1
  12. package/dist/context.js +6 -0
  13. package/dist/context.js.map +1 -1
  14. package/dist/data-plane/client/hosts.d.ts +37 -0
  15. package/dist/data-plane/client/hosts.d.ts.map +1 -0
  16. package/dist/data-plane/client/hosts.js +106 -0
  17. package/dist/data-plane/client/hosts.js.map +1 -0
  18. package/dist/data-plane/client/index.d.ts +13 -0
  19. package/dist/data-plane/client/index.d.ts.map +1 -0
  20. package/dist/data-plane/client/index.js +133 -0
  21. package/dist/data-plane/client/index.js.map +1 -0
  22. package/dist/data-plane/{client.d.ts → client/util.d.ts} +3 -10
  23. package/dist/data-plane/client/util.d.ts.map +1 -0
  24. package/dist/data-plane/client/util.js +85 -0
  25. package/dist/data-plane/client/util.js.map +1 -0
  26. package/dist/data-plane/server/db/pagination.d.ts +69 -9
  27. package/dist/data-plane/server/db/pagination.d.ts.map +1 -1
  28. package/dist/data-plane/server/db/pagination.js +114 -14
  29. package/dist/data-plane/server/db/pagination.js.map +1 -1
  30. package/dist/data-plane/server/routes/notifs.d.ts.map +1 -1
  31. package/dist/data-plane/server/routes/notifs.js +3 -5
  32. package/dist/data-plane/server/routes/notifs.js.map +1 -1
  33. package/dist/data-plane/server/subscription.d.ts.map +1 -1
  34. package/dist/data-plane/server/subscription.js +6 -0
  35. package/dist/data-plane/server/subscription.js.map +1 -1
  36. package/dist/etcd.d.ts +25 -0
  37. package/dist/etcd.d.ts.map +1 -0
  38. package/dist/etcd.js +109 -0
  39. package/dist/etcd.js.map +1 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +14 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/lexicon/index.d.ts +6 -0
  44. package/dist/lexicon/index.d.ts.map +1 -1
  45. package/dist/lexicon/index.js +12 -0
  46. package/dist/lexicon/index.js.map +1 -1
  47. package/dist/lexicon/lexicons.d.ts +304 -156
  48. package/dist/lexicon/lexicons.d.ts.map +1 -1
  49. package/dist/lexicon/lexicons.js +168 -80
  50. package/dist/lexicon/lexicons.js.map +1 -1
  51. package/dist/lexicon/types/app/bsky/embed/video.d.ts +1 -0
  52. package/dist/lexicon/types/app/bsky/embed/video.d.ts.map +1 -1
  53. package/dist/lexicon/types/app/bsky/embed/video.js.map +1 -1
  54. package/dist/lexicon/types/com/atproto/identity/defs.d.ts +17 -0
  55. package/dist/lexicon/types/com/atproto/identity/defs.d.ts.map +1 -0
  56. package/dist/lexicon/types/com/atproto/identity/defs.js +16 -0
  57. package/dist/lexicon/types/com/atproto/identity/defs.js.map +1 -0
  58. package/dist/lexicon/types/com/atproto/identity/refreshIdentity.d.ts +39 -0
  59. package/dist/lexicon/types/com/atproto/identity/refreshIdentity.d.ts.map +1 -0
  60. package/dist/lexicon/types/com/atproto/identity/refreshIdentity.js +7 -0
  61. package/dist/lexicon/types/com/atproto/identity/refreshIdentity.js.map +1 -0
  62. package/dist/lexicon/types/com/atproto/identity/resolveDid.d.ts +40 -0
  63. package/dist/lexicon/types/com/atproto/identity/resolveDid.d.ts.map +1 -0
  64. package/dist/lexicon/types/com/atproto/identity/resolveDid.js +7 -0
  65. package/dist/lexicon/types/com/atproto/identity/resolveDid.js.map +1 -0
  66. package/dist/lexicon/types/com/atproto/identity/resolveHandle.d.ts +1 -0
  67. package/dist/lexicon/types/com/atproto/identity/resolveHandle.d.ts.map +1 -1
  68. package/dist/lexicon/types/com/atproto/identity/resolveIdentity.d.ts +36 -0
  69. package/dist/lexicon/types/com/atproto/identity/resolveIdentity.d.ts.map +1 -0
  70. package/dist/lexicon/types/com/atproto/identity/resolveIdentity.js +7 -0
  71. package/dist/lexicon/types/com/atproto/identity/resolveIdentity.js.map +1 -0
  72. package/dist/lexicon/types/com/atproto/repo/listRecords.d.ts +0 -4
  73. package/dist/lexicon/types/com/atproto/repo/listRecords.d.ts.map +1 -1
  74. package/dist/lexicon/types/com/atproto/repo/listRecords.js.map +1 -1
  75. package/dist/lexicon/types/com/atproto/sync/getRecord.d.ts +0 -2
  76. package/dist/lexicon/types/com/atproto/sync/getRecord.d.ts.map +1 -1
  77. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.d.ts +1 -30
  78. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.d.ts.map +1 -1
  79. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.js +0 -27
  80. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.js.map +1 -1
  81. package/dist/logger.d.ts +1 -0
  82. package/dist/logger.d.ts.map +1 -1
  83. package/dist/logger.js +2 -1
  84. package/dist/logger.js.map +1 -1
  85. package/package.json +16 -15
  86. package/src/api/app/bsky/notification/listNotifications.ts +28 -6
  87. package/src/config.ts +45 -15
  88. package/src/context.ts +12 -1
  89. package/src/data-plane/client/hosts.ts +103 -0
  90. package/src/data-plane/client/index.ts +123 -0
  91. package/src/data-plane/client/util.ts +66 -0
  92. package/src/data-plane/server/db/pagination.ts +158 -35
  93. package/src/data-plane/server/routes/notifs.ts +4 -9
  94. package/src/data-plane/server/subscription.ts +7 -2
  95. package/src/etcd.ts +90 -0
  96. package/src/index.ts +26 -2
  97. package/src/lexicon/index.ts +36 -0
  98. package/src/lexicon/lexicons.ts +183 -83
  99. package/src/lexicon/types/app/bsky/embed/video.ts +1 -0
  100. package/src/lexicon/types/com/atproto/identity/defs.ts +30 -0
  101. package/src/lexicon/types/com/atproto/identity/refreshIdentity.ts +52 -0
  102. package/src/lexicon/types/com/atproto/identity/resolveDid.ts +52 -0
  103. package/src/lexicon/types/com/atproto/identity/resolveHandle.ts +1 -0
  104. package/src/lexicon/types/com/atproto/identity/resolveIdentity.ts +48 -0
  105. package/src/lexicon/types/com/atproto/repo/listRecords.ts +0 -4
  106. package/src/lexicon/types/com/atproto/sync/getRecord.ts +0 -2
  107. package/src/lexicon/types/com/atproto/sync/subscribeRepos.ts +0 -59
  108. package/src/logger.ts +2 -0
  109. package/tests/etcd.test.ts +301 -0
  110. package/tests/views/__snapshots__/notifications.test.ts.snap +3 -3
  111. package/tests/views/notifications.test.ts +190 -10
  112. package/tsconfig.build.tsbuildinfo +1 -1
  113. package/tsconfig.tests.tsbuildinfo +1 -1
  114. package/dist/data-plane/client.d.ts.map +0 -1
  115. package/dist/data-plane/client.js +0 -156
  116. package/dist/data-plane/client.js.map +0 -1
  117. package/src/data-plane/client.ts +0 -154
@@ -0,0 +1,301 @@
1
+ import EventEmitter from 'node:events'
2
+ import { Etcd3, IKeyValue } from 'etcd3'
3
+ import { EtcdHostList } from '../src'
4
+ import { EtcdMap } from '../src/etcd'
5
+
6
+ describe('etcd', () => {
7
+ describe('EtcdMap', () => {
8
+ it('initializes values based on current keys', async () => {
9
+ const etcd = new MockEtcd()
10
+ etcd.watcher.set('service/a', { value: '1' })
11
+ etcd.watcher.set('service/b', { value: '2' })
12
+ etcd.watcher.set('service/c', { value: '3' })
13
+ const map = new EtcdMap(etcd as unknown as Etcd3)
14
+ await map.connect()
15
+ expect(map.get('service/a')).toBe('1')
16
+ expect(map.get('service/b')).toBe('2')
17
+ expect(map.get('service/c')).toBe('3')
18
+ expect([...map.values()]).toEqual(['1', '2', '3'])
19
+ })
20
+
21
+ it('maintains key updates', async () => {
22
+ const etcd = new MockEtcd()
23
+ etcd.watcher.set('service/a', { value: '1' })
24
+ etcd.watcher.set('service/b', { value: '2' })
25
+ etcd.watcher.set('service/c', { value: '3' })
26
+ const map = new EtcdMap(etcd as unknown as Etcd3)
27
+ await map.connect()
28
+ etcd.watcher.set('service/b', { value: '4' })
29
+ expect(map.get('service/a')).toBe('1')
30
+ expect(map.get('service/b')).toBe('4')
31
+ expect(map.get('service/c')).toBe('3')
32
+ expect([...map.values()]).toEqual(['1', '4', '3'])
33
+ })
34
+
35
+ it('maintains key creates', async () => {
36
+ const etcd = new MockEtcd()
37
+ etcd.watcher.set('service/a', { value: '1' })
38
+ const map = new EtcdMap(etcd as unknown as Etcd3)
39
+ await map.connect()
40
+ etcd.watcher.set('service/b', { value: '2' })
41
+ expect(map.get('service/a')).toBe('1')
42
+ expect(map.get('service/b')).toBe('2')
43
+ expect([...map.values()]).toEqual(['1', '2'])
44
+ })
45
+
46
+ it('maintains key deletions', async () => {
47
+ const etcd = new MockEtcd()
48
+ etcd.watcher.set('service/a', { value: '1' })
49
+ etcd.watcher.set('service/b', { value: '2' })
50
+ const map = new EtcdMap(etcd as unknown as Etcd3)
51
+ await map.connect()
52
+ etcd.watcher.del('service/b')
53
+ expect(map.get('service/a')).toBe('1')
54
+ expect(map.get('service/b')).toBe(null)
55
+ expect([...map.values()]).toEqual(['1'])
56
+ })
57
+
58
+ it('notifies of updates', async () => {
59
+ const etcd = new MockEtcd()
60
+ etcd.watcher.set('service/a', { value: '1' })
61
+ etcd.watcher.set('service/b', { value: '2' })
62
+ const map = new EtcdMap(etcd as unknown as Etcd3)
63
+ await map.connect()
64
+ const states: string[][] = [[...map.values()]]
65
+ map.onUpdate((update) => {
66
+ states.push([...update.values()])
67
+ })
68
+ etcd.watcher.set('service/c', { value: '3' })
69
+ etcd.watcher.del('service/b')
70
+ etcd.watcher.set('service/a', { value: '4' })
71
+ expect(states).toEqual([
72
+ ['1', '2'],
73
+ ['1', '2', '3'],
74
+ ['1', '3'],
75
+ ['4', '3'],
76
+ ])
77
+ })
78
+
79
+ it('ignores out-of-order updates', async () => {
80
+ const etcd = new MockEtcd()
81
+ etcd.watcher.set('service/a', { value: '1' })
82
+ const map = new EtcdMap(etcd as unknown as Etcd3)
83
+ await map.connect()
84
+ const states: string[][] = [[...map.values()]]
85
+ map.onUpdate((update) => {
86
+ states.push([...update.values()])
87
+ })
88
+ etcd.watcher.set('service/a', { value: '2' })
89
+ etcd.watcher.set('service/a', { value: '3', overrideRev: 1 }) // old rev
90
+ etcd.watcher.set('service/a', { value: '4' })
91
+ expect(map.get('service/a')).toBe('4')
92
+ expect(states).toEqual([['1'], ['2'], ['4']]) // never witnessed 3
93
+ })
94
+ })
95
+
96
+ describe('EtcdHostList', () => {
97
+ it('initializes values based on current keys', async () => {
98
+ const etcd = new MockEtcd()
99
+ etcd.watcher.set('service/a', { value: 'http://192.168.1.1' })
100
+ etcd.watcher.set('service/b', { value: 'http://192.168.1.2' })
101
+ etcd.watcher.set('service/c', { value: 'http://192.168.1.3' })
102
+ const hostList = new EtcdHostList(etcd as unknown as Etcd3, '')
103
+ await hostList.connect()
104
+ expect([...hostList.get()]).toEqual([
105
+ 'http://192.168.1.1',
106
+ 'http://192.168.1.2',
107
+ 'http://192.168.1.3',
108
+ ])
109
+ })
110
+
111
+ it('maintains key updates', async () => {
112
+ const etcd = new MockEtcd()
113
+ etcd.watcher.set('service/a', { value: 'http://192.168.1.1' })
114
+ etcd.watcher.set('service/b', { value: 'http://192.168.1.2' })
115
+ etcd.watcher.set('service/c', { value: 'http://192.168.1.3' })
116
+ const hostList = new EtcdHostList(etcd as unknown as Etcd3, '')
117
+ await hostList.connect()
118
+ etcd.watcher.set('service/b', { value: 'http://192.168.1.4' })
119
+ expect([...hostList.get()]).toEqual([
120
+ 'http://192.168.1.1',
121
+ 'http://192.168.1.4',
122
+ 'http://192.168.1.3',
123
+ ])
124
+ })
125
+
126
+ it('maintains key creates', async () => {
127
+ const etcd = new MockEtcd()
128
+ etcd.watcher.set('service/a', { value: 'http://192.168.1.1' })
129
+ const hostList = new EtcdHostList(etcd as unknown as Etcd3, '')
130
+ await hostList.connect()
131
+ etcd.watcher.set('service/b', { value: 'http://192.168.1.2' })
132
+ expect([...hostList.get()]).toEqual([
133
+ 'http://192.168.1.1',
134
+ 'http://192.168.1.2',
135
+ ])
136
+ })
137
+
138
+ it('maintains key deletions', async () => {
139
+ const etcd = new MockEtcd()
140
+ etcd.watcher.set('service/a', { value: 'http://192.168.1.1' })
141
+ etcd.watcher.set('service/b', { value: 'http://192.168.1.2' })
142
+ const hostList = new EtcdHostList(etcd as unknown as Etcd3, '')
143
+ await hostList.connect()
144
+ etcd.watcher.del('service/b')
145
+ expect([...hostList.get()]).toEqual(['http://192.168.1.1'])
146
+ })
147
+
148
+ it('notifies of updates', async () => {
149
+ const etcd = new MockEtcd()
150
+ etcd.watcher.set('service/a', { value: 'http://192.168.1.1' })
151
+ etcd.watcher.set('service/b', { value: 'http://192.168.1.2' })
152
+ const hostList = new EtcdHostList(etcd as unknown as Etcd3, '')
153
+ await hostList.connect()
154
+ const states: string[][] = [[...hostList.get()]]
155
+ hostList.onUpdate((updated) => {
156
+ expect([...updated]).toEqual([...hostList.get()])
157
+ states.push([...updated])
158
+ })
159
+ etcd.watcher.set('service/c', { value: 'http://192.168.1.3' })
160
+ etcd.watcher.del('service/b')
161
+ etcd.watcher.set('service/a', { value: 'http://192.168.1.4' })
162
+ expect(states).toEqual([
163
+ ['http://192.168.1.1', 'http://192.168.1.2'],
164
+ ['http://192.168.1.1', 'http://192.168.1.2', 'http://192.168.1.3'],
165
+ ['http://192.168.1.1', 'http://192.168.1.3'],
166
+ ['http://192.168.1.4', 'http://192.168.1.3'],
167
+ ])
168
+ })
169
+
170
+ it('ignores bad host values', async () => {
171
+ const etcd = new MockEtcd()
172
+ etcd.watcher.set('service/a', { value: 'not-a-host' })
173
+ etcd.watcher.set('service/b', { value: 'http://192.168.1.2' })
174
+ const hostList = new EtcdHostList(etcd as unknown as Etcd3, '')
175
+ await hostList.connect()
176
+ expect([...hostList.get()]).toEqual(['http://192.168.1.2'])
177
+ etcd.watcher.set('service/a', { value: 'http://192.168.1.1' })
178
+ etcd.watcher.set('service/c', { value: 'not-a-host' })
179
+ expect([...hostList.get()]).toEqual([
180
+ 'http://192.168.1.1',
181
+ 'http://192.168.1.2',
182
+ ])
183
+ etcd.watcher.set('service/c', { value: 'http://192.168.1.3' })
184
+ expect([...hostList.get()]).toEqual([
185
+ 'http://192.168.1.1',
186
+ 'http://192.168.1.2',
187
+ 'http://192.168.1.3',
188
+ ])
189
+ })
190
+
191
+ it('falls back to static host list when uninitialized or no keys available', async () => {
192
+ const etcd = new MockEtcd()
193
+ const hostList = new EtcdHostList(etcd as unknown as Etcd3, '', [
194
+ 'http://10.0.0.1',
195
+ 'http://10.0.0.2',
196
+ ])
197
+ etcd.watcher.set('service/a', { value: 'http://192.168.1.1' })
198
+ expect([...hostList.get()]).toEqual([
199
+ 'http://10.0.0.1',
200
+ 'http://10.0.0.2',
201
+ ])
202
+ await hostList.connect()
203
+ const states: string[][] = [[...hostList.get()]]
204
+ hostList.onUpdate((updated) => {
205
+ states.push([...updated])
206
+ })
207
+ etcd.watcher.del('service/a')
208
+ etcd.watcher.set('service/b', { value: 'http://192.168.1.2' })
209
+ expect(states).toEqual([
210
+ ['http://192.168.1.1'],
211
+ ['http://10.0.0.1', 'http://10.0.0.2'],
212
+ ['http://192.168.1.2'],
213
+ ])
214
+ })
215
+ })
216
+ })
217
+
218
+ class MockEtcd {
219
+ public watcher = new MockWatcher()
220
+ watch() {
221
+ const watcher = this.watcher
222
+ return {
223
+ prefix() {
224
+ return {
225
+ watcher() {
226
+ return watcher
227
+ },
228
+ }
229
+ },
230
+ }
231
+ }
232
+ getAll() {
233
+ const watcher = this.watcher
234
+ return {
235
+ prefix() {
236
+ return {
237
+ async exec(): Promise<{ kvs: IKeyValue[] }> {
238
+ return { kvs: watcher.getAll() }
239
+ },
240
+ }
241
+ },
242
+ }
243
+ }
244
+ }
245
+
246
+ class MockWatcher extends EventEmitter {
247
+ rev = 1
248
+ kvs: IKeyValue[] = []
249
+ constructor() {
250
+ super()
251
+ process.nextTick(() => this.emit('connected', {}))
252
+ }
253
+ get(key: string): IKeyValue | null {
254
+ const found = this.kvs.find((kv) => kv.key.toString() === key)
255
+ return found ?? null
256
+ }
257
+ getAll(): IKeyValue[] {
258
+ return [...this.kvs]
259
+ }
260
+ set(
261
+ key: string,
262
+ { value, overrideRev }: { value: string; overrideRev?: number },
263
+ ) {
264
+ const found = this.kvs.find((kv) => kv.key.toString() === key)
265
+ const rev = overrideRev ?? ++this.rev
266
+ if (found) {
267
+ found.value = Buffer.from(value)
268
+ found.mod_revision = rev.toString()
269
+ found.version = (parseInt(found.version, 10) + 1).toString()
270
+ this.emit('put', found)
271
+ } else {
272
+ const created = {
273
+ key: Buffer.from(key),
274
+ value: Buffer.from(value),
275
+ create_revision: rev.toString(),
276
+ mod_revision: rev.toString(),
277
+ version: '1',
278
+ lease: '0',
279
+ }
280
+ this.kvs.push(created)
281
+ this.emit('put', created)
282
+ }
283
+ }
284
+ del(key: string) {
285
+ const foundIdx = this.kvs.findIndex((kv) => kv.key.toString() === key)
286
+ if (foundIdx === -1) return
287
+ const [deleted] = this.kvs.splice(foundIdx, 1)
288
+ const rev = ++this.rev
289
+ deleted.value = Buffer.from('')
290
+ deleted.mod_revision = rev.toString()
291
+ deleted.create_revision = '0'
292
+ deleted.version = '0'
293
+ this.emit('delete', deleted)
294
+ }
295
+ on(evt: 'connected', listener: (res: unknown) => void): any
296
+ on(evt: 'put', listener: (kv: IKeyValue) => void): any
297
+ on(evt: 'delete', listener: (kv: IKeyValue) => void): any
298
+ on(evt: string, listener: (...args: any[]) => void) {
299
+ super.on(evt, listener)
300
+ }
301
+ }
@@ -384,7 +384,7 @@ Array [
384
384
 
385
385
  exports[`notification views fetches notifications with default priority 1`] = `
386
386
  Object {
387
- "cursor": "0000000000000__bafycid",
387
+ "cursor": "1970-01-01T00:00:00.000Z",
388
388
  "notifications": Array [
389
389
  Object {
390
390
  "author": Object {
@@ -486,7 +486,7 @@ Object {
486
486
 
487
487
  exports[`notification views fetches notifications with explicit priority 1`] = `
488
488
  Object {
489
- "cursor": "0000000000000__bafycid",
489
+ "cursor": "1970-01-01T00:00:00.000Z",
490
490
  "notifications": Array [
491
491
  Object {
492
492
  "author": Object {
@@ -588,7 +588,7 @@ Object {
588
588
 
589
589
  exports[`notification views fetches notifications with explicit priority 2`] = `
590
590
  Object {
591
- "cursor": "0000000000000__bafycid",
591
+ "cursor": "1970-01-01T00:00:00.000Z",
592
592
  "notifications": Array [
593
593
  Object {
594
594
  "author": Object {
@@ -1,5 +1,6 @@
1
1
  import { AtpAgent } from '@atproto/api'
2
2
  import { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'
3
+ import { delayCursor } from '../../src/api/app/bsky/notification/listNotifications'
3
4
  import { ids } from '../../src/lexicon/lexicons'
4
5
  import { Notification } from '../../src/lexicon/types/app/bsky/notification/listNotifications'
5
6
  import { forSnapshot, paginateAll } from '../_util'
@@ -497,17 +498,196 @@ describe('notification views', () => {
497
498
  expect(results(paginatedAll)).toEqual(results([full.data]))
498
499
  })
499
500
 
500
- it('fails open on clearly bad cursor.', async () => {
501
- const { data: notifs } =
502
- await agent.api.app.bsky.notification.listNotifications(
503
- { cursor: '90210::bafycid' },
504
- {
505
- headers: await network.serviceHeaders(
506
- alice,
507
- ids.AppBskyNotificationListNotifications,
508
- ),
501
+ describe('notifications delay', () => {
502
+ const notificationsDelayMs = 5_000
503
+
504
+ let delayNetwork: TestNetwork
505
+ let delayAgent: AtpAgent
506
+ let delaySc: SeedClient
507
+ let delayAlice: string
508
+
509
+ beforeAll(async () => {
510
+ delayNetwork = await TestNetwork.create({
511
+ bsky: {
512
+ notificationsDelayMs,
509
513
  },
514
+ dbPostgresSchema: 'bsky_views_notifications_delay',
515
+ })
516
+ delayAgent = delayNetwork.bsky.getClient()
517
+ delaySc = delayNetwork.getSeedClient()
518
+ await basicSeed(delaySc)
519
+ await delayNetwork.processAll()
520
+ delayAlice = delaySc.dids.alice
521
+
522
+ // Add to reply chain, post ancestors: alice -> bob -> alice -> carol.
523
+ // Should have added one notification for each of alice and bob.
524
+ await delaySc.reply(
525
+ delaySc.dids.carol,
526
+ delaySc.posts[delayAlice][1].ref,
527
+ delaySc.replies[delayAlice][0].ref,
528
+ 'indeed',
529
+ )
530
+ await delayNetwork.processAll()
531
+
532
+ // @NOTE: Use fake timers after inserting seed data,
533
+ // to avoid inserting all notifications with the same timestamp.
534
+ jest.useFakeTimers({
535
+ doNotFake: [
536
+ 'nextTick',
537
+ 'performance',
538
+ 'setImmediate',
539
+ 'setInterval',
540
+ 'setTimeout',
541
+ ],
542
+ })
543
+ })
544
+
545
+ afterAll(async () => {
546
+ jest.useRealTimers()
547
+ await delayNetwork.close()
548
+ })
549
+
550
+ it('paginates', async () => {
551
+ const firstNotification = await delayNetwork.bsky.db.db
552
+ .selectFrom('notification')
553
+ .selectAll()
554
+ .limit(1)
555
+ .orderBy('sortAt', 'asc')
556
+ .executeTakeFirstOrThrow()
557
+ // Sets the system time to when the first notification happened.
558
+ // At this point we won't have any notifications that already crossed the delay threshold.
559
+ jest.setSystemTime(new Date(firstNotification.sortAt))
560
+
561
+ const results = (results) =>
562
+ sort(results.flatMap((res) => res.notifications))
563
+ const paginator = async (cursor?: string) => {
564
+ const res =
565
+ await delayAgent.api.app.bsky.notification.listNotifications(
566
+ { cursor, limit: 6 },
567
+ {
568
+ headers: await delayNetwork.serviceHeaders(
569
+ delayAlice,
570
+ ids.AppBskyNotificationListNotifications,
571
+ ),
572
+ },
573
+ )
574
+ return res.data
575
+ }
576
+
577
+ const paginatedAllBeforeDelay = await paginateAll(paginator)
578
+ paginatedAllBeforeDelay.forEach((res) =>
579
+ expect(res.notifications.length).toBe(0),
580
+ )
581
+ const fullBeforeDelay =
582
+ await delayAgent.api.app.bsky.notification.listNotifications(
583
+ {},
584
+ {
585
+ headers: await delayNetwork.serviceHeaders(
586
+ delayAlice,
587
+ ids.AppBskyNotificationListNotifications,
588
+ ),
589
+ },
590
+ )
591
+
592
+ expect(fullBeforeDelay.data.notifications.length).toEqual(0)
593
+ expect(results(paginatedAllBeforeDelay)).toEqual(
594
+ results([fullBeforeDelay.data]),
595
+ )
596
+
597
+ const lastNotification = await delayNetwork.bsky.db.db
598
+ .selectFrom('notification')
599
+ .selectAll()
600
+ .limit(1)
601
+ .orderBy('sortAt', 'desc')
602
+ .executeTakeFirstOrThrow()
603
+ // Sets the system time to when the last notification happened and the delay has elapsed.
604
+ // At this point we all notifications already crossed the delay threshold.
605
+ jest.setSystemTime(
606
+ new Date(
607
+ new Date(lastNotification.sortAt).getTime() +
608
+ notificationsDelayMs +
609
+ 1,
610
+ ),
510
611
  )
511
- expect(notifs).toMatchObject({ notifications: [] })
612
+
613
+ const paginatedAllAfterDelay = await paginateAll(paginator)
614
+ paginatedAllAfterDelay.forEach((res) =>
615
+ expect(res.notifications.length).toBeLessThanOrEqual(6),
616
+ )
617
+ const fullAfterDelay =
618
+ await delayAgent.api.app.bsky.notification.listNotifications(
619
+ {},
620
+ {
621
+ headers: await delayNetwork.serviceHeaders(
622
+ delayAlice,
623
+ ids.AppBskyNotificationListNotifications,
624
+ ),
625
+ },
626
+ )
627
+
628
+ expect(fullAfterDelay.data.notifications.length).toEqual(13)
629
+ expect(results(paginatedAllAfterDelay)).toEqual(
630
+ results([fullAfterDelay.data]),
631
+ )
632
+ })
633
+
634
+ describe('cursor delay', () => {
635
+ const delay0s = 0
636
+ const delay5s = 5_000
637
+
638
+ const now = '2021-01-01T01:00:00.000Z'
639
+ const nowMinus2s = '2021-01-01T00:59:58.000Z'
640
+ const nowMinus5s = '2021-01-01T00:59:55.000Z'
641
+ const nowMinus8s = '2021-01-01T00:59:52.000Z'
642
+
643
+ beforeAll(async () => {
644
+ jest.useFakeTimers({ doNotFake: ['performance'] })
645
+ jest.setSystemTime(new Date(now))
646
+ })
647
+
648
+ afterAll(async () => {
649
+ jest.useRealTimers()
650
+ })
651
+
652
+ describe('for undefined cursor', () => {
653
+ it('returns now minus delay', async () => {
654
+ const delayedCursor = delayCursor(undefined, delay5s)
655
+ expect(delayedCursor).toBe(nowMinus5s)
656
+ })
657
+
658
+ it('returns now if delay is 0', async () => {
659
+ const delayedCursor = delayCursor(undefined, delay0s)
660
+ expect(delayedCursor).toBe(now)
661
+ })
662
+ })
663
+
664
+ describe('for defined cursor', () => {
665
+ it('returns original cursor if delay is 0', async () => {
666
+ const originalCursor = nowMinus2s
667
+ const delayedCursor = delayCursor(originalCursor, delay0s)
668
+ expect(delayedCursor).toBe(originalCursor)
669
+ })
670
+
671
+ it('returns "now minus delay" for cursor that is after that', async () => {
672
+ // Cursor is "now - 2s", should become "now - 5s"
673
+ const originalCursor = nowMinus2s
674
+ const cursor = delayCursor(originalCursor, delay5s)
675
+ expect(cursor).toBe(nowMinus5s)
676
+ })
677
+
678
+ it('returns original cursor for cursor that is before "now minus delay"', async () => {
679
+ // Cursor is "now - 8s", should stay like that.
680
+ const originalCursor = nowMinus8s
681
+ const cursor = delayCursor(originalCursor, delay5s)
682
+ expect(cursor).toBe(originalCursor)
683
+ })
684
+
685
+ it('passes through a non-date cursor', async () => {
686
+ const originalCursor = '123_abc'
687
+ const cursor = delayCursor(originalCursor, delay5s)
688
+ expect(cursor).toBe(originalCursor)
689
+ })
690
+ })
691
+ })
512
692
  })
513
693
  })