@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.
- package/CHANGELOG.md +12 -0
- package/LICENSE.txt +7 -0
- package/README.md +93 -0
- package/dist/events.d.ts +49 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +3 -0
- package/dist/events.js.map +1 -0
- package/dist/firehose/index.d.ts +50 -0
- package/dist/firehose/index.d.ts.map +1 -0
- package/dist/firehose/index.js +309 -0
- package/dist/firehose/index.js.map +1 -0
- package/dist/firehose/lexicons.d.ts +118 -0
- package/dist/firehose/lexicons.d.ts.map +1 -0
- package/dist/firehose/lexicons.js +265 -0
- package/dist/firehose/lexicons.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/runner/consecutive-list.d.ts +27 -0
- package/dist/runner/consecutive-list.d.ts.map +1 -0
- package/dist/runner/consecutive-list.js +68 -0
- package/dist/runner/consecutive-list.js.map +1 -0
- package/dist/runner/index.d.ts +4 -0
- package/dist/runner/index.d.ts.map +1 -0
- package/dist/runner/index.js +20 -0
- package/dist/runner/index.js.map +1 -0
- package/dist/runner/memory-runner.d.ts +24 -0
- package/dist/runner/memory-runner.d.ts.map +1 -0
- package/dist/runner/memory-runner.js +92 -0
- package/dist/runner/memory-runner.js.map +1 -0
- package/dist/runner/types.d.ts +5 -0
- package/dist/runner/types.d.ts.map +1 -0
- package/dist/runner/types.js +3 -0
- package/dist/runner/types.js.map +1 -0
- package/dist/util.d.ts +6 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +13 -0
- package/dist/util.js.map +1 -0
- package/jest.config.js +8 -0
- package/package.json +37 -0
- package/src/events.ts +61 -0
- package/src/firehose/index.ts +357 -0
- package/src/firehose/lexicons.ts +407 -0
- package/src/index.ts +3 -0
- package/src/runner/consecutive-list.ts +44 -0
- package/src/runner/index.ts +3 -0
- package/src/runner/memory-runner.ts +72 -0
- package/src/runner/types.ts +8 -0
- package/src/util.ts +10 -0
- package/tests/firehose.test.ts +180 -0
- package/tests/runner.test.ts +122 -0
- package/tsconfig.build.json +8 -0
- 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
|
+
})
|
package/tsconfig.json
ADDED