@atproto/tap 0.3.4 → 0.3.5

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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # @atproto/tap
2
2
 
3
+ ## 0.3.5
4
+
5
+ ### Patch Changes
6
+
7
+ - [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update TypeScript build to rely on references to composite internal projects
8
+
9
+ - [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Bundle only necessary files in the NPM tarball, including the `CHANGELOG.md` and `README.md` files (if present).
10
+
11
+ - [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build with `noImplicitAny` enabled
12
+
13
+ - Updated dependencies [[`28a0b58`](https://github.com/bluesky-social/atproto/commit/28a0b588147863eaef948cd2bb8fc0f19d08cda9), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07)]:
14
+ - @atproto/syntax@0.6.4
15
+ - @atproto/ws-client@0.1.4
16
+ - @atproto/lex@0.1.7
17
+ - @atproto/common@0.6.5
18
+
3
19
  ## 0.3.4
4
20
 
5
21
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/tap",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "license": "MIT",
5
5
  "description": "atproto tap client",
6
6
  "keywords": [
@@ -16,15 +16,27 @@
16
16
  "url": "https://github.com/bluesky-social/atproto",
17
17
  "directory": "packages/tap"
18
18
  },
19
+ "files": [
20
+ "./dist",
21
+ "./README.md",
22
+ "./CHANGELOG.md"
23
+ ],
24
+ "type": "module",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ }
30
+ },
19
31
  "engines": {
20
32
  "node": ">=22"
21
33
  },
22
34
  "dependencies": {
23
35
  "ws": "^8.12.0",
24
- "@atproto/common": "^0.6.4",
25
- "@atproto/lex": "^0.1.6",
26
- "@atproto/syntax": "^0.6.3",
27
- "@atproto/ws-client": "^0.1.3"
36
+ "@atproto/common": "^0.6.5",
37
+ "@atproto/syntax": "^0.6.4",
38
+ "@atproto/lex": "^0.1.7",
39
+ "@atproto/ws-client": "^0.1.4"
28
40
  },
29
41
  "devDependencies": {
30
42
  "@types/express": "^4.17.17",
@@ -34,13 +46,6 @@
34
46
  "http-terminator": "^3.2.0",
35
47
  "vitest": "^4.0.16"
36
48
  },
37
- "type": "module",
38
- "exports": {
39
- ".": {
40
- "types": "./dist/index.d.ts",
41
- "default": "./dist/index.js"
42
- }
43
- },
44
49
  "scripts": {
45
50
  "build": "tsgo --build tsconfig.build.json",
46
51
  "test": "vitest run"
package/src/channel.ts DELETED
@@ -1,161 +0,0 @@
1
- import type { ClientOptions } from 'ws'
2
- import { Deferrable, createDeferrable } from '@atproto/common'
3
- import { lexParse } from '@atproto/lex'
4
- import { WebSocketKeepAlive } from '@atproto/ws-client'
5
- import { TapEvent, parseTapEvent } from './types.js'
6
- import { formatAdminAuthHeader, isCausedBySignal } from './util.js'
7
-
8
- export interface HandlerOpts {
9
- signal: AbortSignal
10
- ack: () => Promise<void>
11
- }
12
-
13
- export interface TapHandler {
14
- onEvent: (evt: TapEvent, opts: HandlerOpts) => void | Promise<void>
15
- onError: (err: Error) => void
16
- }
17
-
18
- export type TapWebsocketOptions = ClientOptions & {
19
- adminPassword?: string
20
- maxReconnectSeconds?: number
21
- heartbeatIntervalMs?: number
22
- onReconnectError?: (error: unknown, n: number, initialSetup: boolean) => void
23
- }
24
-
25
- type BufferedAck = {
26
- id: number
27
- defer: Deferrable
28
- }
29
-
30
- export class TapChannel implements AsyncDisposable {
31
- private ws: WebSocketKeepAlive
32
- private handler: TapHandler
33
-
34
- private readonly abortController: AbortController = new AbortController()
35
- private readonly destroyDefer: Deferrable = createDeferrable()
36
-
37
- private bufferedAcks: BufferedAck[] = []
38
-
39
- constructor(
40
- url: string,
41
- handler: TapHandler,
42
- wsOpts: TapWebsocketOptions = {},
43
- ) {
44
- this.handler = handler
45
- const { adminPassword, ...rest } = wsOpts
46
- let headers = rest.headers
47
- if (adminPassword) {
48
- headers ??= {}
49
- headers['Authorization'] = formatAdminAuthHeader(adminPassword)
50
- }
51
- this.ws = new WebSocketKeepAlive({
52
- getUrl: async () => url,
53
- onReconnect: () => {
54
- this.flushBufferedAcks()
55
- },
56
- signal: this.abortController.signal,
57
- ...rest,
58
- headers,
59
- })
60
- }
61
-
62
- async ackEvent(id: number): Promise<void> {
63
- if (this.ws.isConnected()) {
64
- try {
65
- await this.sendAck(id)
66
- } catch {
67
- await this.bufferAndSendAck(id)
68
- }
69
- } else {
70
- await this.bufferAndSendAck(id)
71
- }
72
- }
73
-
74
- private async sendAck(id: number): Promise<void> {
75
- await this.ws.send(JSON.stringify({ type: 'ack', id }))
76
- }
77
-
78
- // resolves after the ack has been actually sent
79
- private async bufferAndSendAck(id: number): Promise<void> {
80
- const defer = createDeferrable()
81
- this.bufferedAcks.push({
82
- id,
83
- defer,
84
- })
85
- await defer.complete
86
- }
87
-
88
- private async flushBufferedAcks(): Promise<void> {
89
- while (this.bufferedAcks.length > 0) {
90
- try {
91
- const ack = this.bufferedAcks.at(0)
92
- if (!ack) {
93
- return
94
- }
95
- await this.sendAck(ack.id)
96
- ack.defer.resolve()
97
- this.bufferedAcks = this.bufferedAcks.slice(1)
98
- } catch (cause) {
99
- const error = new Error(
100
- `failed to send ack for event ${this.bufferedAcks[0]}`,
101
- { cause },
102
- )
103
- this.handler.onError(error)
104
- return
105
- }
106
- }
107
- }
108
-
109
- async start() {
110
- this.abortController.signal.throwIfAborted()
111
- try {
112
- for await (const chunk of this.ws) {
113
- await this.processWsEvent(chunk)
114
- }
115
- } catch (err) {
116
- if (!isCausedBySignal(err, this.abortController.signal)) {
117
- throw err
118
- }
119
- } finally {
120
- this.destroyDefer.resolve()
121
- }
122
- }
123
-
124
- private async processWsEvent(chunk: Uint8Array) {
125
- let evt: TapEvent
126
- try {
127
- const data = lexParse(chunk.toString(), {
128
- // Reject invalid CIDs and blobs
129
- strict: true,
130
- })
131
- evt = parseTapEvent(data)
132
- } catch (cause) {
133
- const error = new Error(`Failed to parse message`, { cause })
134
- this.handler.onError(error)
135
- return
136
- }
137
-
138
- try {
139
- await this.handler.onEvent(evt, {
140
- signal: this.abortController.signal,
141
- ack: async () => {
142
- await this.ackEvent(evt.id)
143
- },
144
- })
145
- } catch (cause) {
146
- // Don't ack on error - let Tap retry
147
- const error = new Error(`Failed to process event ${evt.id}`, { cause })
148
- this.handler.onError(error)
149
- return
150
- }
151
- }
152
-
153
- async destroy(): Promise<void> {
154
- this.abortController.abort()
155
- await this.destroyDefer.complete
156
- }
157
-
158
- async [Symbol.asyncDispose](): Promise<void> {
159
- await this.destroy()
160
- }
161
- }
package/src/client.ts DELETED
@@ -1,106 +0,0 @@
1
- import { DidDocument, didDocument } from '@atproto/common'
2
- import { TapChannel, TapHandler, TapWebsocketOptions } from './channel.js'
3
- import { RepoInfo, repoInfoSchema } from './types.js'
4
- import { formatAdminAuthHeader } from './util.js'
5
-
6
- export interface TapConfig {
7
- adminPassword?: string
8
- }
9
-
10
- export class Tap {
11
- url: string
12
- private adminPassword?: string
13
- private authHeader?: string
14
-
15
- private addReposUrl: URL
16
- private removeReposUrl: URL
17
-
18
- constructor(url: string, config: TapConfig = {}) {
19
- if (!url.startsWith('http://') && !url.startsWith('https://')) {
20
- throw new Error('Invalid URL, expected http:// or https://')
21
- }
22
- this.url = url
23
- this.adminPassword = config.adminPassword
24
- if (this.adminPassword) {
25
- this.authHeader = formatAdminAuthHeader(this.adminPassword)
26
- }
27
-
28
- this.addReposUrl = new URL('/repos/add', this.url)
29
- this.removeReposUrl = new URL('/repos/remove', this.url)
30
- }
31
-
32
- private getHeaders(): Record<string, string> {
33
- const headers: Record<string, string> = {
34
- 'Content-Type': 'application/json',
35
- }
36
- if (this.authHeader) {
37
- headers['Authorization'] = this.authHeader
38
- }
39
- return headers
40
- }
41
-
42
- channel(handler: TapHandler, opts?: TapWebsocketOptions): TapChannel {
43
- const url = new URL(this.url)
44
- url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
45
- url.pathname = '/channel'
46
- return new TapChannel(url.toString(), handler, {
47
- adminPassword: this.adminPassword,
48
- ...opts,
49
- })
50
- }
51
-
52
- async addRepos(dids: string[]): Promise<void> {
53
- const response = await fetch(this.addReposUrl, {
54
- method: 'POST',
55
- headers: this.getHeaders(),
56
- body: JSON.stringify({ dids }),
57
- })
58
- await response.body?.cancel() // expect empty body
59
-
60
- if (!response.ok) {
61
- throw new Error(`Failed to add repos: ${response.statusText}`)
62
- }
63
- }
64
-
65
- async removeRepos(dids: string[]): Promise<void> {
66
- const response = await fetch(this.removeReposUrl, {
67
- method: 'POST',
68
- headers: this.getHeaders(),
69
- body: JSON.stringify({ dids }),
70
- })
71
- await response.body?.cancel() // expect empty body
72
-
73
- if (!response.ok) {
74
- throw new Error(`Failed to remove repos: ${response.statusText}`)
75
- }
76
- }
77
-
78
- async resolveDid(did: string): Promise<DidDocument | null> {
79
- const response = await fetch(new URL(`/resolve/${did}`, this.url), {
80
- method: 'GET',
81
- headers: this.getHeaders(),
82
- })
83
-
84
- if (response.status === 404) {
85
- return null
86
- } else if (!response.ok) {
87
- await response.body?.cancel()
88
- throw new Error(`Failed to resolve DID: ${response.statusText}`)
89
- }
90
- return didDocument.parse(await response.json())
91
- }
92
-
93
- async getRepoInfo(did: string): Promise<RepoInfo> {
94
- const response = await fetch(new URL(`/info/${did}`, this.url), {
95
- method: 'GET',
96
- headers: this.getHeaders(),
97
- })
98
-
99
- if (!response.ok) {
100
- await response.body?.cancel()
101
- throw new Error(`Failed to get repo info: ${response.statusText}`)
102
- }
103
-
104
- return repoInfoSchema.parse(await response.json())
105
- }
106
- }
package/src/index.ts DELETED
@@ -1,6 +0,0 @@
1
- export * from './types.js'
2
- export * from './client.js'
3
- export * from './channel.js'
4
- export * from './simple-indexer.js'
5
- export * from './lex-indexer.js'
6
- export * from './util.js'
@@ -1,193 +0,0 @@
1
- import { Infer, Main, RecordSchema, getMain } from '@atproto/lex'
2
- import { AtUriString, NsidString } from '@atproto/syntax'
3
- import { HandlerOpts, TapHandler } from './channel.js'
4
- import { IdentityEvent, RecordEvent, TapEvent } from './types.js'
5
-
6
- type BaseRecordEvent = Omit<RecordEvent, 'record' | 'action' | 'cid'>
7
-
8
- export type CreateEvent<R> = BaseRecordEvent & {
9
- action: 'create'
10
- record: R
11
- cid: string
12
- }
13
-
14
- export type UpdateEvent<R> = BaseRecordEvent & {
15
- action: 'update'
16
- record: R
17
- cid: string
18
- }
19
-
20
- export type PutEvent<R> = CreateEvent<R> | UpdateEvent<R>
21
-
22
- export type DeleteEvent = BaseRecordEvent & {
23
- action: 'delete'
24
- }
25
-
26
- export type CreateHandler<R> = (
27
- evt: CreateEvent<R>,
28
- opts: HandlerOpts,
29
- ) => Promise<void>
30
-
31
- export type UpdateHandler<R> = (
32
- evt: UpdateEvent<R>,
33
- opts: HandlerOpts,
34
- ) => Promise<void>
35
-
36
- export type PutHandler<R> = (
37
- evt: PutEvent<R>,
38
- opts: HandlerOpts,
39
- ) => Promise<void>
40
-
41
- export type DeleteHandler = (
42
- evt: DeleteEvent,
43
- opts: HandlerOpts,
44
- ) => Promise<void>
45
-
46
- export type UntypedHandler = (
47
- evt: RecordEvent,
48
- opts: HandlerOpts,
49
- ) => Promise<void>
50
-
51
- export type IdentityHandler = (
52
- evt: IdentityEvent,
53
- opts: HandlerOpts,
54
- ) => Promise<void>
55
-
56
- export type ErrorHandler = (err: Error) => void
57
-
58
- export type RecordHandler<R> =
59
- | CreateHandler<R>
60
- | UpdateHandler<R>
61
- | PutHandler<R>
62
- | DeleteHandler
63
-
64
- interface RegisteredHandler {
65
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
- handler: RecordHandler<any>
67
- schema: RecordSchema
68
- }
69
-
70
- export class LexIndexer implements TapHandler {
71
- private handlers = new Map<string, RegisteredHandler>()
72
- private otherHandler: UntypedHandler | undefined
73
- private identityHandler: IdentityHandler | undefined
74
- private errorHandler: ErrorHandler | undefined
75
-
76
- private handlerKey(
77
- collection: NsidString,
78
- action: RecordEvent['action'],
79
- ): string {
80
- return `${collection}:${action}`
81
- }
82
-
83
- private register<const T extends RecordSchema>(
84
- action: RecordEvent['action'],
85
- ns: Main<T>,
86
- handler: RecordHandler<Infer<T>>,
87
- ): this {
88
- const schema = getMain(ns)
89
- const key = this.handlerKey(schema.$type, action)
90
- if (this.handlers.has(key)) {
91
- throw new Error(`Handler already registered for ${key}`)
92
- }
93
- this.handlers.set(key, { schema, handler })
94
- return this
95
- }
96
-
97
- create<const T extends RecordSchema>(
98
- ns: Main<T>,
99
- handler: CreateHandler<Infer<T>>,
100
- ): this {
101
- return this.register('create', ns, handler)
102
- }
103
-
104
- update<const T extends RecordSchema>(
105
- ns: Main<T>,
106
- handler: UpdateHandler<Infer<T>>,
107
- ): this {
108
- return this.register('update', ns, handler)
109
- }
110
-
111
- delete<const T extends RecordSchema>(
112
- ns: Main<T>,
113
- handler: DeleteHandler,
114
- ): this {
115
- return this.register('delete', ns, handler)
116
- }
117
-
118
- put<const T extends RecordSchema>(
119
- ns: Main<T>,
120
- handler: PutHandler<Infer<T>>,
121
- ): this {
122
- this.register('create', ns, handler)
123
- this.register('update', ns, handler)
124
- return this
125
- }
126
-
127
- other(fn: UntypedHandler): this {
128
- if (this.otherHandler) {
129
- throw new Error(`Handler already registered for "other"`)
130
- }
131
- this.otherHandler = fn
132
- return this
133
- }
134
-
135
- identity(fn: IdentityHandler): this {
136
- if (this.identityHandler) {
137
- throw new Error(`Handler already registered for "identity"`)
138
- }
139
- this.identityHandler = fn
140
- return this
141
- }
142
-
143
- error(fn: ErrorHandler): this {
144
- if (this.errorHandler) {
145
- throw new Error(`Handler already registered for "error"`)
146
- }
147
- this.errorHandler = fn
148
- return this
149
- }
150
-
151
- async onEvent(evt: TapEvent, opts: HandlerOpts): Promise<void> {
152
- if (evt.type === 'identity') {
153
- await this.identityHandler?.(evt, opts)
154
- } else {
155
- await this.handleRecordEvent(evt, opts)
156
- }
157
- await opts.ack()
158
- }
159
-
160
- private async handleRecordEvent(
161
- evt: RecordEvent,
162
- opts: HandlerOpts,
163
- ): Promise<void> {
164
- const { collection, action } = evt
165
- const key = this.handlerKey(collection, action)
166
- const registered = this.handlers.get(key)
167
-
168
- if (!registered) {
169
- await this.otherHandler?.(evt, opts)
170
- return
171
- }
172
-
173
- if (action === 'create' || action === 'update') {
174
- const match = registered.schema.safeValidate(evt.record)
175
- if (!match.success) {
176
- const uriStr: AtUriString = `at://${evt.did}/${evt.collection}/${evt.rkey}`
177
- throw new Error(`Record validation failed for ${uriStr}`, {
178
- cause: match.reason,
179
- })
180
- }
181
- }
182
-
183
- await (registered.handler as UntypedHandler)(evt, opts)
184
- }
185
-
186
- onError(err: Error): void {
187
- if (this.errorHandler) {
188
- this.errorHandler(err)
189
- } else {
190
- throw err
191
- }
192
- }
193
- }
@@ -1,52 +0,0 @@
1
- import { HandlerOpts, TapHandler } from './channel.js'
2
- import { IdentityEvent, RecordEvent, TapEvent } from './types.js'
3
-
4
- type IdentityEventHandler = (
5
- evt: IdentityEvent,
6
- opts?: HandlerOpts,
7
- ) => Promise<void>
8
-
9
- type RecordEventHandler = (
10
- evt: RecordEvent,
11
- opts?: HandlerOpts,
12
- ) => Promise<void>
13
-
14
- type ErrorHandler = (err: Error) => void
15
-
16
- export class SimpleIndexer implements TapHandler {
17
- private identityHandler: IdentityEventHandler | undefined
18
- private recordHandler: RecordEventHandler | undefined
19
- private errorHandler: ErrorHandler | undefined
20
-
21
- identity(fn: IdentityEventHandler): this {
22
- this.identityHandler = fn
23
- return this
24
- }
25
-
26
- record(fn: RecordEventHandler): this {
27
- this.recordHandler = fn
28
- return this
29
- }
30
-
31
- error(fn: ErrorHandler): this {
32
- this.errorHandler = fn
33
- return this
34
- }
35
-
36
- async onEvent(evt: TapEvent, opts: HandlerOpts): Promise<void> {
37
- if (evt.type === 'record') {
38
- await this.recordHandler?.(evt, opts)
39
- } else {
40
- await this.identityHandler?.(evt, opts)
41
- }
42
- await opts.ack()
43
- }
44
-
45
- onError(err: Error) {
46
- if (this.errorHandler) {
47
- this.errorHandler(err)
48
- } else {
49
- throw err
50
- }
51
- }
52
- }
package/src/types.ts DELETED
@@ -1,113 +0,0 @@
1
- import { LexMap, LexValue, l } from '@atproto/lex'
2
- import { DidString, HandleString, NsidString } from '@atproto/syntax'
3
-
4
- export const recordEventDataSchema = l.object({
5
- did: l.string({ format: 'did' }),
6
- rev: l.string(),
7
- collection: l.string({ format: 'nsid' }),
8
- rkey: l.string({ format: 'record-key' }),
9
- action: l.enum(['create', 'update', 'delete']),
10
- record: l.optional(l.lexMap()),
11
- cid: l.optional(l.string({ format: 'cid' })),
12
- live: l.boolean(),
13
- })
14
-
15
- export const identityEventDataSchema = l.object({
16
- did: l.string({ format: 'did' }),
17
- handle: l.string({ format: 'handle' }),
18
- is_active: l.boolean(),
19
- status: l.enum([
20
- 'active',
21
- 'takendown',
22
- 'suspended',
23
- 'deactivated',
24
- 'deleted',
25
- ]),
26
- })
27
-
28
- export const recordEventSchema = l.object({
29
- id: l.integer(),
30
- type: l.literal('record'),
31
- record: recordEventDataSchema,
32
- })
33
-
34
- export const identityEventSchema = l.object({
35
- id: l.integer(),
36
- type: l.literal('identity'),
37
- identity: identityEventDataSchema,
38
- })
39
-
40
- export const tapEventSchema = l.discriminatedUnion('type', [
41
- recordEventSchema,
42
- identityEventSchema,
43
- ])
44
-
45
- export type RecordEvent = {
46
- id: number
47
- type: 'record'
48
- action: 'create' | 'update' | 'delete'
49
- did: DidString
50
- rev: string
51
- collection: NsidString
52
- rkey: string
53
- record?: LexMap
54
- cid?: string
55
- live: boolean
56
- }
57
-
58
- export type IdentityEvent = {
59
- id: number
60
- type: 'identity'
61
- did: DidString
62
- handle: HandleString
63
- isActive: boolean
64
- status: RepoStatus
65
- }
66
-
67
- export type RepoStatus =
68
- | 'active'
69
- | 'takendown'
70
- | 'suspended'
71
- | 'deactivated'
72
- | 'deleted'
73
-
74
- export type TapEvent = IdentityEvent | RecordEvent
75
-
76
- export const parseTapEvent = (data: LexValue): TapEvent => {
77
- const parsed = tapEventSchema.parse(data)
78
- if (parsed.type === 'identity') {
79
- return {
80
- id: parsed.id,
81
- type: parsed.type,
82
- did: parsed.identity.did,
83
- handle: parsed.identity.handle,
84
- isActive: parsed.identity.is_active,
85
- status: parsed.identity.status,
86
- }
87
- } else {
88
- return {
89
- id: parsed.id,
90
- type: parsed.type,
91
- action: parsed.record.action,
92
- did: parsed.record.did,
93
- rev: parsed.record.rev,
94
- collection: parsed.record.collection,
95
- rkey: parsed.record.rkey,
96
- record: parsed.record.record,
97
- cid: parsed.record.cid,
98
- live: parsed.record.live,
99
- }
100
- }
101
- }
102
-
103
- export const repoInfoSchema = l.object({
104
- did: l.string(),
105
- handle: l.string(),
106
- state: l.string(),
107
- rev: l.string(),
108
- records: l.integer(),
109
- error: l.optional(l.string()),
110
- retries: l.optional(l.integer()),
111
- })
112
-
113
- export type RepoInfo = l.Infer<typeof repoInfoSchema>