@atproto/ozone 0.0.12 → 0.0.14
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 +17 -0
- package/dist/api/label/queryLabels.d.ts +3 -0
- package/dist/api/label/subscribeLabels.d.ts +3 -0
- package/dist/config/config.d.ts +3 -0
- package/dist/config/env.d.ts +3 -0
- package/dist/context.d.ts +3 -0
- package/dist/db/index.js +3 -1
- package/dist/db/index.js.map +2 -2
- package/dist/db/schema/label.d.ts +4 -0
- package/dist/index.js +593 -244
- package/dist/index.js.map +3 -3
- package/dist/logger.d.ts +1 -0
- package/dist/mod-service/util.d.ts +3 -0
- package/dist/sequencer/index.d.ts +2 -0
- package/dist/sequencer/outbox.d.ts +16 -0
- package/dist/sequencer/sequencer.d.ts +33 -0
- package/package.json +10 -10
- package/src/api/admin/emitModerationEvent.ts +16 -10
- package/src/api/index.ts +4 -0
- package/src/api/label/queryLabels.ts +58 -0
- package/src/api/label/subscribeLabels.ts +25 -0
- package/src/api/temp/fetchLabels.ts +2 -4
- package/src/config/config.ts +6 -0
- package/src/config/env.ts +6 -0
- package/src/context.ts +12 -0
- package/src/db/migrations/20231219T205730722Z-init.ts +7 -1
- package/src/db/schema/label.ts +7 -0
- package/src/index.ts +2 -0
- package/src/lexicon/lexicons.ts +1 -1
- package/src/logger.ts +2 -0
- package/src/mod-service/index.ts +73 -72
- package/src/mod-service/status.ts +3 -0
- package/src/mod-service/util.ts +17 -0
- package/src/mod-service/views.ts +2 -5
- package/src/sequencer/index.ts +2 -0
- package/src/sequencer/outbox.ts +122 -0
- package/src/sequencer/sequencer.ts +143 -0
- package/tests/__snapshots__/moderation-events.test.ts.snap +53 -75
- package/tests/__snapshots__/moderation.test.ts.snap +4 -4
- package/tests/moderation-appeals.test.ts +19 -7
- package/tests/moderation-events.test.ts +7 -7
- package/tests/moderation-statuses.test.ts +2 -2
- package/tests/moderation.test.ts +14 -13
- package/tests/query-labels.test.ts +163 -0
- package/tests/repo-search.test.ts +0 -1
- package/tests/sequencer.test.ts +222 -0
- package/tests/server.test.ts +2 -0
package/tests/moderation.test.ts
CHANGED
|
@@ -607,7 +607,7 @@ describe('moderation', () => {
|
|
|
607
607
|
},
|
|
608
608
|
{
|
|
609
609
|
encoding: 'application/json',
|
|
610
|
-
headers: network.
|
|
610
|
+
headers: network.ozone.adminAuthHeaders('triage'),
|
|
611
611
|
},
|
|
612
612
|
)
|
|
613
613
|
await expect(attemptLabel).rejects.toThrow(
|
|
@@ -748,7 +748,7 @@ describe('moderation', () => {
|
|
|
748
748
|
},
|
|
749
749
|
{
|
|
750
750
|
encoding: 'application/json',
|
|
751
|
-
headers: network.
|
|
751
|
+
headers: network.ozone.adminAuthHeaders('moderator'),
|
|
752
752
|
},
|
|
753
753
|
)
|
|
754
754
|
// cleanup
|
|
@@ -775,11 +775,11 @@ describe('moderation', () => {
|
|
|
775
775
|
},
|
|
776
776
|
{
|
|
777
777
|
encoding: 'application/json',
|
|
778
|
-
headers: network.
|
|
778
|
+
headers: network.ozone.adminAuthHeaders('triage'),
|
|
779
779
|
},
|
|
780
780
|
)
|
|
781
781
|
await expect(attemptTakedownTriage).rejects.toThrow(
|
|
782
|
-
'Must be a full moderator to
|
|
782
|
+
'Must be a full moderator to take this type of action',
|
|
783
783
|
)
|
|
784
784
|
})
|
|
785
785
|
|
|
@@ -800,7 +800,7 @@ describe('moderation', () => {
|
|
|
800
800
|
const { data: statusesAfterTakedown } =
|
|
801
801
|
await agent.api.com.atproto.admin.queryModerationStatuses(
|
|
802
802
|
{ subject: sc.dids.bob },
|
|
803
|
-
{ headers: network.
|
|
803
|
+
{ headers: network.ozone.adminAuthHeaders('moderator') },
|
|
804
804
|
)
|
|
805
805
|
|
|
806
806
|
expect(statusesAfterTakedown.subjectStatuses[0]).toMatchObject({
|
|
@@ -818,11 +818,11 @@ describe('moderation', () => {
|
|
|
818
818
|
const [{ data: eventList }, { data: statuses }] = await Promise.all([
|
|
819
819
|
agent.api.com.atproto.admin.queryModerationEvents(
|
|
820
820
|
{ subject: sc.dids.bob },
|
|
821
|
-
{ headers: network.
|
|
821
|
+
{ headers: network.ozone.adminAuthHeaders('moderator') },
|
|
822
822
|
),
|
|
823
823
|
agent.api.com.atproto.admin.queryModerationStatuses(
|
|
824
824
|
{ subject: sc.dids.bob },
|
|
825
|
-
{ headers: network.
|
|
825
|
+
{ headers: network.ozone.adminAuthHeaders('moderator') },
|
|
826
826
|
),
|
|
827
827
|
])
|
|
828
828
|
|
|
@@ -879,7 +879,7 @@ describe('moderation', () => {
|
|
|
879
879
|
},
|
|
880
880
|
{
|
|
881
881
|
encoding: 'application/json',
|
|
882
|
-
headers: network.
|
|
882
|
+
headers: network.ozone.adminAuthHeaders(),
|
|
883
883
|
},
|
|
884
884
|
)
|
|
885
885
|
return result.data
|
|
@@ -901,7 +901,7 @@ describe('moderation', () => {
|
|
|
901
901
|
},
|
|
902
902
|
{
|
|
903
903
|
encoding: 'application/json',
|
|
904
|
-
headers: network.
|
|
904
|
+
headers: network.ozone.adminAuthHeaders(),
|
|
905
905
|
},
|
|
906
906
|
)
|
|
907
907
|
}
|
|
@@ -909,7 +909,7 @@ describe('moderation', () => {
|
|
|
909
909
|
async function getRecordLabels(uri: string) {
|
|
910
910
|
const result = await agent.api.com.atproto.admin.getRecord(
|
|
911
911
|
{ uri },
|
|
912
|
-
{ headers: network.
|
|
912
|
+
{ headers: network.ozone.adminAuthHeaders() },
|
|
913
913
|
)
|
|
914
914
|
const labels = result.data.labels ?? []
|
|
915
915
|
return labels.map((l) => l.val)
|
|
@@ -918,7 +918,7 @@ describe('moderation', () => {
|
|
|
918
918
|
async function getRepoLabels(did: string) {
|
|
919
919
|
const result = await agent.api.com.atproto.admin.getRepo(
|
|
920
920
|
{ did },
|
|
921
|
-
{ headers: network.
|
|
921
|
+
{ headers: network.ozone.adminAuthHeaders() },
|
|
922
922
|
)
|
|
923
923
|
const labels = result.data.labels ?? []
|
|
924
924
|
return labels.map((l) => l.val)
|
|
@@ -933,7 +933,7 @@ describe('moderation', () => {
|
|
|
933
933
|
const { ctx } = network.bsky
|
|
934
934
|
post = sc.posts[sc.dids.carol][0]
|
|
935
935
|
blob = post.images[1]
|
|
936
|
-
imageUri = ctx.imgUriBuilder
|
|
936
|
+
imageUri = ctx.views.imgUriBuilder
|
|
937
937
|
.getPresetUri(
|
|
938
938
|
'feed_thumbnail',
|
|
939
939
|
sc.dids.carol,
|
|
@@ -974,7 +974,8 @@ describe('moderation', () => {
|
|
|
974
974
|
})
|
|
975
975
|
})
|
|
976
976
|
|
|
977
|
-
|
|
977
|
+
// @TODO add back in with image invalidation, see bluesky-social/atproto#2087
|
|
978
|
+
it.skip('prevents image blob from being served, even when cached.', async () => {
|
|
978
979
|
const fetchImage = await fetch(imageUri)
|
|
979
980
|
expect(fetchImage.status).toEqual(404)
|
|
980
981
|
expect(await fetchImage.json()).toEqual({ message: 'Image not found' })
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import AtpAgent from '@atproto/api'
|
|
2
|
+
import { TestNetwork } from '@atproto/dev-env'
|
|
3
|
+
import { DisconnectError, Subscription } from '@atproto/xrpc-server'
|
|
4
|
+
import { ids, lexicons } from '../src/lexicon/lexicons'
|
|
5
|
+
import { Label } from '../src/lexicon/types/com/atproto/label/defs'
|
|
6
|
+
import {
|
|
7
|
+
OutputSchema as LabelMessage,
|
|
8
|
+
isLabels,
|
|
9
|
+
} from '../src/lexicon/types/com/atproto/label/subscribeLabels'
|
|
10
|
+
|
|
11
|
+
describe('ozone query labels', () => {
|
|
12
|
+
let network: TestNetwork
|
|
13
|
+
let agent: AtpAgent
|
|
14
|
+
|
|
15
|
+
let labels: Label[]
|
|
16
|
+
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
network = await TestNetwork.create({
|
|
19
|
+
dbPostgresSchema: 'ozone_query_labels',
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
agent = network.ozone.getClient()
|
|
23
|
+
|
|
24
|
+
labels = [
|
|
25
|
+
{
|
|
26
|
+
src: 'did:example:labeler',
|
|
27
|
+
uri: 'did:example:blah',
|
|
28
|
+
val: 'spam',
|
|
29
|
+
neg: false,
|
|
30
|
+
cts: new Date().toISOString(),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
src: 'did:example:labeler',
|
|
34
|
+
uri: 'did:example:blah',
|
|
35
|
+
val: 'impersonation',
|
|
36
|
+
neg: false,
|
|
37
|
+
cts: new Date().toISOString(),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
src: 'did:example:labeler',
|
|
41
|
+
uri: 'at://did:example:blah/app.bsky.feed.post/1234abcde',
|
|
42
|
+
val: 'spam',
|
|
43
|
+
neg: false,
|
|
44
|
+
cts: new Date().toISOString(),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
src: 'did:example:labeler',
|
|
48
|
+
uri: 'at://did:example:blah/app.bsky.feed.post/1234abcfg',
|
|
49
|
+
val: 'spam',
|
|
50
|
+
neg: false,
|
|
51
|
+
cts: new Date().toISOString(),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
src: 'did:example:labeler',
|
|
55
|
+
uri: 'at://did:example:blah/app.bsky.actor.profile/self',
|
|
56
|
+
val: 'spam',
|
|
57
|
+
neg: false,
|
|
58
|
+
cts: new Date().toISOString(),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
src: 'did:example:labeler',
|
|
62
|
+
uri: 'did:example:thing',
|
|
63
|
+
val: 'spam',
|
|
64
|
+
neg: false,
|
|
65
|
+
cts: new Date().toISOString(),
|
|
66
|
+
},
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
const modService = network.ozone.ctx.modService(network.ozone.ctx.db)
|
|
70
|
+
await modService.createLabels(labels)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
afterAll(async () => {
|
|
74
|
+
await network.close()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('returns all labels', async () => {
|
|
78
|
+
const res = await agent.api.com.atproto.label.queryLabels({
|
|
79
|
+
uriPatterns: ['*'],
|
|
80
|
+
})
|
|
81
|
+
expect(res.data.labels).toEqual(labels)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('returns all labels even when an additional pattern is supplied', async () => {
|
|
85
|
+
const res = await agent.api.com.atproto.label.queryLabels({
|
|
86
|
+
uriPatterns: ['*', 'did:example:blah'],
|
|
87
|
+
})
|
|
88
|
+
expect(res.data.labels).toEqual(labels)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('returns all labels that match an exact uri pattern', async () => {
|
|
92
|
+
const res = await agent.api.com.atproto.label.queryLabels({
|
|
93
|
+
uriPatterns: ['did:example:blah'],
|
|
94
|
+
})
|
|
95
|
+
expect(res.data.labels).toEqual(labels.slice(0, 2))
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('returns all labels that match one of multiple exact uris', async () => {
|
|
99
|
+
const res = await agent.api.com.atproto.label.queryLabels({
|
|
100
|
+
uriPatterns: [
|
|
101
|
+
'at://did:example:blah/app.bsky.feed.post/1234abcfg',
|
|
102
|
+
'at://did:example:blah/app.bsky.actor.profile/self',
|
|
103
|
+
],
|
|
104
|
+
})
|
|
105
|
+
expect(res.data.labels).toEqual(labels.slice(3, 5))
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('returns all labels that match one of multiple uris, exact & glob', async () => {
|
|
109
|
+
const res = await agent.api.com.atproto.label.queryLabels({
|
|
110
|
+
uriPatterns: ['at://did:example:blah/app.bsky*', 'did:example:blah'],
|
|
111
|
+
})
|
|
112
|
+
expect(res.data.labels).toEqual(labels.slice(0, 5))
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('paginates', async () => {
|
|
116
|
+
const res1 = await agent.api.com.atproto.label.queryLabels({
|
|
117
|
+
uriPatterns: ['at://did:example:blah/app.bsky*', 'did:example:blah'],
|
|
118
|
+
limit: 3,
|
|
119
|
+
})
|
|
120
|
+
const res2 = await agent.api.com.atproto.label.queryLabels({
|
|
121
|
+
uriPatterns: ['at://did:example:blah/app.bsky*', 'did:example:blah'],
|
|
122
|
+
limit: 3,
|
|
123
|
+
cursor: res1.data.cursor,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
expect([...res1.data.labels, ...res2.data.labels]).toEqual(
|
|
127
|
+
labels.slice(0, 5),
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('subscribeLabels', () => {
|
|
132
|
+
it('streams all labels from initial cursor.', async () => {
|
|
133
|
+
const ac = new AbortController()
|
|
134
|
+
let doneTimer: NodeJS.Timeout
|
|
135
|
+
const resetDoneTimer = () => {
|
|
136
|
+
clearTimeout(doneTimer)
|
|
137
|
+
doneTimer = setTimeout(() => ac.abort(new DisconnectError()), 100)
|
|
138
|
+
}
|
|
139
|
+
const sub = new Subscription({
|
|
140
|
+
signal: ac.signal,
|
|
141
|
+
service: agent.service.origin.replace('http://', 'ws://'),
|
|
142
|
+
method: ids.ComAtprotoLabelSubscribeLabels,
|
|
143
|
+
getParams() {
|
|
144
|
+
return { cursor: 0 }
|
|
145
|
+
},
|
|
146
|
+
validate(obj) {
|
|
147
|
+
return lexicons.assertValidXrpcMessage<LabelMessage>(
|
|
148
|
+
ids.ComAtprotoLabelSubscribeLabels,
|
|
149
|
+
obj,
|
|
150
|
+
)
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
const streamedLabels: Label[] = []
|
|
154
|
+
for await (const message of sub) {
|
|
155
|
+
resetDoneTimer()
|
|
156
|
+
if (isLabels(message)) {
|
|
157
|
+
streamedLabels.push(...message.labels)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
expect(streamedLabels).toEqual(labels)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
})
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { TestNetwork } from '@atproto/dev-env'
|
|
2
|
+
import { readFromGenerator, wait } from '@atproto/common'
|
|
3
|
+
import { LabelsEvt, Sequencer } from '../src/sequencer'
|
|
4
|
+
import Outbox from '../src/sequencer/outbox'
|
|
5
|
+
import { randomStr } from '@atproto/crypto'
|
|
6
|
+
import { Label } from '../src/lexicon/types/com/atproto/label/defs'
|
|
7
|
+
|
|
8
|
+
describe('sequencer', () => {
|
|
9
|
+
let network: TestNetwork
|
|
10
|
+
let sequencer: Sequencer
|
|
11
|
+
|
|
12
|
+
let totalEvts = 0
|
|
13
|
+
let lastSeen: number
|
|
14
|
+
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
network = await TestNetwork.create({
|
|
17
|
+
dbPostgresSchema: 'ozone_sequencer',
|
|
18
|
+
})
|
|
19
|
+
sequencer = network.ozone.ctx.sequencer
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterAll(async () => {
|
|
23
|
+
await network.close()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const loadFromDb = (lastSeen: number) => {
|
|
27
|
+
return sequencer.db.db
|
|
28
|
+
.selectFrom('label')
|
|
29
|
+
.selectAll()
|
|
30
|
+
.where('id', '>', lastSeen)
|
|
31
|
+
.orderBy('id', 'asc')
|
|
32
|
+
.execute()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const evtToDbRow = (e: LabelsEvt) => {
|
|
36
|
+
const label = e.labels[0]
|
|
37
|
+
return {
|
|
38
|
+
id: e.seq,
|
|
39
|
+
...label,
|
|
40
|
+
cid: label.cid ? label.cid : '',
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const caughtUp = (outbox: Outbox): (() => Promise<boolean>) => {
|
|
45
|
+
return async () => {
|
|
46
|
+
const lastEvt = await outbox.sequencer.curr()
|
|
47
|
+
if (lastEvt === null) return true
|
|
48
|
+
return outbox.lastSeen >= (lastEvt ?? 0)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const createLabels = async (count: number): Promise<Label[]> => {
|
|
53
|
+
const labels: Label[] = []
|
|
54
|
+
for (let i = 0; i < count; i++) {
|
|
55
|
+
const did = `did:example:${randomStr(10, 'base32')}`
|
|
56
|
+
const label = {
|
|
57
|
+
src: 'did:example:labeler',
|
|
58
|
+
uri: did,
|
|
59
|
+
val: 'spam',
|
|
60
|
+
neg: false,
|
|
61
|
+
cts: new Date().toISOString(),
|
|
62
|
+
}
|
|
63
|
+
await network.ozone.ctx.db.transaction((dbTxn) =>
|
|
64
|
+
network.ozone.ctx.modService(dbTxn).createLabels([label]),
|
|
65
|
+
)
|
|
66
|
+
labels.push(label)
|
|
67
|
+
}
|
|
68
|
+
return labels
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
it('sends to outbox', async () => {
|
|
72
|
+
const count = 20
|
|
73
|
+
totalEvts += count
|
|
74
|
+
await createLabels(count)
|
|
75
|
+
const outbox = new Outbox(sequencer)
|
|
76
|
+
const evts = await readFromGenerator(outbox.events(-1), caughtUp(outbox))
|
|
77
|
+
expect(evts.length).toBe(totalEvts)
|
|
78
|
+
|
|
79
|
+
const fromDb = await loadFromDb(-1)
|
|
80
|
+
expect(evts.map(evtToDbRow)).toEqual(fromDb)
|
|
81
|
+
|
|
82
|
+
lastSeen = evts.at(-1)?.seq ?? lastSeen
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('sequences negative labels', async () => {
|
|
86
|
+
const count = 5
|
|
87
|
+
totalEvts += count
|
|
88
|
+
const created = await createLabels(count)
|
|
89
|
+
const toNegate = created
|
|
90
|
+
.slice(0, 2)
|
|
91
|
+
.map((l) => ({ ...l, neg: true, cts: new Date().toISOString() }))
|
|
92
|
+
await network.ozone.ctx
|
|
93
|
+
.modService(network.ozone.ctx.db)
|
|
94
|
+
.createLabels(toNegate)
|
|
95
|
+
|
|
96
|
+
const outbox = new Outbox(sequencer)
|
|
97
|
+
const evts = await readFromGenerator(
|
|
98
|
+
outbox.events(lastSeen),
|
|
99
|
+
caughtUp(outbox),
|
|
100
|
+
)
|
|
101
|
+
expect(evts.length).toBe(count)
|
|
102
|
+
|
|
103
|
+
const fromDb = await loadFromDb(lastSeen)
|
|
104
|
+
expect(evts.map(evtToDbRow)).toEqual(fromDb)
|
|
105
|
+
expect(evts[3].labels[0].uri).toEqual(toNegate[0].uri)
|
|
106
|
+
expect(evts[3].labels[0].neg).toBe(true)
|
|
107
|
+
expect(evts[4].labels[0].uri).toEqual(toNegate[1].uri)
|
|
108
|
+
expect(evts[4].labels[0].neg).toBe(true)
|
|
109
|
+
|
|
110
|
+
lastSeen = evts.at(-1)?.seq ?? lastSeen
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('handles cut over', async () => {
|
|
114
|
+
const count = 20
|
|
115
|
+
totalEvts += count
|
|
116
|
+
const outbox = new Outbox(sequencer)
|
|
117
|
+
const createPromise = createLabels(count)
|
|
118
|
+
const [evts] = await Promise.all([
|
|
119
|
+
readFromGenerator(outbox.events(-1), caughtUp(outbox), createPromise),
|
|
120
|
+
createPromise,
|
|
121
|
+
])
|
|
122
|
+
expect(evts.length).toBe(totalEvts)
|
|
123
|
+
|
|
124
|
+
const fromDb = await loadFromDb(-1)
|
|
125
|
+
expect(evts.map(evtToDbRow)).toEqual(fromDb)
|
|
126
|
+
|
|
127
|
+
lastSeen = evts.at(-1)?.seq ?? lastSeen
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('only gets events after cursor', async () => {
|
|
131
|
+
const count = 20
|
|
132
|
+
totalEvts += count
|
|
133
|
+
const outbox = new Outbox(sequencer)
|
|
134
|
+
const createPromise = createLabels(count)
|
|
135
|
+
const [evts] = await Promise.all([
|
|
136
|
+
readFromGenerator(
|
|
137
|
+
outbox.events(lastSeen),
|
|
138
|
+
caughtUp(outbox),
|
|
139
|
+
createPromise,
|
|
140
|
+
),
|
|
141
|
+
createPromise,
|
|
142
|
+
])
|
|
143
|
+
|
|
144
|
+
// +1 because we send the lastSeen date as well
|
|
145
|
+
expect(evts.length).toBe(count)
|
|
146
|
+
|
|
147
|
+
const fromDb = await loadFromDb(lastSeen)
|
|
148
|
+
expect(evts.map(evtToDbRow)).toEqual(fromDb)
|
|
149
|
+
|
|
150
|
+
lastSeen = evts.at(-1)?.seq ?? lastSeen
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('buffers events that are not being read', async () => {
|
|
154
|
+
const count = 20
|
|
155
|
+
totalEvts += count
|
|
156
|
+
const outbox = new Outbox(sequencer)
|
|
157
|
+
const createPromise = createLabels(count)
|
|
158
|
+
const gen = outbox.events(lastSeen)
|
|
159
|
+
// read enough to start streaming then wait so that the rest go into the buffer,
|
|
160
|
+
// then stream out from buffer
|
|
161
|
+
const [firstPart] = await Promise.all([
|
|
162
|
+
readFromGenerator(gen, caughtUp(outbox), createPromise, 5),
|
|
163
|
+
createPromise,
|
|
164
|
+
])
|
|
165
|
+
const secondPart = await readFromGenerator(
|
|
166
|
+
gen,
|
|
167
|
+
caughtUp(outbox),
|
|
168
|
+
createPromise,
|
|
169
|
+
)
|
|
170
|
+
const evts = [...firstPart, ...secondPart]
|
|
171
|
+
expect(evts.length).toBe(count)
|
|
172
|
+
|
|
173
|
+
const fromDb = await loadFromDb(lastSeen)
|
|
174
|
+
expect(evts.map(evtToDbRow)).toEqual(fromDb)
|
|
175
|
+
|
|
176
|
+
lastSeen = evts.at(-1)?.seq ?? lastSeen
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('errors when buffer is overloaded', async () => {
|
|
180
|
+
const count = 20
|
|
181
|
+
totalEvts += count
|
|
182
|
+
const outbox = new Outbox(sequencer, { maxBufferSize: 5 })
|
|
183
|
+
const gen = outbox.events(lastSeen)
|
|
184
|
+
const createPromise = createLabels(count)
|
|
185
|
+
// read enough to start streaming then wait to stream rest until buffer is overloaded
|
|
186
|
+
const overloadBuffer = async () => {
|
|
187
|
+
await Promise.all([
|
|
188
|
+
readFromGenerator(gen, caughtUp(outbox), createPromise, 5),
|
|
189
|
+
createPromise,
|
|
190
|
+
])
|
|
191
|
+
await wait(500)
|
|
192
|
+
await readFromGenerator(gen, caughtUp(outbox), createPromise)
|
|
193
|
+
}
|
|
194
|
+
await expect(overloadBuffer).rejects.toThrow('Stream consumer too slow')
|
|
195
|
+
|
|
196
|
+
await createPromise
|
|
197
|
+
|
|
198
|
+
const fromDb = await loadFromDb(lastSeen)
|
|
199
|
+
lastSeen = fromDb.at(-1)?.id ?? lastSeen
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('handles many open connections', async () => {
|
|
203
|
+
const count = 20
|
|
204
|
+
const outboxes: Outbox[] = []
|
|
205
|
+
for (let i = 0; i < 50; i++) {
|
|
206
|
+
outboxes.push(new Outbox(sequencer))
|
|
207
|
+
}
|
|
208
|
+
const createPromise = createLabels(count)
|
|
209
|
+
const readOutboxes = Promise.all(
|
|
210
|
+
outboxes.map((o) =>
|
|
211
|
+
readFromGenerator(o.events(lastSeen), caughtUp(o), createPromise),
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
const [results] = await Promise.all([readOutboxes, createPromise])
|
|
215
|
+
const fromDb = await loadFromDb(lastSeen)
|
|
216
|
+
for (const result of results) {
|
|
217
|
+
expect(result.length).toBe(count)
|
|
218
|
+
expect(result.map(evtToDbRow)).toEqual(fromDb)
|
|
219
|
+
}
|
|
220
|
+
lastSeen = results[0].at(-1)?.seq ?? lastSeen
|
|
221
|
+
})
|
|
222
|
+
})
|
package/tests/server.test.ts
CHANGED
|
@@ -55,6 +55,8 @@ describe('server', () => {
|
|
|
55
55
|
})
|
|
56
56
|
|
|
57
57
|
it('healthcheck fails when database is unavailable.', async () => {
|
|
58
|
+
// destory sequencer to release connection that would prevent the db from closing
|
|
59
|
+
await ozone.ctx.sequencer.destroy()
|
|
58
60
|
await ozone.ctx.db.close()
|
|
59
61
|
let error: AxiosError
|
|
60
62
|
try {
|