@atproto/bsync 0.0.31 → 0.0.33

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 (48) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/logger.js +2 -2
  6. package/dist/logger.js.map +1 -1
  7. package/package.json +17 -13
  8. package/bin/migration-create.ts +0 -38
  9. package/buf.gen.yaml +0 -12
  10. package/jest.config.cjs +0 -21
  11. package/proto/bsync.proto +0 -134
  12. package/src/client.ts +0 -25
  13. package/src/config.ts +0 -90
  14. package/src/context.ts +0 -48
  15. package/src/db/index.ts +0 -200
  16. package/src/db/migrations/20240108T220751294Z-init.ts +0 -26
  17. package/src/db/migrations/20240717T224303472Z-notif-ops.ts +0 -24
  18. package/src/db/migrations/20250527T022203400Z-add-operation.ts +0 -20
  19. package/src/db/migrations/20250603T163446567Z-alter-operation.ts +0 -19
  20. package/src/db/migrations/index.ts +0 -8
  21. package/src/db/migrations/provider.ts +0 -8
  22. package/src/db/schema/index.ts +0 -16
  23. package/src/db/schema/mute_item.ts +0 -13
  24. package/src/db/schema/mute_op.ts +0 -18
  25. package/src/db/schema/notif_item.ts +0 -13
  26. package/src/db/schema/notif_op.ts +0 -16
  27. package/src/db/schema/operation.ts +0 -20
  28. package/src/db/types.ts +0 -19
  29. package/src/index.ts +0 -130
  30. package/src/logger.ts +0 -26
  31. package/src/routes/add-mute-operation.ts +0 -154
  32. package/src/routes/add-notif-operation.ts +0 -80
  33. package/src/routes/auth.ts +0 -15
  34. package/src/routes/delete-operations.ts +0 -45
  35. package/src/routes/index.ts +0 -28
  36. package/src/routes/put-operation.ts +0 -115
  37. package/src/routes/scan-mute-operations.ts +0 -65
  38. package/src/routes/scan-notif-operations.ts +0 -64
  39. package/src/routes/scan-operations.ts +0 -67
  40. package/src/routes/util.ts +0 -67
  41. package/tests/delete-operations.test.ts +0 -108
  42. package/tests/mutes.test.ts +0 -352
  43. package/tests/notifications.test.ts +0 -209
  44. package/tests/operations.test.ts +0 -327
  45. package/tsconfig.build.json +0 -8
  46. package/tsconfig.build.tsbuildinfo +0 -1
  47. package/tsconfig.json +0 -7
  48. package/tsconfig.tests.json +0 -7
package/src/db/index.ts DELETED
@@ -1,200 +0,0 @@
1
- import assert from 'node:assert'
2
- import { EventEmitter } from 'node:events'
3
- import {
4
- Kysely,
5
- KyselyPlugin,
6
- PluginTransformQueryArgs,
7
- PluginTransformResultArgs,
8
- PostgresDialect,
9
- QueryResult,
10
- RootOperationNode,
11
- UnknownRow,
12
- } from 'kysely'
13
- import { Migrator } from 'kysely/migration'
14
- // eslint-disable-next-line import/default
15
- import pg from 'pg'
16
- // eslint-disable-next-line import/no-named-as-default-member
17
- const { Pool: PgPool, types: pgTypes } = pg
18
- type PgPool = InstanceType<typeof PgPool>
19
- import type TypedEmitter from 'typed-emitter'
20
- import { dbLogger } from '../logger.js'
21
- import * as migrations from './migrations/index.js'
22
- import { DbMigrationProvider } from './migrations/provider.js'
23
- import { DatabaseSchema, DatabaseSchemaType } from './schema/index.js'
24
- import { PgOptions } from './types.js'
25
-
26
- export class Database {
27
- pool: PgPool
28
- db: DatabaseSchema
29
- migrator: Migrator
30
- txEvt = new EventEmitter() as TxnEmitter
31
- destroyed = false
32
-
33
- constructor(
34
- public opts: PgOptions,
35
- instances?: { db: DatabaseSchema; pool: PgPool },
36
- ) {
37
- // if instances are provided, use those
38
- if (instances) {
39
- this.db = instances.db
40
- this.pool = instances.pool
41
- } else {
42
- // else create a pool & connect
43
- const { schema, url } = opts
44
- const pool =
45
- opts.pool ??
46
- new PgPool({
47
- connectionString: url,
48
- max: opts.poolSize,
49
- maxUses: opts.poolMaxUses,
50
- idleTimeoutMillis: opts.poolIdleTimeoutMs,
51
- })
52
-
53
- // Select count(*) and other pg bigints as js integer
54
- pgTypes.setTypeParser(pgTypes.builtins.INT8, (n) => parseInt(n, 10))
55
-
56
- // Setup schema usage, primarily for test parallelism (each test suite runs in its own pg schema)
57
- if (schema && !/^[a-z_]+$/i.test(schema)) {
58
- throw new Error(
59
- `Postgres schema must only contain [A-Za-z_]: ${schema}`,
60
- )
61
- }
62
-
63
- pool.on('error', onPoolError)
64
- pool.on('connect', (client) => {
65
- client.on('error', onClientError)
66
- if (schema) {
67
- // Shared objects such as extensions will go in the public schema
68
- client.query(`SET search_path TO "${schema}",public;`)
69
- }
70
- })
71
-
72
- this.pool = pool
73
- this.db = new Kysely<DatabaseSchemaType>({
74
- dialect: new PostgresDialect({ pool }),
75
- })
76
- }
77
-
78
- this.migrator = new Migrator({
79
- db: this.db,
80
- migrationTableSchema: opts.schema,
81
- provider: new DbMigrationProvider(migrations),
82
- })
83
- }
84
-
85
- get schema(): string | undefined {
86
- return this.opts.schema
87
- }
88
-
89
- get isTransaction() {
90
- return this.db.isTransaction
91
- }
92
-
93
- assertTransaction() {
94
- assert(this.isTransaction, 'Transaction required')
95
- }
96
-
97
- assertNotTransaction() {
98
- assert(!this.isTransaction, 'Cannot be in a transaction')
99
- }
100
-
101
- async transaction<T>(fn: (db: Database) => Promise<T>): Promise<T> {
102
- const leakyTxPlugin = new LeakyTxPlugin()
103
- const { dbTxn, txRes } = await this.db
104
- .withPlugin(leakyTxPlugin)
105
- .transaction()
106
- .execute(async (txn) => {
107
- const dbTxn = new Database(this.opts, {
108
- db: txn,
109
- pool: this.pool,
110
- })
111
- const txRes = await fn(dbTxn)
112
- .catch(async (err) => {
113
- leakyTxPlugin.endTx()
114
- // ensure that all in-flight queries are flushed & the connection is open
115
- await dbTxn.db.getExecutor().provideConnection(noopAsync)
116
- throw err
117
- })
118
- .finally(() => leakyTxPlugin.endTx())
119
- return { dbTxn, txRes }
120
- })
121
- dbTxn?.txEvt.emit('commit')
122
- return txRes
123
- }
124
-
125
- onCommit(fn: () => void) {
126
- this.assertTransaction()
127
- this.txEvt.once('commit', fn)
128
- }
129
-
130
- async close(): Promise<void> {
131
- if (this.destroyed) return
132
- await this.db.destroy()
133
- this.destroyed = true
134
- }
135
-
136
- async migrateToOrThrow(migration: string) {
137
- if (this.schema) {
138
- await this.db.schema.createSchema(this.schema).ifNotExists().execute()
139
- }
140
- const { error, results } = await this.migrator.migrateTo(migration)
141
- if (error) {
142
- throw error
143
- }
144
- if (!results) {
145
- throw new Error('An unknown failure occurred while migrating')
146
- }
147
- return results
148
- }
149
-
150
- async migrateToLatestOrThrow() {
151
- if (this.schema) {
152
- await this.db.schema.createSchema(this.schema).ifNotExists().execute()
153
- }
154
- const { error, results } = await this.migrator.migrateToLatest()
155
- if (error) {
156
- throw error
157
- }
158
- if (!results) {
159
- throw new Error('An unknown failure occurred while migrating')
160
- }
161
- return results
162
- }
163
- }
164
-
165
- export default Database
166
-
167
- const onPoolError = (err: Error) => dbLogger.error({ err }, 'db pool error')
168
- const onClientError = (err: Error) => dbLogger.error({ err }, 'db client error')
169
-
170
- // utils
171
- // -------
172
-
173
- class LeakyTxPlugin implements KyselyPlugin {
174
- private txOver = false
175
-
176
- endTx() {
177
- this.txOver = true
178
- }
179
-
180
- transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
181
- if (this.txOver) {
182
- throw new Error('tx already failed')
183
- }
184
- return args.node
185
- }
186
-
187
- async transformResult(
188
- args: PluginTransformResultArgs,
189
- ): Promise<QueryResult<UnknownRow>> {
190
- return args.result
191
- }
192
- }
193
-
194
- type TxnEmitter = TypedEmitter.default<TxnEvents>
195
-
196
- type TxnEvents = {
197
- commit: () => void
198
- }
199
-
200
- const noopAsync = async () => {}
@@ -1,26 +0,0 @@
1
- import { Kysely, sql } from 'kysely'
2
-
3
- export async function up(db: Kysely<unknown>): Promise<void> {
4
- await db.schema
5
- .createTable('mute_op')
6
- .addColumn('id', 'bigserial', (col) => col.primaryKey())
7
- .addColumn('type', 'int2', (col) => col.notNull()) // integer enum: 0->add, 1->remove, 2->clear
8
- .addColumn('actorDid', 'varchar', (col) => col.notNull())
9
- .addColumn('subject', 'varchar', (col) => col.notNull())
10
- .addColumn('createdAt', 'timestamptz', (col) =>
11
- col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),
12
- )
13
- .execute()
14
- await db.schema
15
- .createTable('mute_item')
16
- .addColumn('actorDid', 'varchar', (col) => col.notNull())
17
- .addColumn('subject', 'varchar', (col) => col.notNull())
18
- .addColumn('fromId', 'bigint', (col) => col.notNull())
19
- .addPrimaryKeyConstraint('mute_item_pkey', ['actorDid', 'subject'])
20
- .execute()
21
- }
22
-
23
- export async function down(db: Kysely<unknown>): Promise<void> {
24
- await db.schema.dropTable('mute_item').execute()
25
- await db.schema.dropTable('mute_op').execute()
26
- }
@@ -1,24 +0,0 @@
1
- import { Kysely, sql } from 'kysely'
2
-
3
- export async function up(db: Kysely<unknown>): Promise<void> {
4
- await db.schema
5
- .createTable('notif_op')
6
- .addColumn('id', 'bigserial', (col) => col.primaryKey())
7
- .addColumn('actorDid', 'varchar', (col) => col.notNull())
8
- .addColumn('priority', 'boolean')
9
- .addColumn('createdAt', 'timestamptz', (col) =>
10
- col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),
11
- )
12
- .execute()
13
- await db.schema
14
- .createTable('notif_item')
15
- .addColumn('actorDid', 'varchar', (col) => col.primaryKey())
16
- .addColumn('priority', 'boolean', (col) => col.notNull())
17
- .addColumn('fromId', 'bigint', (col) => col.notNull())
18
- .execute()
19
- }
20
-
21
- export async function down(db: Kysely<unknown>): Promise<void> {
22
- await db.schema.dropTable('notif_item').execute()
23
- await db.schema.dropTable('notif_op').execute()
24
- }
@@ -1,20 +0,0 @@
1
- import { Kysely, sql } from 'kysely'
2
-
3
- export async function up(db: Kysely<unknown>): Promise<void> {
4
- await db.schema
5
- .createTable('operation')
6
- .addColumn('id', 'bigserial', (col) => col.primaryKey())
7
- .addColumn('collection', 'varchar', (col) => col.notNull())
8
- .addColumn('actorDid', 'varchar', (col) => col.notNull())
9
- .addColumn('rkey', 'varchar', (col) => col.notNull())
10
- .addColumn('method', 'int2', (col) => col.notNull())
11
- .addColumn('payload', sql`bytea`)
12
- .addColumn('createdAt', 'timestamptz', (col) =>
13
- col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),
14
- )
15
- .execute()
16
- }
17
-
18
- export async function down(db: Kysely<unknown>): Promise<void> {
19
- await db.schema.dropTable('operation').execute()
20
- }
@@ -1,19 +0,0 @@
1
- import { Kysely } from 'kysely'
2
-
3
- export async function up(db: Kysely<unknown>): Promise<void> {
4
- await db.schema
5
- .alterTable('operation')
6
- .renameColumn('collection', 'namespace')
7
- .execute()
8
-
9
- await db.schema.alterTable('operation').renameColumn('rkey', 'key').execute()
10
- }
11
-
12
- export async function down(db: Kysely<unknown>): Promise<void> {
13
- await db.schema
14
- .alterTable('operation')
15
- .renameColumn('namespace', 'collection')
16
- .execute()
17
-
18
- await db.schema.alterTable('operation').renameColumn('key', 'rkey').execute()
19
- }
@@ -1,8 +0,0 @@
1
- // NOTE this file can be edited by hand, but it is also appended to by the migration:create command.
2
- // It's important that every migration is exported from here with the proper name. We'd simplify
3
- // this with kysely's FileMigrationProvider, but it doesn't play nicely with the build process.
4
-
5
- export * as _20240108T220751294Z from './20240108T220751294Z-init.js'
6
- export * as _20240717T224303472Z from './20240717T224303472Z-notif-ops.js'
7
- export * as _20250527T022203400Z from './20250527T022203400Z-add-operation.js'
8
- export * as _20250603T163446567Z from './20250603T163446567Z-alter-operation.js'
@@ -1,8 +0,0 @@
1
- import { Migration, MigrationProvider } from 'kysely/migration'
2
-
3
- export class DbMigrationProvider implements MigrationProvider {
4
- constructor(private migrations: Record<string, Migration>) {}
5
- async getMigrations(): Promise<Record<string, Migration>> {
6
- return this.migrations
7
- }
8
- }
@@ -1,16 +0,0 @@
1
- import { Kysely } from 'kysely'
2
- import * as muteItem from './mute_item.js'
3
- import * as muteOp from './mute_op.js'
4
- import * as notifItem from './notif_item.js'
5
- import * as notifOp from './notif_op.js'
6
- import * as op from './operation.js'
7
-
8
- export type DatabaseSchemaType = muteItem.PartialDB &
9
- muteOp.PartialDB &
10
- notifItem.PartialDB &
11
- notifOp.PartialDB &
12
- op.PartialDB
13
-
14
- export type DatabaseSchema = Kysely<DatabaseSchemaType>
15
-
16
- export default DatabaseSchema
@@ -1,13 +0,0 @@
1
- import { Selectable } from 'kysely'
2
-
3
- export interface MuteItem {
4
- actorDid: string
5
- subject: string // did or aturi for list
6
- fromId: number
7
- }
8
-
9
- export type MuteItemEntry = Selectable<MuteItem>
10
-
11
- export const tableName = 'mute_item'
12
-
13
- export type PartialDB = { [tableName]: MuteItem }
@@ -1,18 +0,0 @@
1
- import { GeneratedAlways, Selectable } from 'kysely'
2
- import { MuteOperation_Type } from '../../proto/bsync_pb.js'
3
-
4
- export interface MuteOp {
5
- id: GeneratedAlways<number>
6
- type: MuteOperation_Type // integer enum: 0->add, 1->remove, 2->clear
7
- actorDid: string
8
- subject: string // did or aturi for list
9
- createdAt: GeneratedAlways<Date>
10
- }
11
-
12
- export type MuteOpEntry = Selectable<MuteOp>
13
-
14
- export const tableName = 'mute_op'
15
-
16
- export type PartialDB = { [tableName]: MuteOp }
17
-
18
- export const createMuteOpChannel = 'mute_op_create' // used with listen/notify
@@ -1,13 +0,0 @@
1
- import { Selectable } from 'kysely'
2
-
3
- export interface NotifItem {
4
- actorDid: string
5
- priority: boolean
6
- fromId: number
7
- }
8
-
9
- export type NotifItemEntry = Selectable<NotifItem>
10
-
11
- export const tableName = 'notif_item'
12
-
13
- export type PartialDB = { [tableName]: NotifItem }
@@ -1,16 +0,0 @@
1
- import { GeneratedAlways, Selectable } from 'kysely'
2
-
3
- export interface NotifOp {
4
- id: GeneratedAlways<number>
5
- actorDid: string
6
- priority: boolean | null
7
- createdAt: GeneratedAlways<Date>
8
- }
9
-
10
- export type NotifOpEntry = Selectable<NotifOp>
11
-
12
- export const tableName = 'notif_op'
13
-
14
- export type PartialDB = { [tableName]: NotifOp }
15
-
16
- export const createNotifOpChannel = 'notif_op_create' // used with listen/notify
@@ -1,20 +0,0 @@
1
- import { GeneratedAlways } from 'kysely'
2
- import { Method } from '../../proto/bsync_pb.js'
3
-
4
- export type OperationMethod = Method.CREATE | Method.UPDATE | Method.DELETE
5
-
6
- export interface Operation {
7
- id: GeneratedAlways<number>
8
- actorDid: string
9
- namespace: string
10
- key: string
11
- method: OperationMethod
12
- payload: Uint8Array<ArrayBuffer>
13
- createdAt: GeneratedAlways<Date>
14
- }
15
-
16
- export const tableName = 'operation'
17
-
18
- export type PartialDB = { [tableName]: Operation }
19
-
20
- export const createOperationChannel = 'operation_create' // used with listen/notify
package/src/db/types.ts DELETED
@@ -1,19 +0,0 @@
1
- import { DynamicModule, RawBuilder, SelectQueryBuilder } from 'kysely'
2
- // eslint-disable-next-line import/default
3
- import pg from 'pg'
4
- type PgPool = pg.Pool
5
-
6
- export type DbRef =
7
- | RawBuilder<unknown>
8
- | ReturnType<DynamicModule<unknown>['ref']>
9
-
10
- export type AnyQb = SelectQueryBuilder<any, any, any>
11
-
12
- export type PgOptions = {
13
- url: string
14
- pool?: PgPool
15
- schema?: string
16
- poolSize?: number
17
- poolMaxUses?: number
18
- poolIdleTimeoutMs?: number
19
- }
package/src/index.ts DELETED
@@ -1,130 +0,0 @@
1
- import events from 'node:events'
2
- import http from 'node:http'
3
- import { connectNodeAdapter } from '@connectrpc/connect-node'
4
- // eslint-disable-next-line import/default
5
- import httpTerminator from 'http-terminator'
6
- import { ServerConfig } from './config.js'
7
- import { AppContext, AppContextOptions } from './context.js'
8
- import { createMuteOpChannel } from './db/schema/mute_op.js'
9
- import { createNotifOpChannel } from './db/schema/notif_op.js'
10
- import { createOperationChannel } from './db/schema/operation.js'
11
- import { dbLogger, loggerMiddleware } from './logger.js'
12
- import routes from './routes/index.js'
13
-
14
- export * from './config.js'
15
- export * from './client.js'
16
- export { Database } from './db/index.js'
17
- export { AppContext } from './context.js'
18
- export { httpLogger } from './logger.js'
19
-
20
- type BsyncServiceState = 'initialized' | 'started' | 'destroyed'
21
-
22
- export class BsyncService {
23
- public ctx: AppContext
24
- public server: http.Server
25
- private terminator: httpTerminator.HttpTerminator
26
- private ac: AbortController
27
- private state: BsyncServiceState = 'initialized'
28
-
29
- constructor(opts: {
30
- ctx: AppContext
31
- server: http.Server
32
- ac: AbortController
33
- }) {
34
- this.ctx = opts.ctx
35
- this.server = opts.server
36
- this.ac = opts.ac
37
- this.terminator = httpTerminator.createHttpTerminator(opts)
38
- }
39
-
40
- static async create(
41
- cfg: ServerConfig,
42
- overrides?: Partial<AppContextOptions>,
43
- ): Promise<BsyncService> {
44
- const ac = new AbortController()
45
- const ctx = await AppContext.fromConfig(cfg, ac.signal, overrides)
46
- const handler = connectNodeAdapter({
47
- routes: routes(ctx),
48
- shutdownSignal: ac.signal,
49
- })
50
- const server = http.createServer((req, res) => {
51
- loggerMiddleware(req, res)
52
- if (isHealth(req.url)) {
53
- res.statusCode = 200
54
- res.setHeader('content-type', 'application/json')
55
- return res.end(JSON.stringify({ version: cfg.service.version }))
56
- }
57
- handler(req, res)
58
- })
59
- return new BsyncService({ ctx, server, ac })
60
- }
61
-
62
- async start(): Promise<http.Server> {
63
- if (this.state !== 'initialized') {
64
- throw new Error(`${this.constructor.name} already started`)
65
- }
66
- this.state = 'started'
67
-
68
- const dbStatsInterval = setInterval(() => {
69
- dbLogger.info(
70
- {
71
- idleCount: this.ctx.db.pool.idleCount,
72
- totalCount: this.ctx.db.pool.totalCount,
73
- waitingCount: this.ctx.db.pool.waitingCount,
74
- },
75
- 'db pool stats',
76
- )
77
- }, 10000)
78
-
79
- this.ac.signal.addEventListener('abort', () => {
80
- clearInterval(dbStatsInterval)
81
- })
82
-
83
- await this.setupAppEvents()
84
- this.server.listen(this.ctx.cfg.service.port)
85
- this.server.keepAliveTimeout = 90000
86
- await events.once(this.server, 'listening')
87
- return this.server
88
- }
89
-
90
- async destroy(): Promise<void> {
91
- if (this.state === 'destroyed') return
92
- this.state = 'destroyed'
93
- this.ac.abort()
94
- try {
95
- await this.terminator.terminate()
96
- } finally {
97
- await this.ctx.db.close()
98
- }
99
- }
100
-
101
- async setupAppEvents() {
102
- const conn = await this.ctx.db.pool.connect()
103
- this.ac.signal.addEventListener('abort', () => conn.release(), {
104
- once: true,
105
- })
106
- // if these error, unhandled rejection should cause process to exit
107
- conn.query(`listen ${createMuteOpChannel}`)
108
- conn.query(`listen ${createNotifOpChannel}`)
109
- conn.query(`listen ${createOperationChannel}`)
110
- conn.on('notification', (notif) => {
111
- if (notif.channel === createMuteOpChannel) {
112
- this.ctx.events.emit(createMuteOpChannel)
113
- }
114
- if (notif.channel === createNotifOpChannel) {
115
- this.ctx.events.emit(createNotifOpChannel)
116
- }
117
- if (notif.channel === createOperationChannel) {
118
- this.ctx.events.emit(createOperationChannel)
119
- }
120
- })
121
- }
122
- }
123
-
124
- export default BsyncService
125
-
126
- const isHealth = (urlStr: string | undefined) => {
127
- if (!urlStr) return false
128
- const url = new URL(urlStr, 'http://host')
129
- return url.pathname === '/_health'
130
- }
package/src/logger.ts DELETED
@@ -1,26 +0,0 @@
1
- import { type IncomingMessage } from 'node:http'
2
- import { pinoHttp, stdSerializers } from 'pino-http'
3
- import { obfuscateHeaders, subsystemLogger } from '@atproto/common'
4
-
5
- export const dbLogger: ReturnType<typeof subsystemLogger> =
6
- subsystemLogger('bsync:db')
7
- export const httpLogger: ReturnType<typeof subsystemLogger> =
8
- subsystemLogger('bsync')
9
-
10
- export const loggerMiddleware = pinoHttp({
11
- logger: httpLogger,
12
- redact: {
13
- paths: ['req.headers.authorization'],
14
- },
15
- serializers: {
16
- err: (err: unknown) => ({
17
- code: err?.['code'],
18
- message: err?.['message'],
19
- }),
20
- req: (req: IncomingMessage) => {
21
- const serialized = stdSerializers.req(req)
22
- const headers = obfuscateHeaders(serialized.headers)
23
- return { ...serialized, headers }
24
- },
25
- },
26
- })