@atproto/ozone 0.0.11 → 0.0.13
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 +875 -454
- package/dist/index.js.map +3 -3
- package/dist/lexicon/index.d.ts +2 -0
- package/dist/lexicon/lexicons.d.ts +27 -0
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts +1 -1
- package/dist/lexicon/types/com/atproto/admin/updateAccountPassword.d.ts +26 -0
- 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/index.ts +12 -0
- package/src/lexicon/lexicons.ts +32 -1
- package/src/lexicon/types/app/bsky/actor/defs.ts +2 -0
- package/src/lexicon/types/com/atproto/admin/updateAccountPassword.ts +39 -0
- 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/src/mod-service/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ import { BlobPushEvent } from '../db/schema/blob_push_event'
|
|
|
43
43
|
import { BackgroundQueue } from '../background'
|
|
44
44
|
import { EventPusher } from '../daemon'
|
|
45
45
|
import { jsonb } from '../db/types'
|
|
46
|
+
import { LabelChannel } from '../db/schema/label'
|
|
46
47
|
|
|
47
48
|
export type ModerationServiceCreator = (db: Database) => ModerationService
|
|
48
49
|
|
|
@@ -438,24 +439,22 @@ export class ModerationService {
|
|
|
438
439
|
takedownRef,
|
|
439
440
|
}))
|
|
440
441
|
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}),
|
|
458
|
-
])
|
|
442
|
+
const repoEvts = await this.db.db
|
|
443
|
+
.insertInto('repo_push_event')
|
|
444
|
+
.values(values)
|
|
445
|
+
.onConflict((oc) =>
|
|
446
|
+
oc.columns(['subjectDid', 'eventType']).doUpdateSet({
|
|
447
|
+
takedownRef,
|
|
448
|
+
confirmedAt: null,
|
|
449
|
+
attempts: 0,
|
|
450
|
+
lastAttempted: null,
|
|
451
|
+
}),
|
|
452
|
+
)
|
|
453
|
+
.returning('id')
|
|
454
|
+
.execute()
|
|
455
|
+
await this.formatAndCreateLabels(subject.did, null, {
|
|
456
|
+
create: [UNSPECCED_TAKEDOWN_LABEL],
|
|
457
|
+
})
|
|
459
458
|
|
|
460
459
|
this.db.onCommit(() => {
|
|
461
460
|
this.backgroundQueue.add(async () => {
|
|
@@ -467,23 +466,21 @@ export class ModerationService {
|
|
|
467
466
|
}
|
|
468
467
|
|
|
469
468
|
async reverseTakedownRepo(subject: RepoSubject) {
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}),
|
|
486
|
-
])
|
|
469
|
+
const repoEvts = await this.db.db
|
|
470
|
+
.updateTable('repo_push_event')
|
|
471
|
+
.where('eventType', 'in', TAKEDOWNS)
|
|
472
|
+
.where('subjectDid', '=', subject.did)
|
|
473
|
+
.set({
|
|
474
|
+
takedownRef: null,
|
|
475
|
+
confirmedAt: null,
|
|
476
|
+
attempts: 0,
|
|
477
|
+
lastAttempted: null,
|
|
478
|
+
})
|
|
479
|
+
.returning('id')
|
|
480
|
+
.execute()
|
|
481
|
+
await this.formatAndCreateLabels(subject.did, null, {
|
|
482
|
+
negate: [UNSPECCED_TAKEDOWN_LABEL],
|
|
483
|
+
})
|
|
487
484
|
|
|
488
485
|
this.db.onCommit(() => {
|
|
489
486
|
this.backgroundQueue.add(async () => {
|
|
@@ -509,22 +506,22 @@ export class ModerationService {
|
|
|
509
506
|
if (blobCids && blobCids.length > 0) {
|
|
510
507
|
labels.push(UNSPECCED_TAKEDOWN_BLOBS_LABEL)
|
|
511
508
|
}
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
.
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
509
|
+
const recordEvts = await this.db.db
|
|
510
|
+
.insertInto('record_push_event')
|
|
511
|
+
.values(values)
|
|
512
|
+
.onConflict((oc) =>
|
|
513
|
+
oc.columns(['subjectUri', 'eventType']).doUpdateSet({
|
|
514
|
+
takedownRef,
|
|
515
|
+
confirmedAt: null,
|
|
516
|
+
attempts: 0,
|
|
517
|
+
lastAttempted: null,
|
|
518
|
+
}),
|
|
519
|
+
)
|
|
520
|
+
.returning('id')
|
|
521
|
+
.execute()
|
|
522
|
+
await this.formatAndCreateLabels(subject.uri, subject.cid, {
|
|
523
|
+
create: labels,
|
|
524
|
+
})
|
|
528
525
|
|
|
529
526
|
this.db.onCommit(() => {
|
|
530
527
|
this.backgroundQueue.add(async () => {
|
|
@@ -579,29 +576,31 @@ export class ModerationService {
|
|
|
579
576
|
if (blobCids && blobCids.length > 0) {
|
|
580
577
|
labels.push(UNSPECCED_TAKEDOWN_BLOBS_LABEL)
|
|
581
578
|
}
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
579
|
+
const recordEvts = await this.db.db
|
|
580
|
+
.updateTable('record_push_event')
|
|
581
|
+
.where('eventType', 'in', TAKEDOWNS)
|
|
582
|
+
.where('subjectDid', '=', subject.did)
|
|
583
|
+
.where('subjectUri', '=', subject.uri)
|
|
584
|
+
.set({
|
|
585
|
+
takedownRef: null,
|
|
586
|
+
confirmedAt: null,
|
|
587
|
+
attempts: 0,
|
|
588
|
+
lastAttempted: null,
|
|
589
|
+
})
|
|
590
|
+
.returning('id')
|
|
591
|
+
.execute()
|
|
592
|
+
await this.formatAndCreateLabels(subject.uri, subject.cid, {
|
|
593
|
+
negate: labels,
|
|
594
|
+
}),
|
|
595
|
+
this.db.onCommit(() => {
|
|
596
|
+
this.backgroundQueue.add(async () => {
|
|
597
|
+
await Promise.all(
|
|
598
|
+
recordEvts.map((evt) =>
|
|
599
|
+
this.eventPusher.attemptRecordEvent(evt.id),
|
|
600
|
+
),
|
|
601
|
+
)
|
|
593
602
|
})
|
|
594
|
-
.returning('id')
|
|
595
|
-
.execute(),
|
|
596
|
-
this.formatAndCreateLabels(subject.uri, subject.cid, { negate: labels }),
|
|
597
|
-
])
|
|
598
|
-
this.db.onCommit(() => {
|
|
599
|
-
this.backgroundQueue.add(async () => {
|
|
600
|
-
await Promise.all(
|
|
601
|
-
recordEvts.map((evt) => this.eventPusher.attemptRecordEvent(evt.id)),
|
|
602
|
-
)
|
|
603
603
|
})
|
|
604
|
-
})
|
|
605
604
|
|
|
606
605
|
if (blobCids && blobCids.length > 0) {
|
|
607
606
|
const blobEvts = await this.db.db
|
|
@@ -858,12 +857,14 @@ export class ModerationService {
|
|
|
858
857
|
neg: !!l.neg,
|
|
859
858
|
}))
|
|
860
859
|
const { ref } = this.db.db.dynamic
|
|
860
|
+
await sql`notify ${ref(LabelChannel)}`.execute(this.db.db)
|
|
861
861
|
const excluded = (col: string) => ref(`excluded.${col}`)
|
|
862
862
|
await this.db.db
|
|
863
863
|
.insertInto('label')
|
|
864
864
|
.values(dbVals)
|
|
865
865
|
.onConflict((oc) =>
|
|
866
866
|
oc.columns(['src', 'uri', 'cid', 'val']).doUpdateSet({
|
|
867
|
+
id: sql`${excluded('id')}`,
|
|
867
868
|
neg: sql`${excluded('neg')}`,
|
|
868
869
|
cts: sql`${excluded('cts')}`,
|
|
869
870
|
}),
|
|
@@ -179,6 +179,9 @@ export const adjustModerationSubjectStatus = async (
|
|
|
179
179
|
subjectStatus.appealed = true
|
|
180
180
|
newStatus.lastAppealedAt = createdAt
|
|
181
181
|
subjectStatus.lastAppealedAt = createdAt
|
|
182
|
+
// Set reviewState to escalated when appeal events are emitted
|
|
183
|
+
subjectStatus.reviewState = REVIEWESCALATED
|
|
184
|
+
newStatus.reviewState = REVIEWESCALATED
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
if (
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { LabelRow } from '../db/schema/label'
|
|
2
|
+
import { Label } from '../lexicon/types/com/atproto/label/defs'
|
|
3
|
+
|
|
4
|
+
export const formatLabel = (row: LabelRow): Label => {
|
|
5
|
+
const label: Label = {
|
|
6
|
+
src: row.src,
|
|
7
|
+
uri: row.uri,
|
|
8
|
+
val: row.val,
|
|
9
|
+
neg: row.neg,
|
|
10
|
+
cts: row.cts,
|
|
11
|
+
}
|
|
12
|
+
if (row.cid !== '') {
|
|
13
|
+
// @NOTE avoiding undefined values on label, which dag-cbor chokes on when serializing.
|
|
14
|
+
label.cid = row.cid
|
|
15
|
+
}
|
|
16
|
+
return label
|
|
17
|
+
}
|
package/src/mod-service/views.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from './types'
|
|
25
25
|
import { REASONOTHER } from '../lexicon/types/com/atproto/moderation/defs'
|
|
26
26
|
import { subjectFromEventRow, subjectFromStatusRow } from './subject'
|
|
27
|
+
import { formatLabel } from './util'
|
|
27
28
|
|
|
28
29
|
export type AppviewAuth = () => Promise<
|
|
29
30
|
| {
|
|
@@ -403,11 +404,7 @@ export class ModerationViews {
|
|
|
403
404
|
.if(!includeNeg, (qb) => qb.where('neg', '=', false))
|
|
404
405
|
.selectAll()
|
|
405
406
|
.execute()
|
|
406
|
-
return res.map((l) => (
|
|
407
|
-
...l,
|
|
408
|
-
cid: l.cid === '' ? undefined : l.cid,
|
|
409
|
-
neg: l.neg,
|
|
410
|
-
}))
|
|
407
|
+
return res.map((l) => formatLabel(l))
|
|
411
408
|
}
|
|
412
409
|
|
|
413
410
|
async getSubjectStatus(
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { AsyncBuffer, AsyncBufferFullError } from '@atproto/common'
|
|
2
|
+
import { InvalidRequestError } from '@atproto/xrpc-server'
|
|
3
|
+
import { Sequencer, LabelsEvt } from './sequencer'
|
|
4
|
+
|
|
5
|
+
export type OutboxOpts = {
|
|
6
|
+
maxBufferSize: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class Outbox {
|
|
10
|
+
private caughtUp = false
|
|
11
|
+
lastSeen = -1
|
|
12
|
+
|
|
13
|
+
cutoverBuffer: LabelsEvt[]
|
|
14
|
+
outBuffer: AsyncBuffer<LabelsEvt>
|
|
15
|
+
|
|
16
|
+
constructor(public sequencer: Sequencer, opts: Partial<OutboxOpts> = {}) {
|
|
17
|
+
const { maxBufferSize = 500 } = opts
|
|
18
|
+
this.cutoverBuffer = []
|
|
19
|
+
this.outBuffer = new AsyncBuffer<LabelsEvt>(maxBufferSize)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// event stream occurs in 3 phases
|
|
23
|
+
// 1. backfill events: events that have been added to the DB since the last time a connection was open.
|
|
24
|
+
// The outbox is not yet listening for new events from the sequencer
|
|
25
|
+
// 2. cutover: the outbox has caught up with where the sequencer purports to be,
|
|
26
|
+
// but the sequencer might already be halfway through sending out a round of updates.
|
|
27
|
+
// Therefore, we start accepting the sequencer's events in a buffer, while making our own request to the
|
|
28
|
+
// database to ensure we're caught up. We then dedupe the query & the buffer & stream the events in order
|
|
29
|
+
// 3. streaming: we're all caught up on historic state, so the sequencer outputs events and we
|
|
30
|
+
// immediately yield them
|
|
31
|
+
async *events(
|
|
32
|
+
backfillCursor?: number,
|
|
33
|
+
signal?: AbortSignal,
|
|
34
|
+
): AsyncGenerator<LabelsEvt> {
|
|
35
|
+
// catch up as much as we can
|
|
36
|
+
if (backfillCursor !== undefined) {
|
|
37
|
+
for await (const evt of this.getBackfill(backfillCursor)) {
|
|
38
|
+
if (signal?.aborted) return
|
|
39
|
+
this.lastSeen = evt.seq
|
|
40
|
+
yield evt
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
// if not backfill, we don't need to cutover, just start streaming
|
|
44
|
+
this.caughtUp = true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// streams updates from sequencer, but buffers them for cutover as it makes a last request
|
|
48
|
+
|
|
49
|
+
const addToBuffer = (evts) => {
|
|
50
|
+
if (this.caughtUp) {
|
|
51
|
+
this.outBuffer.pushMany(evts)
|
|
52
|
+
} else {
|
|
53
|
+
this.cutoverBuffer = [...this.cutoverBuffer, ...evts]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!signal?.aborted) {
|
|
58
|
+
this.sequencer.on('events', addToBuffer)
|
|
59
|
+
}
|
|
60
|
+
signal?.addEventListener('abort', () =>
|
|
61
|
+
this.sequencer.off('events', addToBuffer),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const cutover = async () => {
|
|
65
|
+
// only need to perform cutover if we've been backfilling
|
|
66
|
+
if (backfillCursor !== undefined) {
|
|
67
|
+
const cutoverEvts = await this.sequencer.requestLabelRange({
|
|
68
|
+
earliestId: this.lastSeen > -1 ? this.lastSeen : backfillCursor,
|
|
69
|
+
})
|
|
70
|
+
this.outBuffer.pushMany(cutoverEvts)
|
|
71
|
+
// dont worry about dupes, we ensure order on yield
|
|
72
|
+
this.outBuffer.pushMany(this.cutoverBuffer)
|
|
73
|
+
this.caughtUp = true
|
|
74
|
+
this.cutoverBuffer = []
|
|
75
|
+
} else {
|
|
76
|
+
this.caughtUp = true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
cutover()
|
|
80
|
+
|
|
81
|
+
while (true) {
|
|
82
|
+
try {
|
|
83
|
+
for await (const evt of this.outBuffer.events()) {
|
|
84
|
+
if (signal?.aborted) return
|
|
85
|
+
if (evt.seq > this.lastSeen) {
|
|
86
|
+
this.lastSeen = evt.seq
|
|
87
|
+
yield evt
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err instanceof AsyncBufferFullError) {
|
|
92
|
+
throw new InvalidRequestError(
|
|
93
|
+
'Stream consumer too slow',
|
|
94
|
+
'ConsumerTooSlow',
|
|
95
|
+
)
|
|
96
|
+
} else {
|
|
97
|
+
throw err
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// yields only historical events
|
|
104
|
+
async *getBackfill(backfillCursor: number) {
|
|
105
|
+
const PAGE_SIZE = 500
|
|
106
|
+
while (true) {
|
|
107
|
+
const evts = await this.sequencer.requestLabelRange({
|
|
108
|
+
earliestId: this.lastSeen > -1 ? this.lastSeen : backfillCursor,
|
|
109
|
+
limit: PAGE_SIZE,
|
|
110
|
+
})
|
|
111
|
+
for (const evt of evts) {
|
|
112
|
+
yield evt
|
|
113
|
+
}
|
|
114
|
+
// if we're within half a pagesize of the sequencer, we call it good & switch to cutover
|
|
115
|
+
const seqCursor = this.sequencer.lastSeen ?? -1
|
|
116
|
+
if (seqCursor - this.lastSeen < PAGE_SIZE / 2) break
|
|
117
|
+
if (evts.length < 1) break
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default Outbox
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import EventEmitter from 'events'
|
|
2
|
+
import TypedEmitter from 'typed-emitter'
|
|
3
|
+
import { seqLogger as log } from '../logger'
|
|
4
|
+
import Database from '../db'
|
|
5
|
+
import { Labels as LabelsEvt } from '../lexicon/types/com/atproto/label/subscribeLabels'
|
|
6
|
+
import { LabelChannel, Label as LabelTable } from '../db/schema/label'
|
|
7
|
+
import { Selectable } from 'kysely'
|
|
8
|
+
import { formatLabel } from '../mod-service/util'
|
|
9
|
+
import { PoolClient } from 'pg'
|
|
10
|
+
|
|
11
|
+
export type { Labels as LabelsEvt } from '../lexicon/types/com/atproto/label/subscribeLabels'
|
|
12
|
+
type LabelRow = Selectable<LabelTable>
|
|
13
|
+
|
|
14
|
+
export class Sequencer extends (EventEmitter as new () => SequencerEmitter) {
|
|
15
|
+
destroyed = false
|
|
16
|
+
pollPromise: Promise<void> | undefined
|
|
17
|
+
queued = false
|
|
18
|
+
conn: PoolClient | undefined
|
|
19
|
+
|
|
20
|
+
constructor(public db: Database, public lastSeen = 0) {
|
|
21
|
+
super()
|
|
22
|
+
// note: this does not err when surpassed, just prints a warning to stderr
|
|
23
|
+
this.setMaxListeners(100)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async start() {
|
|
27
|
+
const curr = await this.curr()
|
|
28
|
+
this.lastSeen = curr ?? 0
|
|
29
|
+
this.poll()
|
|
30
|
+
this.conn = await this.db.pool.connect()
|
|
31
|
+
this.conn.query(`listen ${LabelChannel}`) // if this errors, unhandled rejection should cause process to exit
|
|
32
|
+
this.conn.on('notification', (notif) => {
|
|
33
|
+
if (notif.channel === LabelChannel) {
|
|
34
|
+
this.poll()
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async destroy() {
|
|
40
|
+
if (this.destroyed) return
|
|
41
|
+
this.destroyed = true
|
|
42
|
+
if (this.conn) {
|
|
43
|
+
this.conn.release()
|
|
44
|
+
this.conn = undefined
|
|
45
|
+
}
|
|
46
|
+
if (this.pollPromise) {
|
|
47
|
+
await this.pollPromise
|
|
48
|
+
}
|
|
49
|
+
this.emit('close')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async curr(): Promise<number | null> {
|
|
53
|
+
const got = await this.db.db
|
|
54
|
+
.selectFrom('label')
|
|
55
|
+
.selectAll()
|
|
56
|
+
.orderBy('id', 'desc')
|
|
57
|
+
.limit(1)
|
|
58
|
+
.executeTakeFirst()
|
|
59
|
+
return got?.id ?? null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async next(cursor: number): Promise<LabelRow | null> {
|
|
63
|
+
const got = await this.db.db
|
|
64
|
+
.selectFrom('label')
|
|
65
|
+
.selectAll()
|
|
66
|
+
.where('id', '>', cursor)
|
|
67
|
+
.limit(1)
|
|
68
|
+
.orderBy('id', 'asc')
|
|
69
|
+
.executeTakeFirst()
|
|
70
|
+
return got || null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async requestLabelRange(opts: {
|
|
74
|
+
earliestId?: number
|
|
75
|
+
limit?: number
|
|
76
|
+
}): Promise<LabelsEvt[]> {
|
|
77
|
+
const { earliestId, limit } = opts
|
|
78
|
+
|
|
79
|
+
let seqQb = this.db.db.selectFrom('label').selectAll().orderBy('id', 'asc')
|
|
80
|
+
if (earliestId !== undefined) {
|
|
81
|
+
seqQb = seqQb.where('id', '>', earliestId)
|
|
82
|
+
}
|
|
83
|
+
if (limit !== undefined) {
|
|
84
|
+
seqQb = seqQb.limit(limit)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const rows = await seqQb.execute()
|
|
88
|
+
if (rows.length < 1) {
|
|
89
|
+
return []
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const evts: LabelsEvt[] = []
|
|
93
|
+
for (const row of rows) {
|
|
94
|
+
evts.push({
|
|
95
|
+
seq: row.id,
|
|
96
|
+
labels: [formatLabel(row)],
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return evts
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private poll() {
|
|
104
|
+
if (this.destroyed) return
|
|
105
|
+
if (this.pollPromise) {
|
|
106
|
+
this.queued = true
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
this.queued = false
|
|
110
|
+
this.pollPromise = this.requestLabelRange({
|
|
111
|
+
earliestId: this.lastSeen,
|
|
112
|
+
limit: 500,
|
|
113
|
+
})
|
|
114
|
+
.then((evts) => {
|
|
115
|
+
this.emit('events', evts)
|
|
116
|
+
this.lastSeen = evts.at(-1)?.seq ?? this.lastSeen
|
|
117
|
+
if (evts.length > 0) {
|
|
118
|
+
this.queued = true
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
.catch((err) => {
|
|
122
|
+
log.error(
|
|
123
|
+
{ err, lastSeen: this.lastSeen },
|
|
124
|
+
'sequencer failed to poll db',
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
.finally(() => {
|
|
128
|
+
this.pollPromise = undefined
|
|
129
|
+
if (this.queued) {
|
|
130
|
+
this.poll()
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type SequencerEvents = {
|
|
137
|
+
events: (evts: LabelsEvt[]) => void
|
|
138
|
+
close: () => void
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type SequencerEmitter = TypedEmitter<SequencerEvents>
|
|
142
|
+
|
|
143
|
+
export default Sequencer
|