@atproto/sync 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 (54) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE.txt +7 -0
  3. package/README.md +93 -0
  4. package/dist/events.d.ts +49 -0
  5. package/dist/events.d.ts.map +1 -0
  6. package/dist/events.js +3 -0
  7. package/dist/events.js.map +1 -0
  8. package/dist/firehose/index.d.ts +50 -0
  9. package/dist/firehose/index.d.ts.map +1 -0
  10. package/dist/firehose/index.js +309 -0
  11. package/dist/firehose/index.js.map +1 -0
  12. package/dist/firehose/lexicons.d.ts +118 -0
  13. package/dist/firehose/lexicons.d.ts.map +1 -0
  14. package/dist/firehose/lexicons.js +265 -0
  15. package/dist/firehose/lexicons.js.map +1 -0
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +20 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/runner/consecutive-list.d.ts +27 -0
  21. package/dist/runner/consecutive-list.d.ts.map +1 -0
  22. package/dist/runner/consecutive-list.js +68 -0
  23. package/dist/runner/consecutive-list.js.map +1 -0
  24. package/dist/runner/index.d.ts +4 -0
  25. package/dist/runner/index.d.ts.map +1 -0
  26. package/dist/runner/index.js +20 -0
  27. package/dist/runner/index.js.map +1 -0
  28. package/dist/runner/memory-runner.d.ts +24 -0
  29. package/dist/runner/memory-runner.d.ts.map +1 -0
  30. package/dist/runner/memory-runner.js +92 -0
  31. package/dist/runner/memory-runner.js.map +1 -0
  32. package/dist/runner/types.d.ts +5 -0
  33. package/dist/runner/types.d.ts.map +1 -0
  34. package/dist/runner/types.js +3 -0
  35. package/dist/runner/types.js.map +1 -0
  36. package/dist/util.d.ts +6 -0
  37. package/dist/util.d.ts.map +1 -0
  38. package/dist/util.js +13 -0
  39. package/dist/util.js.map +1 -0
  40. package/jest.config.js +8 -0
  41. package/package.json +37 -0
  42. package/src/events.ts +61 -0
  43. package/src/firehose/index.ts +357 -0
  44. package/src/firehose/lexicons.ts +407 -0
  45. package/src/index.ts +3 -0
  46. package/src/runner/consecutive-list.ts +44 -0
  47. package/src/runner/index.ts +3 -0
  48. package/src/runner/memory-runner.ts +72 -0
  49. package/src/runner/types.ts +8 -0
  50. package/src/util.ts +10 -0
  51. package/tests/firehose.test.ts +180 -0
  52. package/tests/runner.test.ts +122 -0
  53. package/tsconfig.build.json +8 -0
  54. package/tsconfig.json +4 -0
@@ -0,0 +1,180 @@
1
+ import {
2
+ mockResolvers,
3
+ SeedClient,
4
+ TestNetworkNoAppView,
5
+ } from '@atproto/dev-env'
6
+ import { Firehose, FirehoseOptions, MemoryRunner } from '../src'
7
+ import { IdResolver } from '@atproto/identity'
8
+ import { Create, Event } from '../src/events'
9
+ import { createDeferrable, wait } from '@atproto/common'
10
+
11
+ describe('firehose', () => {
12
+ let network: TestNetworkNoAppView
13
+ let sc: SeedClient
14
+ let idResolver: IdResolver
15
+
16
+ beforeAll(async () => {
17
+ network = await TestNetworkNoAppView.create({
18
+ dbPostgresSchema: 'sync_firehose',
19
+ })
20
+ idResolver = new IdResolver({ plcUrl: network.plc.url })
21
+ mockResolvers(idResolver, network.pds)
22
+ sc = network.getSeedClient()
23
+ })
24
+
25
+ afterAll(async () => {
26
+ await network.close()
27
+ })
28
+
29
+ const createAndReadFirehose = async (
30
+ count: number,
31
+ opts: Partial<FirehoseOptions> = {},
32
+ addRandomWait = false,
33
+ ): Promise<Event[]> => {
34
+ const defer = createDeferrable()
35
+ const evts: Event[] = []
36
+ const firehose = new Firehose({
37
+ idResolver,
38
+ service: network.pds.url.replace('http', 'ws'),
39
+ handleEvent: async (evt) => {
40
+ if (addRandomWait) {
41
+ const time = Math.floor(Math.random()) * 20
42
+ await wait(time)
43
+ }
44
+ evts.push(evt)
45
+ if (evts.length >= count) {
46
+ defer.resolve()
47
+ }
48
+ },
49
+ onError: (err) => {
50
+ throw err
51
+ },
52
+ ...opts,
53
+ })
54
+ firehose.start()
55
+ await defer.complete
56
+ await firehose.destroy()
57
+ return evts
58
+ }
59
+
60
+ let alice: string
61
+
62
+ it('reads events from firehose', async () => {
63
+ const evtsPromise = createAndReadFirehose(5)
64
+ await wait(10) // give the websocket just a second to spin up
65
+ const aliceRes = await sc.createAccount('alice', {
66
+ handle: 'alice.test',
67
+ email: 'alice@test.com',
68
+ password: 'alice-pass',
69
+ })
70
+ alice = aliceRes.did
71
+ await sc.post(alice, 'one')
72
+ await sc.post(alice, 'two')
73
+ await sc.post(alice, 'three')
74
+
75
+ const evts = await evtsPromise
76
+ expect(evts.length).toBe(5)
77
+ expect(evts.at(0)).toMatchObject({
78
+ event: 'identity',
79
+ did: alice,
80
+ handle: aliceRes.handle,
81
+ didDocument: {
82
+ id: alice,
83
+ },
84
+ })
85
+ expect(evts.at(1)).toMatchObject({
86
+ event: 'account',
87
+ did: alice,
88
+ active: true,
89
+ status: undefined,
90
+ })
91
+ expect(evts.at(2)).toMatchObject({
92
+ event: 'create',
93
+ did: alice,
94
+ collection: 'app.bsky.feed.post',
95
+ record: {
96
+ text: 'one',
97
+ },
98
+ })
99
+ expect(evts.at(3)).toMatchObject({
100
+ event: 'create',
101
+ did: alice,
102
+ collection: 'app.bsky.feed.post',
103
+ record: {
104
+ text: 'two',
105
+ },
106
+ })
107
+ expect(evts.at(4)).toMatchObject({
108
+ event: 'create',
109
+ did: alice,
110
+ collection: 'app.bsky.feed.post',
111
+ record: {
112
+ text: 'three',
113
+ },
114
+ })
115
+ })
116
+
117
+ it('does not naively pass through invalid handle evts', async () => {
118
+ const evtsPromise = createAndReadFirehose(1)
119
+ await wait(10) // give the websocket just a second to spin up
120
+ await network.pds.ctx.sequencer.sequenceIdentityEvt(
121
+ alice,
122
+ 'bad-handle.test',
123
+ )
124
+ const evts = await evtsPromise
125
+ expect(evts.at(0)).toMatchObject({ handle: 'alice.test' })
126
+ })
127
+
128
+ it('processes events through the sync queue', async () => {
129
+ const currCursor = await network.pds.ctx.sequencer.curr()
130
+ const runner = new MemoryRunner({
131
+ startCursor: currCursor ?? undefined,
132
+ })
133
+ const evtsPromise = createAndReadFirehose(20, { runner }, true)
134
+ const createAndPost = async (name: string) => {
135
+ const user = await sc.createAccount('name', {
136
+ handle: `${name}.test`,
137
+ email: `${name}@example.com`,
138
+ password: `${name}-pass`,
139
+ })
140
+ const did = user.did
141
+ const post1 = await sc.post(did, 'one')
142
+ const post2 = await sc.post(did, 'two')
143
+ const post3 = await sc.post(did, 'three')
144
+ return {
145
+ did,
146
+ post1: post1.ref.uriStr,
147
+ post2: post2.ref.uriStr,
148
+ post3: post3.ref.uriStr,
149
+ }
150
+ }
151
+ const res = await Promise.all([
152
+ createAndPost('user1'),
153
+ createAndPost('user2'),
154
+ createAndPost('user3'),
155
+ createAndPost('user4'),
156
+ ])
157
+ const evts = await evtsPromise
158
+ const user1Evts = evts.filter((e) => e.did === res[0].did)
159
+ const user2Evts = evts.filter((e) => e.did === res[1].did)
160
+ const user3Evts = evts.filter((e) => e.did === res[2].did)
161
+ const user4Evts = evts.filter((e) => e.did === res[3].did)
162
+ const EVT_ORDER = ['identity', 'account', 'create', 'create', 'create']
163
+ expect(user1Evts.map((e) => e.event)).toEqual(EVT_ORDER)
164
+ expect(user2Evts.map((e) => e.event)).toEqual(EVT_ORDER)
165
+ expect(user3Evts.map((e) => e.event)).toEqual(EVT_ORDER)
166
+ expect(user4Evts.map((e) => e.event)).toEqual(EVT_ORDER)
167
+ expect(
168
+ user1Evts.slice(2, 5).map((e) => (e as Create).uri.toString()),
169
+ ).toEqual([res[0].post1, res[0].post2, res[0].post3])
170
+ expect(
171
+ user2Evts.slice(2, 5).map((e) => (e as Create).uri.toString()),
172
+ ).toEqual([res[1].post1, res[1].post2, res[1].post3])
173
+ expect(
174
+ user3Evts.slice(2, 5).map((e) => (e as Create).uri.toString()),
175
+ ).toEqual([res[2].post1, res[2].post2, res[2].post3])
176
+ expect(
177
+ user4Evts.slice(2, 5).map((e) => (e as Create).uri.toString()),
178
+ ).toEqual([res[3].post1, res[3].post2, res[3].post3])
179
+ })
180
+ })
@@ -0,0 +1,122 @@
1
+ import { wait } from '@atproto/common'
2
+ import { ConsecutiveList, MemoryRunner } from '../src/runner'
3
+
4
+ describe('EventRunner utils', () => {
5
+ describe('ConsecutiveList', () => {
6
+ it('tracks consecutive complete items.', () => {
7
+ const consecutive = new ConsecutiveList<number>()
8
+ // add items
9
+ const item1 = consecutive.push(1)
10
+ const item2 = consecutive.push(2)
11
+ const item3 = consecutive.push(3)
12
+ expect(item1.isComplete).toEqual(false)
13
+ expect(item2.isComplete).toEqual(false)
14
+ expect(item3.isComplete).toEqual(false)
15
+ // complete items out of order
16
+ expect(consecutive.list.length).toBe(3)
17
+ expect(item2.complete()).toEqual([])
18
+ expect(item2.isComplete).toEqual(true)
19
+ expect(consecutive.list.length).toBe(3)
20
+ expect(item1.complete()).toEqual([1, 2])
21
+ expect(item1.isComplete).toEqual(true)
22
+ expect(consecutive.list.length).toBe(1)
23
+ expect(item3.complete()).toEqual([3])
24
+ expect(consecutive.list.length).toBe(0)
25
+ expect(item3.isComplete).toEqual(true)
26
+ })
27
+ })
28
+
29
+ describe('MemoryRunner', () => {
30
+ it('performs work in parallel across partitions, serial within a partition.', async () => {
31
+ const runner = new MemoryRunner({ concurrency: Infinity })
32
+ const complete: number[] = []
33
+ // partition 1 items start slow but get faster: slow should still complete first.
34
+ runner.addTask('1', async () => {
35
+ await wait(30)
36
+ complete.push(11)
37
+ })
38
+ runner.addTask('1', async () => {
39
+ await wait(20)
40
+ complete.push(12)
41
+ })
42
+ runner.addTask('1', async () => {
43
+ await wait(1)
44
+ complete.push(13)
45
+ })
46
+ expect(runner.partitions.size).toEqual(1)
47
+ // partition 2 items complete quickly except the last, which is slowest of all events.
48
+ runner.addTask('2', async () => {
49
+ await wait(1)
50
+ complete.push(21)
51
+ })
52
+ runner.addTask('2', async () => {
53
+ await wait(1)
54
+ complete.push(22)
55
+ })
56
+ runner.addTask('2', async () => {
57
+ await wait(1)
58
+ complete.push(23)
59
+ })
60
+ runner.addTask('2', async () => {
61
+ await wait(60)
62
+ complete.push(24)
63
+ })
64
+ expect(runner.partitions.size).toEqual(2)
65
+ await runner.mainQueue.onIdle()
66
+ expect(complete).toEqual([21, 22, 23, 11, 12, 13, 24])
67
+ expect(runner.partitions.size).toEqual(0)
68
+ })
69
+
70
+ it('limits overall concurrency.', async () => {
71
+ const runner = new MemoryRunner({ concurrency: 1 })
72
+ const complete: number[] = []
73
+ // if concurrency were not constrained, partition 1 would complete all items
74
+ // before any items from partition 2. since it is constrained, the work is complete in the order added.
75
+ runner.addTask('1', async () => {
76
+ await wait(1)
77
+ complete.push(11)
78
+ })
79
+ runner.addTask('2', async () => {
80
+ await wait(10)
81
+ complete.push(21)
82
+ })
83
+ runner.addTask('1', async () => {
84
+ await wait(1)
85
+ complete.push(12)
86
+ })
87
+ runner.addTask('2', async () => {
88
+ await wait(10)
89
+ complete.push(22)
90
+ })
91
+ // only partition 1 exists so far due to the concurrency
92
+ expect(runner.partitions.size).toEqual(1)
93
+ await runner.mainQueue.onIdle()
94
+ expect(complete).toEqual([11, 21, 12, 22])
95
+ expect(runner.partitions.size).toEqual(0)
96
+ })
97
+
98
+ it('settles with many items.', async () => {
99
+ const runner = new MemoryRunner({ concurrency: 100 })
100
+ const complete: { partition: string; id: number }[] = []
101
+ const partitions = new Set<string>()
102
+ for (let i = 0; i < 500; ++i) {
103
+ const partition = Math.floor(Math.random() * 16).toString(10)
104
+ partitions.add(partition)
105
+ runner.addTask(partition, async () => {
106
+ await wait((i % 2) * 2)
107
+ complete.push({ partition, id: i })
108
+ })
109
+ }
110
+ expect(runner.partitions.size).toEqual(partitions.size)
111
+ await runner.mainQueue.onIdle()
112
+ expect(complete.length).toEqual(500)
113
+ for (const partition of partitions) {
114
+ const ids = complete
115
+ .filter((item) => item.partition === partition)
116
+ .map((item) => item.id)
117
+ expect(ids).toEqual([...ids].sort((a, b) => a - b))
118
+ }
119
+ expect(runner.partitions.size).toEqual(0)
120
+ })
121
+ })
122
+ })
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig/node.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./dist"
6
+ },
7
+ "include": ["./src"]
8
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "include": [],
3
+ "references": [{ "path": "./tsconfig.build.json" }]
4
+ }