@atproto/pds 0.4.117 → 0.4.118

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 (137) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/account-manager/account-manager.d.ts +5 -2
  3. package/dist/account-manager/account-manager.d.ts.map +1 -1
  4. package/dist/account-manager/account-manager.js.map +1 -1
  5. package/dist/actor-store/blob/transactor.d.ts +2 -1
  6. package/dist/actor-store/blob/transactor.d.ts.map +1 -1
  7. package/dist/actor-store/blob/transactor.js +19 -5
  8. package/dist/actor-store/blob/transactor.js.map +1 -1
  9. package/dist/api/com/atproto/repo/uploadBlob.d.ts.map +1 -1
  10. package/dist/api/com/atproto/repo/uploadBlob.js +1 -0
  11. package/dist/api/com/atproto/repo/uploadBlob.js.map +1 -1
  12. package/dist/config/env.d.ts +1 -0
  13. package/dist/config/env.d.ts.map +1 -1
  14. package/dist/config/env.js +1 -0
  15. package/dist/config/env.js.map +1 -1
  16. package/dist/config/secrets.d.ts +1 -0
  17. package/dist/config/secrets.d.ts.map +1 -1
  18. package/dist/config/secrets.js +1 -0
  19. package/dist/config/secrets.js.map +1 -1
  20. package/dist/context.d.ts +3 -0
  21. package/dist/context.d.ts.map +1 -1
  22. package/dist/context.js +19 -0
  23. package/dist/context.js.map +1 -1
  24. package/dist/lexicon/index.d.ts +10 -0
  25. package/dist/lexicon/index.d.ts.map +1 -1
  26. package/dist/lexicon/index.js +20 -0
  27. package/dist/lexicon/index.js.map +1 -1
  28. package/dist/lexicon/lexicons.d.ts +516 -0
  29. package/dist/lexicon/lexicons.d.ts.map +1 -1
  30. package/dist/lexicon/lexicons.js +277 -0
  31. package/dist/lexicon/lexicons.js.map +1 -1
  32. package/dist/lexicon/types/app/bsky/unspecced/defs.d.ts +27 -0
  33. package/dist/lexicon/types/app/bsky/unspecced/defs.d.ts.map +1 -1
  34. package/dist/lexicon/types/app/bsky/unspecced/defs.js +18 -0
  35. package/dist/lexicon/types/app/bsky/unspecced/defs.js.map +1 -1
  36. package/dist/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacks.d.ts +36 -0
  37. package/dist/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacks.d.ts.map +1 -0
  38. package/dist/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacks.js +7 -0
  39. package/dist/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacks.js.map +1 -0
  40. package/dist/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.d.ts +37 -0
  41. package/dist/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.d.ts.map +1 -0
  42. package/dist/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.js +7 -0
  43. package/dist/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.js.map +1 -0
  44. package/dist/lexicon/types/app/bsky/unspecced/getTrends.d.ts +36 -0
  45. package/dist/lexicon/types/app/bsky/unspecced/getTrends.d.ts.map +1 -0
  46. package/dist/lexicon/types/app/bsky/unspecced/getTrends.js +7 -0
  47. package/dist/lexicon/types/app/bsky/unspecced/getTrends.js.map +1 -0
  48. package/dist/lexicon/types/app/bsky/unspecced/getTrendsSkeleton.d.ts +38 -0
  49. package/dist/lexicon/types/app/bsky/unspecced/getTrendsSkeleton.d.ts.map +1 -0
  50. package/dist/lexicon/types/app/bsky/unspecced/getTrendsSkeleton.js +7 -0
  51. package/dist/lexicon/types/app/bsky/unspecced/getTrendsSkeleton.js.map +1 -0
  52. package/dist/lexicon/types/com/atproto/admin/updateAccountSigningKey.d.ts +31 -0
  53. package/dist/lexicon/types/com/atproto/admin/updateAccountSigningKey.d.ts.map +1 -0
  54. package/dist/lexicon/types/com/atproto/admin/updateAccountSigningKey.js +7 -0
  55. package/dist/lexicon/types/com/atproto/admin/updateAccountSigningKey.js.map +1 -0
  56. package/dist/repo/prepare.d.ts.map +1 -1
  57. package/dist/repo/prepare.js +1 -0
  58. package/dist/repo/prepare.js.map +1 -1
  59. package/dist/repo/types.d.ts +1 -0
  60. package/dist/repo/types.d.ts.map +1 -1
  61. package/dist/repo/types.js.map +1 -1
  62. package/dist/scripts/index.d.ts +8 -1
  63. package/dist/scripts/index.d.ts.map +1 -1
  64. package/dist/scripts/index.js +11 -0
  65. package/dist/scripts/index.js.map +1 -1
  66. package/dist/scripts/publish-identity.d.ts +8 -0
  67. package/dist/scripts/publish-identity.d.ts.map +1 -0
  68. package/dist/scripts/publish-identity.js +46 -0
  69. package/dist/scripts/publish-identity.js.map +1 -0
  70. package/dist/scripts/rebuild-repo.d.ts +10 -2
  71. package/dist/scripts/rebuild-repo.d.ts.map +1 -1
  72. package/dist/scripts/rebuild-repo.js +21 -12
  73. package/dist/scripts/rebuild-repo.js.map +1 -1
  74. package/dist/scripts/rotate-keys.d.ts +18 -0
  75. package/dist/scripts/rotate-keys.d.ts.map +1 -0
  76. package/dist/scripts/rotate-keys.js +115 -0
  77. package/dist/scripts/rotate-keys.js.map +1 -0
  78. package/dist/scripts/sequencer-recovery/index.d.ts +3 -0
  79. package/dist/scripts/sequencer-recovery/index.d.ts.map +1 -0
  80. package/dist/scripts/sequencer-recovery/index.js +17 -0
  81. package/dist/scripts/sequencer-recovery/index.js.map +1 -0
  82. package/dist/scripts/sequencer-recovery/recoverer.d.ts +27 -0
  83. package/dist/scripts/sequencer-recovery/recoverer.d.ts.map +1 -0
  84. package/dist/scripts/sequencer-recovery/recoverer.js +235 -0
  85. package/dist/scripts/sequencer-recovery/recoverer.js.map +1 -0
  86. package/dist/scripts/sequencer-recovery/recovery-db.d.ts +18 -0
  87. package/dist/scripts/sequencer-recovery/recovery-db.d.ts.map +1 -0
  88. package/dist/scripts/sequencer-recovery/recovery-db.js +45 -0
  89. package/dist/scripts/sequencer-recovery/recovery-db.js.map +1 -0
  90. package/dist/scripts/sequencer-recovery/repair-repos.d.ts +3 -0
  91. package/dist/scripts/sequencer-recovery/repair-repos.d.ts.map +1 -0
  92. package/dist/scripts/sequencer-recovery/repair-repos.js +45 -0
  93. package/dist/scripts/sequencer-recovery/repair-repos.js.map +1 -0
  94. package/dist/scripts/sequencer-recovery/user-queues.d.ts +12 -0
  95. package/dist/scripts/sequencer-recovery/user-queues.d.ts.map +1 -0
  96. package/dist/scripts/sequencer-recovery/user-queues.js +54 -0
  97. package/dist/scripts/sequencer-recovery/user-queues.js.map +1 -0
  98. package/dist/scripts/util.d.ts +2 -0
  99. package/dist/scripts/util.d.ts.map +1 -0
  100. package/dist/scripts/util.js +12 -0
  101. package/dist/scripts/util.js.map +1 -0
  102. package/dist/sequencer/sequencer.d.ts +2 -0
  103. package/dist/sequencer/sequencer.d.ts.map +1 -1
  104. package/dist/sequencer/sequencer.js +52 -42
  105. package/dist/sequencer/sequencer.js.map +1 -1
  106. package/package.json +10 -10
  107. package/src/account-manager/account-manager.ts +6 -2
  108. package/src/actor-store/blob/transactor.ts +25 -7
  109. package/src/api/com/atproto/repo/uploadBlob.ts +1 -0
  110. package/src/config/env.ts +2 -0
  111. package/src/config/secrets.ts +2 -0
  112. package/src/context.ts +23 -0
  113. package/src/lexicon/index.ts +62 -0
  114. package/src/lexicon/lexicons.ts +285 -0
  115. package/src/lexicon/types/app/bsky/unspecced/defs.ts +45 -0
  116. package/src/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacks.ts +54 -0
  117. package/src/lexicon/types/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.ts +55 -0
  118. package/src/lexicon/types/app/bsky/unspecced/getTrends.ts +54 -0
  119. package/src/lexicon/types/app/bsky/unspecced/getTrendsSkeleton.ts +56 -0
  120. package/src/lexicon/types/com/atproto/admin/updateAccountSigningKey.ts +48 -0
  121. package/src/repo/prepare.ts +1 -0
  122. package/src/repo/types.ts +1 -0
  123. package/src/scripts/README.md +40 -0
  124. package/src/scripts/index.ts +15 -0
  125. package/src/scripts/publish-identity.ts +54 -0
  126. package/src/scripts/rebuild-repo.ts +39 -12
  127. package/src/scripts/rotate-keys.ts +141 -0
  128. package/src/scripts/sequencer-recovery/index.ts +23 -0
  129. package/src/scripts/sequencer-recovery/recoverer.ts +289 -0
  130. package/src/scripts/sequencer-recovery/recovery-db.ts +64 -0
  131. package/src/scripts/sequencer-recovery/repair-repos.ts +48 -0
  132. package/src/scripts/sequencer-recovery/user-queues.ts +41 -0
  133. package/src/scripts/util.ts +7 -0
  134. package/src/sequencer/sequencer.ts +43 -40
  135. package/tests/recovery.test.ts +178 -0
  136. package/tsconfig.build.tsbuildinfo +1 -1
  137. package/tsconfig.tests.tsbuildinfo +1 -1
@@ -0,0 +1,289 @@
1
+ import { rmIfExists } from '@atproto/common'
2
+ import { Secp256k1Keypair } from '@atproto/crypto'
3
+ import {
4
+ BlockMap,
5
+ CidSet,
6
+ CommitData,
7
+ WriteOpAction,
8
+ cborToLexRecord,
9
+ parseDataKey,
10
+ readCar,
11
+ } from '@atproto/repo'
12
+ import {
13
+ AccountManager,
14
+ AccountStatus,
15
+ } from '../../account-manager/account-manager'
16
+ import { ActorStore } from '../../actor-store/actor-store'
17
+ import { ActorStoreTransactor } from '../../actor-store/actor-store-transactor'
18
+ import { countAll } from '../../db'
19
+ import {
20
+ PreparedWrite,
21
+ prepareCreate,
22
+ prepareDelete,
23
+ prepareUpdate,
24
+ } from '../../repo'
25
+ import { AccountEvt, CommitEvt, SeqEvt, Sequencer } from '../../sequencer'
26
+ import { RecoveryDb } from './recovery-db'
27
+ import { UserQueues } from './user-queues'
28
+
29
+ export type RecovererContextNoDb = {
30
+ sequencer: Sequencer
31
+ accountManager: AccountManager
32
+ actorStore: ActorStore
33
+ }
34
+
35
+ export type RecovererContext = RecovererContextNoDb & {
36
+ recoveryDb: RecoveryDb
37
+ }
38
+
39
+ const PAGE_SIZE = 5000
40
+
41
+ export class Recoverer {
42
+ queues: UserQueues
43
+ failed: Set<string>
44
+
45
+ constructor(
46
+ public ctx: RecovererContext,
47
+ opts: { concurrency: number },
48
+ ) {
49
+ this.queues = new UserQueues(opts.concurrency)
50
+ this.failed = new Set()
51
+ }
52
+
53
+ async run(startCursor = 0) {
54
+ const failed = await this.ctx.recoveryDb.db
55
+ .selectFrom('failed')
56
+ .select('did')
57
+ .execute()
58
+ for (const row of failed) {
59
+ this.failed.add(row.did)
60
+ }
61
+
62
+ const totalRes = await this.ctx.sequencer.db.db
63
+ .selectFrom('repo_seq')
64
+ .select(countAll.as('count'))
65
+ .executeTakeFirstOrThrow()
66
+ const totalEvts = totalRes.count
67
+ let completed = 0
68
+
69
+ let cursor: number | undefined = startCursor
70
+ while (cursor !== undefined) {
71
+ const page = await this.ctx.sequencer.requestSeqRange({
72
+ earliestSeq: cursor,
73
+ limit: PAGE_SIZE,
74
+ })
75
+ page.forEach((evt) => this.processEvent(evt))
76
+ cursor = page.at(-1)?.seq
77
+
78
+ await this.queues.onEmpty()
79
+
80
+ completed += PAGE_SIZE
81
+ const percentComplete = (completed / totalEvts) * 100
82
+ console.log(`${percentComplete.toFixed(2)}% - ${cursor}`)
83
+ }
84
+
85
+ await this.queues.processAll()
86
+ }
87
+
88
+ async processAll() {
89
+ await this.queues.processAll()
90
+ }
91
+
92
+ async destroy() {
93
+ await this.queues.destroy()
94
+ }
95
+
96
+ processEvent(evt: SeqEvt) {
97
+ const did = didFromEvt(evt)
98
+ if (!did) {
99
+ return
100
+ }
101
+ this.queues.addToUser(did, async () => {
102
+ if (this.failed.has(did)) {
103
+ return
104
+ }
105
+ await processSeqEvt(this.ctx, evt).catch(async (err) => {
106
+ this.failed.add(did)
107
+ await trackFailure(this.ctx.recoveryDb, did, err)
108
+ })
109
+ })
110
+ }
111
+ }
112
+
113
+ export const processSeqEvt = async (ctx: RecovererContext, evt: SeqEvt) => {
114
+ // only need to process commits & tombstones
115
+ if (evt.type === 'account') {
116
+ await processAccountEvt(ctx, evt.evt)
117
+ }
118
+ if (evt.type === 'commit') {
119
+ await processCommit(ctx, evt.evt).catch()
120
+ }
121
+ }
122
+
123
+ const processCommit = async (ctx: RecovererContext, evt: CommitEvt) => {
124
+ const did = evt.repo
125
+ const { writes, blocks } = await parseCommitEvt(evt)
126
+ if (evt.since === null) {
127
+ const actorExists = await ctx.actorStore.exists(did)
128
+ if (!actorExists) {
129
+ await processRepoCreation(ctx, evt, writes, blocks)
130
+ return
131
+ }
132
+ }
133
+ await ctx.actorStore.transact(did, async (actorTxn) => {
134
+ const root = await actorTxn.repo.storage.getRootDetailed()
135
+ if (root.rev >= evt.rev) {
136
+ return
137
+ }
138
+ const commit = await actorTxn.repo.formatCommit(writes)
139
+ commit.newBlocks = blocks
140
+ commit.cid = evt.commit
141
+ commit.rev = evt.rev
142
+ await Promise.all([
143
+ actorTxn.repo.storage.applyCommit(commit),
144
+ actorTxn.repo.indexWrites(writes, commit.rev),
145
+ trackBlobs(actorTxn, writes),
146
+ ])
147
+ })
148
+ }
149
+
150
+ const processRepoCreation = async (
151
+ ctx: RecovererContext,
152
+ evt: CommitEvt,
153
+ writes: PreparedWrite[],
154
+ blocks: BlockMap,
155
+ ) => {
156
+ const did = evt.repo
157
+ const keypair = await Secp256k1Keypair.create({ exportable: true })
158
+ await ctx.actorStore.create(did, keypair)
159
+ const commit: CommitData = {
160
+ cid: evt.commit,
161
+ rev: evt.rev,
162
+ since: evt.since,
163
+ prev: null,
164
+ newBlocks: blocks,
165
+ relevantBlocks: new BlockMap(),
166
+ removedCids: new CidSet(),
167
+ }
168
+ await ctx.actorStore.transact(did, (actorTxn) =>
169
+ Promise.all([
170
+ actorTxn.repo.storage.applyCommit(commit, true),
171
+ actorTxn.repo.indexWrites(writes, commit.rev),
172
+ actorTxn.repo.blob.processWriteBlobs(commit.rev, writes),
173
+ ]),
174
+ )
175
+ await trackNewAccount(ctx.recoveryDb, did)
176
+ }
177
+
178
+ const processAccountEvt = async (ctx: RecovererContext, evt: AccountEvt) => {
179
+ // do not need to process deactivation/takedowns because we backup account DB as well
180
+ if (evt.status !== AccountStatus.Deleted) {
181
+ return
182
+ }
183
+ const { directory } = await ctx.actorStore.getLocation(evt.did)
184
+ await rmIfExists(directory, true)
185
+ await ctx.accountManager.deleteAccount(evt.did)
186
+ }
187
+
188
+ const trackBlobs = async (
189
+ store: ActorStoreTransactor,
190
+ writes: PreparedWrite[],
191
+ ) => {
192
+ await store.repo.blob.deleteDereferencedBlobs(writes)
193
+
194
+ for (const write of writes) {
195
+ if (
196
+ write.action === WriteOpAction.Create ||
197
+ write.action === WriteOpAction.Update
198
+ ) {
199
+ for (const blob of write.blobs) {
200
+ await store.repo.blob.insertBlobMetadata(blob)
201
+ await store.repo.blob.associateBlob(blob, write.uri)
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ const trackFailure = async (
208
+ recoveryDb: RecoveryDb,
209
+ did: string,
210
+ err: unknown,
211
+ ) => {
212
+ await recoveryDb.db
213
+ .insertInto('failed')
214
+ .values({
215
+ did,
216
+ error: err?.toString(),
217
+ fixed: 0,
218
+ })
219
+ .onConflict((oc) => oc.doNothing())
220
+ .execute()
221
+ }
222
+
223
+ const trackNewAccount = async (recoveryDb: RecoveryDb, did: string) => {
224
+ await recoveryDb.db
225
+ .insertInto('new_account')
226
+ .values({
227
+ did,
228
+ published: 0,
229
+ })
230
+ .onConflict((oc) => oc.doNothing())
231
+ .execute()
232
+ }
233
+
234
+ const parseCommitEvt = async (
235
+ evt: CommitEvt,
236
+ ): Promise<{
237
+ writes: PreparedWrite[]
238
+ blocks: BlockMap
239
+ }> => {
240
+ const did = evt.repo
241
+ const evtCar = await readCar(evt.blocks)
242
+ const writesUnfiltered = await Promise.all(
243
+ evt.ops.map(async (op) => {
244
+ const { collection, rkey } = parseDataKey(op.path)
245
+ if (op.action === 'delete') {
246
+ return prepareDelete({ did, collection, rkey })
247
+ }
248
+ if (!op.cid) return undefined
249
+ const recordBytes = evtCar.blocks.get(op.cid)
250
+ if (!recordBytes) return undefined
251
+ const record = cborToLexRecord(recordBytes)
252
+
253
+ if (op.action === 'create') {
254
+ return prepareCreate({
255
+ did,
256
+ collection,
257
+ rkey,
258
+ record,
259
+ validate: false,
260
+ })
261
+ } else {
262
+ return prepareUpdate({
263
+ did,
264
+ collection,
265
+ rkey,
266
+ record,
267
+ validate: false,
268
+ })
269
+ }
270
+ }),
271
+ )
272
+ const writes = writesUnfiltered.filter(
273
+ (w) => w !== undefined,
274
+ ) as PreparedWrite[]
275
+ return {
276
+ writes,
277
+ blocks: evtCar.blocks,
278
+ }
279
+ }
280
+
281
+ const didFromEvt = (evt: SeqEvt): string | null => {
282
+ if (evt.type === 'account') {
283
+ return evt.evt.did
284
+ } else if (evt.type === 'commit') {
285
+ return evt.evt.repo
286
+ } else {
287
+ return null
288
+ }
289
+ }
@@ -0,0 +1,64 @@
1
+ import path from 'node:path'
2
+ import { Kysely } from 'kysely'
3
+ import { Database, Migrator } from '../../db'
4
+
5
+ export interface NewAccount {
6
+ did: string
7
+ published: 0 | 1
8
+ }
9
+
10
+ export interface Failed {
11
+ did: string
12
+ error: string | null
13
+ fixed: 0 | 1
14
+ }
15
+
16
+ export type RecoveryDbSchema = {
17
+ new_account: NewAccount
18
+ failed: Failed
19
+ }
20
+
21
+ export type RecoveryDb = Database<RecoveryDbSchema>
22
+
23
+ export const getRecoveryDbFromSequencerLoc = (
24
+ sequencerLoc: string,
25
+ ): Promise<RecoveryDb> => {
26
+ const recoveryDbLoc = path.join(path.dirname(sequencerLoc), 'recovery.sqlite')
27
+ return getAndMigrateRecoveryDb(recoveryDbLoc)
28
+ }
29
+
30
+ export const getAndMigrateRecoveryDb = async (
31
+ location: string,
32
+ disableWalAutoCheckpoint = false,
33
+ ): Promise<RecoveryDb> => {
34
+ const pragmas: Record<string, string> = disableWalAutoCheckpoint
35
+ ? { wal_autocheckpoint: '0' }
36
+ : {}
37
+ const db = Database.sqlite(location, pragmas)
38
+ const migrator = new Migrator(db.db, migrations)
39
+ await migrator.migrateToLatestOrThrow()
40
+ return db
41
+ }
42
+
43
+ const migrations = {
44
+ '001': {
45
+ up: async (db: Kysely<unknown>) => {
46
+ await db.schema
47
+ .createTable('new_account')
48
+ .addColumn('did', 'varchar', (col) => col.primaryKey())
49
+ .addColumn('published', 'int2', (col) => col.notNull())
50
+ .execute()
51
+
52
+ await db.schema
53
+ .createTable('failed')
54
+ .addColumn('did', 'varchar', (col) => col.primaryKey())
55
+ .addColumn('error', 'varchar')
56
+ .addColumn('fixed', 'int2', (col) => col.notNull())
57
+ .execute()
58
+ },
59
+ down: async (db: Kysely<unknown>) => {
60
+ await db.schema.dropTable('new_account').execute()
61
+ await db.schema.dropTable('failed').execute()
62
+ },
63
+ },
64
+ }
@@ -0,0 +1,48 @@
1
+ import { parseRepoSeqRows } from '../../sequencer'
2
+ import { rebuildRepo } from '../rebuild-repo'
3
+ import {
4
+ RecovererContext,
5
+ RecovererContextNoDb,
6
+ processSeqEvt,
7
+ } from './recoverer'
8
+ import { getRecoveryDbFromSequencerLoc } from './recovery-db'
9
+
10
+ export const repairRepos = async (ctx: RecovererContextNoDb) => {
11
+ const recoveryDb = await getRecoveryDbFromSequencerLoc(
12
+ ctx.sequencer.dbLocation,
13
+ )
14
+ const repairRes = await recoveryDb.db
15
+ .selectFrom('failed')
16
+ .select('did')
17
+ .where('failed.fixed', '=', 0)
18
+ .execute()
19
+ const dids = repairRes.map((row) => row.did)
20
+ let fixed = 0
21
+ for (const did of dids) {
22
+ await rebuildRepo(ctx, did, false)
23
+ await recoverFromSequencer({ ...ctx, recoveryDb }, did)
24
+ fixed++
25
+ console.log(`${fixed}/${dids.length}`)
26
+ }
27
+ }
28
+
29
+ const recoverFromSequencer = async (ctx: RecovererContext, did: string) => {
30
+ const didEvts = await ctx.sequencer.db.db
31
+ .selectFrom('repo_seq')
32
+ .selectAll()
33
+ .where('did', '=', did)
34
+ .orderBy('seq', 'asc')
35
+ .execute()
36
+ const seqEvts = parseRepoSeqRows(didEvts)
37
+ for (const evt of seqEvts) {
38
+ await processSeqEvt(ctx, evt)
39
+ }
40
+ await ctx.recoveryDb.db
41
+ .updateTable('failed')
42
+ .set({
43
+ fixed: 1,
44
+ error: null,
45
+ })
46
+ .where('did', '=', did)
47
+ .execute()
48
+ }
@@ -0,0 +1,41 @@
1
+ import PQueue from 'p-queue'
2
+ export class UserQueues {
3
+ main: PQueue
4
+ queues = new Map<string, PQueue>()
5
+
6
+ constructor(concurrency: number) {
7
+ this.main = new PQueue({ concurrency })
8
+ }
9
+
10
+ async addToUser(did: string, task: () => Promise<void>) {
11
+ if (this.main.isPaused) return
12
+ return this.main.add(() => {
13
+ return this.getQueue(did).add(task)
14
+ })
15
+ }
16
+
17
+ private getQueue(did: string) {
18
+ let queue = this.queues.get(did)
19
+ if (!queue) {
20
+ queue = new PQueue({ concurrency: 1 })
21
+ queue.once('idle', () => this.queues.delete(did))
22
+ this.queues.set(did, queue)
23
+ }
24
+ return queue
25
+ }
26
+
27
+ async onEmpty() {
28
+ await this.main.onEmpty()
29
+ }
30
+
31
+ async processAll() {
32
+ await this.main.onIdle()
33
+ }
34
+
35
+ async destroy() {
36
+ this.main.pause()
37
+ this.main.clear()
38
+ this.queues.forEach((q) => q.clear())
39
+ await this.processAll()
40
+ }
41
+ }
@@ -0,0 +1,7 @@
1
+ export const parseIntArg = (arg: string): number => {
2
+ const parsed = parseInt(arg, 10)
3
+ if (isNaN(parsed)) {
4
+ throw new Error(`Invalid arg, expected number: ${arg}`)
5
+ }
6
+ return parsed
7
+ }
@@ -33,7 +33,7 @@ export class Sequencer extends (EventEmitter as new () => SequencerEmitter) {
33
33
  triesWithNoResults = 0
34
34
 
35
35
  constructor(
36
- dbLocation: string,
36
+ public dbLocation: string,
37
37
  public crawlers: Crawlers,
38
38
  public lastSeen = 0,
39
39
  disableWalAutoCheckpoint = false,
@@ -126,45 +126,7 @@ export class Sequencer extends (EventEmitter as new () => SequencerEmitter) {
126
126
  return []
127
127
  }
128
128
 
129
- const seqEvts: SeqEvt[] = []
130
- for (const row of rows) {
131
- // should never hit this because of WHERE clause
132
- if (row.seq === null) {
133
- continue
134
- }
135
- const evt = cborDecode(row.event)
136
- if (row.eventType === 'append') {
137
- seqEvts.push({
138
- type: 'commit',
139
- seq: row.seq,
140
- time: row.sequencedAt,
141
- evt: evt as CommitEvt,
142
- })
143
- } else if (row.eventType === 'sync') {
144
- seqEvts.push({
145
- type: 'sync',
146
- seq: row.seq,
147
- time: row.sequencedAt,
148
- evt: evt as SyncEvt,
149
- })
150
- } else if (row.eventType === 'identity') {
151
- seqEvts.push({
152
- type: 'identity',
153
- seq: row.seq,
154
- time: row.sequencedAt,
155
- evt: evt as IdentityEvt,
156
- })
157
- } else if (row.eventType === 'account') {
158
- seqEvts.push({
159
- type: 'account',
160
- seq: row.seq,
161
- time: row.sequencedAt,
162
- evt: evt as AccountEvt,
163
- })
164
- }
165
- }
166
-
167
- return seqEvts
129
+ return parseRepoSeqRows(rows)
168
130
  }
169
131
 
170
132
  private async pollDb(): Promise<void> {
@@ -243,6 +205,47 @@ export class Sequencer extends (EventEmitter as new () => SequencerEmitter) {
243
205
  }
244
206
  }
245
207
 
208
+ export const parseRepoSeqRows = (rows: RepoSeqEntry[]): SeqEvt[] => {
209
+ const seqEvts: SeqEvt[] = []
210
+ for (const row of rows) {
211
+ // should never hit this because of WHERE clause
212
+ if (row.seq === null) {
213
+ continue
214
+ }
215
+ const evt = cborDecode(row.event)
216
+ if (row.eventType === 'append') {
217
+ seqEvts.push({
218
+ type: 'commit',
219
+ seq: row.seq,
220
+ time: row.sequencedAt,
221
+ evt: evt as CommitEvt,
222
+ })
223
+ } else if (row.eventType === 'sync') {
224
+ seqEvts.push({
225
+ type: 'sync',
226
+ seq: row.seq,
227
+ time: row.sequencedAt,
228
+ evt: evt as SyncEvt,
229
+ })
230
+ } else if (row.eventType === 'identity') {
231
+ seqEvts.push({
232
+ type: 'identity',
233
+ seq: row.seq,
234
+ time: row.sequencedAt,
235
+ evt: evt as IdentityEvt,
236
+ })
237
+ } else if (row.eventType === 'account') {
238
+ seqEvts.push({
239
+ type: 'account',
240
+ seq: row.seq,
241
+ time: row.sequencedAt,
242
+ evt: evt as AccountEvt,
243
+ })
244
+ }
245
+ }
246
+ return seqEvts
247
+ }
248
+
246
249
  type SeqRow = RepoSeqEntry
247
250
 
248
251
  type SequencerEvents = {