@atproto/tap 0.1.2 → 0.2.0
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 +29 -0
- package/LICENSE.txt +1 -1
- package/dist/channel.d.ts +2 -1
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +23 -14
- package/dist/channel.js.map +1 -1
- package/dist/lex-indexer.d.ts.map +1 -1
- package/dist/lex-indexer.js +8 -6
- package/dist/lex-indexer.js.map +1 -1
- package/dist/types.d.ts +107 -258
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +33 -30
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +1 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +9 -0
- package/dist/util.js.map +1 -1
- package/package.json +8 -9
- package/src/channel.ts +28 -19
- package/src/lex-indexer.ts +14 -8
- package/src/types.ts +41 -37
- package/src/util.ts +9 -0
- package/tests/_util.ts +24 -1
- package/tests/channel.test.ts +40 -48
- package/tests/client.test.ts +6 -5
- package/tests/lex-indexer.test.ts +4 -1
- package/tests/simple-indexer.test.ts +1 -0
- package/tests/util.test.ts +1 -0
- package/tsconfig.json +4 -1
- package/tsconfig.tests.json +9 -0
- package/vitest.config.ts +5 -0
- package/jest.config.js +0 -10
package/src/channel.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ClientOptions } from 'ws'
|
|
2
|
-
import { Deferrable, createDeferrable
|
|
2
|
+
import { Deferrable, createDeferrable } from '@atproto/common'
|
|
3
|
+
import { lexParse } from '@atproto/lex'
|
|
3
4
|
import { WebSocketKeepAlive } from '@atproto/ws-client'
|
|
4
5
|
import { TapEvent, parseTapEvent } from './types'
|
|
5
|
-
import { formatAdminAuthHeader } from './util'
|
|
6
|
+
import { formatAdminAuthHeader, isCausedBySignal } from './util'
|
|
6
7
|
|
|
7
8
|
export interface HandlerOpts {
|
|
8
9
|
signal: AbortSignal
|
|
@@ -26,7 +27,7 @@ type BufferedAck = {
|
|
|
26
27
|
defer: Deferrable
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
export class TapChannel {
|
|
30
|
+
export class TapChannel implements AsyncDisposable {
|
|
30
31
|
private ws: WebSocketKeepAlive
|
|
31
32
|
private handler: TapHandler
|
|
32
33
|
|
|
@@ -94,38 +95,43 @@ export class TapChannel {
|
|
|
94
95
|
await this.sendAck(ack.id)
|
|
95
96
|
ack.defer.resolve()
|
|
96
97
|
this.bufferedAcks = this.bufferedAcks.slice(1)
|
|
97
|
-
} catch (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}),
|
|
98
|
+
} catch (cause) {
|
|
99
|
+
const error = new Error(
|
|
100
|
+
`failed to send ack for event ${this.bufferedAcks[0]}`,
|
|
101
|
+
{ cause },
|
|
102
102
|
)
|
|
103
|
+
this.handler.onError(error)
|
|
103
104
|
return
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
async start() {
|
|
110
|
+
this.abortController.signal.throwIfAborted()
|
|
109
111
|
try {
|
|
110
112
|
for await (const chunk of this.ws) {
|
|
111
113
|
await this.processWsEvent(chunk)
|
|
112
114
|
}
|
|
113
115
|
} catch (err) {
|
|
114
|
-
if (
|
|
115
|
-
this.destroyDefer.resolve()
|
|
116
|
-
} else {
|
|
116
|
+
if (!isCausedBySignal(err, this.abortController.signal)) {
|
|
117
117
|
throw err
|
|
118
118
|
}
|
|
119
|
+
} finally {
|
|
120
|
+
this.destroyDefer.resolve()
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
private async processWsEvent(chunk: Uint8Array) {
|
|
123
125
|
let evt: TapEvent
|
|
124
126
|
try {
|
|
125
|
-
const data = chunk.toString()
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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)
|
|
129
135
|
return
|
|
130
136
|
}
|
|
131
137
|
|
|
@@ -136,11 +142,10 @@ export class TapChannel {
|
|
|
136
142
|
await this.ackEvent(evt.id)
|
|
137
143
|
},
|
|
138
144
|
})
|
|
139
|
-
} catch (
|
|
145
|
+
} catch (cause) {
|
|
140
146
|
// Don't ack on error - let Tap retry
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
)
|
|
147
|
+
const error = new Error(`Failed to process event ${evt.id}`, { cause })
|
|
148
|
+
this.handler.onError(error)
|
|
144
149
|
return
|
|
145
150
|
}
|
|
146
151
|
}
|
|
@@ -149,4 +154,8 @@ export class TapChannel {
|
|
|
149
154
|
this.abortController.abort()
|
|
150
155
|
await this.destroyDefer.complete
|
|
151
156
|
}
|
|
157
|
+
|
|
158
|
+
async [Symbol.asyncDispose](): Promise<void> {
|
|
159
|
+
await this.destroy()
|
|
160
|
+
}
|
|
152
161
|
}
|
package/src/lex-indexer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Infer, Main, RecordSchema, getMain } from '@atproto/lex'
|
|
2
|
-
import {
|
|
2
|
+
import { AtUriString, NsidString } from '@atproto/syntax'
|
|
3
3
|
import { HandlerOpts, TapHandler } from './channel'
|
|
4
4
|
import { IdentityEvent, RecordEvent, TapEvent } from './types'
|
|
5
5
|
|
|
@@ -73,12 +73,15 @@ export class LexIndexer implements TapHandler {
|
|
|
73
73
|
private identityHandler: IdentityHandler | undefined
|
|
74
74
|
private errorHandler: ErrorHandler | undefined
|
|
75
75
|
|
|
76
|
-
private handlerKey(
|
|
76
|
+
private handlerKey(
|
|
77
|
+
collection: NsidString,
|
|
78
|
+
action: RecordEvent['action'],
|
|
79
|
+
): string {
|
|
77
80
|
return `${collection}:${action}`
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
private register<const T extends RecordSchema>(
|
|
81
|
-
action:
|
|
84
|
+
action: RecordEvent['action'],
|
|
82
85
|
ns: Main<T>,
|
|
83
86
|
handler: RecordHandler<Infer<T>>,
|
|
84
87
|
): this {
|
|
@@ -117,7 +120,8 @@ export class LexIndexer implements TapHandler {
|
|
|
117
120
|
handler: PutHandler<Infer<T>>,
|
|
118
121
|
): this {
|
|
119
122
|
this.register('create', ns, handler)
|
|
120
|
-
|
|
123
|
+
this.register('update', ns, handler)
|
|
124
|
+
return this
|
|
121
125
|
}
|
|
122
126
|
|
|
123
127
|
other(fn: UntypedHandler): this {
|
|
@@ -167,10 +171,12 @@ export class LexIndexer implements TapHandler {
|
|
|
167
171
|
}
|
|
168
172
|
|
|
169
173
|
if (action === 'create' || action === 'update') {
|
|
170
|
-
const match = registered.schema.
|
|
171
|
-
if (!match) {
|
|
172
|
-
const uriStr =
|
|
173
|
-
throw new Error(`Record validation failed for ${uriStr}
|
|
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
|
+
})
|
|
174
180
|
}
|
|
175
181
|
}
|
|
176
182
|
|
package/src/types.ts
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LexMap, LexValue, l } from '@atproto/lex'
|
|
2
|
+
import { DidString, HandleString, NsidString } from '@atproto/syntax'
|
|
2
3
|
|
|
3
|
-
export const recordEventDataSchema =
|
|
4
|
-
did:
|
|
5
|
-
rev:
|
|
6
|
-
collection:
|
|
7
|
-
rkey:
|
|
8
|
-
action:
|
|
9
|
-
record:
|
|
10
|
-
cid:
|
|
11
|
-
live:
|
|
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.unknownObject()),
|
|
11
|
+
cid: l.optional(l.string({ format: 'cid' })),
|
|
12
|
+
live: l.boolean(),
|
|
12
13
|
})
|
|
13
14
|
|
|
14
|
-
export const identityEventDataSchema =
|
|
15
|
-
did:
|
|
16
|
-
handle:
|
|
17
|
-
is_active:
|
|
18
|
-
status:
|
|
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([
|
|
19
20
|
'active',
|
|
20
21
|
'takendown',
|
|
21
22
|
'suspended',
|
|
@@ -24,29 +25,32 @@ export const identityEventDataSchema = z.object({
|
|
|
24
25
|
]),
|
|
25
26
|
})
|
|
26
27
|
|
|
27
|
-
export const recordEventSchema =
|
|
28
|
-
id:
|
|
29
|
-
type:
|
|
28
|
+
export const recordEventSchema = l.object({
|
|
29
|
+
id: l.integer(),
|
|
30
|
+
type: l.literal('record'),
|
|
30
31
|
record: recordEventDataSchema,
|
|
31
32
|
})
|
|
32
33
|
|
|
33
|
-
export const identityEventSchema =
|
|
34
|
-
id:
|
|
35
|
-
type:
|
|
34
|
+
export const identityEventSchema = l.object({
|
|
35
|
+
id: l.integer(),
|
|
36
|
+
type: l.literal('identity'),
|
|
36
37
|
identity: identityEventDataSchema,
|
|
37
38
|
})
|
|
38
39
|
|
|
39
|
-
export const tapEventSchema =
|
|
40
|
+
export const tapEventSchema = l.discriminatedUnion('type', [
|
|
41
|
+
recordEventSchema,
|
|
42
|
+
identityEventSchema,
|
|
43
|
+
])
|
|
40
44
|
|
|
41
45
|
export type RecordEvent = {
|
|
42
46
|
id: number
|
|
43
47
|
type: 'record'
|
|
44
48
|
action: 'create' | 'update' | 'delete'
|
|
45
|
-
did:
|
|
49
|
+
did: DidString
|
|
46
50
|
rev: string
|
|
47
|
-
collection:
|
|
51
|
+
collection: NsidString
|
|
48
52
|
rkey: string
|
|
49
|
-
record?:
|
|
53
|
+
record?: LexMap
|
|
50
54
|
cid?: string
|
|
51
55
|
live: boolean
|
|
52
56
|
}
|
|
@@ -54,8 +58,8 @@ export type RecordEvent = {
|
|
|
54
58
|
export type IdentityEvent = {
|
|
55
59
|
id: number
|
|
56
60
|
type: 'identity'
|
|
57
|
-
did:
|
|
58
|
-
handle:
|
|
61
|
+
did: DidString
|
|
62
|
+
handle: HandleString
|
|
59
63
|
isActive: boolean
|
|
60
64
|
status: RepoStatus
|
|
61
65
|
}
|
|
@@ -69,7 +73,7 @@ export type RepoStatus =
|
|
|
69
73
|
|
|
70
74
|
export type TapEvent = IdentityEvent | RecordEvent
|
|
71
75
|
|
|
72
|
-
export const parseTapEvent = (data:
|
|
76
|
+
export const parseTapEvent = (data: LexValue): TapEvent => {
|
|
73
77
|
const parsed = tapEventSchema.parse(data)
|
|
74
78
|
if (parsed.type === 'identity') {
|
|
75
79
|
return {
|
|
@@ -96,14 +100,14 @@ export const parseTapEvent = (data: unknown): TapEvent => {
|
|
|
96
100
|
}
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
export const repoInfoSchema =
|
|
100
|
-
did:
|
|
101
|
-
handle:
|
|
102
|
-
state:
|
|
103
|
-
rev:
|
|
104
|
-
records:
|
|
105
|
-
error:
|
|
106
|
-
retries:
|
|
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()),
|
|
107
111
|
})
|
|
108
112
|
|
|
109
|
-
export type RepoInfo =
|
|
113
|
+
export type RepoInfo = l.Infer<typeof repoInfoSchema>
|
package/src/util.ts
CHANGED
|
@@ -31,3 +31,12 @@ const timingSafeEqual = (a: string, b: string): boolean => {
|
|
|
31
31
|
}
|
|
32
32
|
return bufA.compare(bufB) === 0
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
export function isCausedBySignal(err: unknown, signal: AbortSignal) {
|
|
36
|
+
if (!signal.aborted) return false
|
|
37
|
+
if (signal.reason == null) return false // Ignore nullish reasons
|
|
38
|
+
return (
|
|
39
|
+
err === signal.reason ||
|
|
40
|
+
(err instanceof Error && err.cause === signal.reason)
|
|
41
|
+
)
|
|
42
|
+
}
|
package/tests/_util.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws'
|
|
1
2
|
import { HandlerOpts } from '../src/channel'
|
|
2
3
|
import { IdentityEvent, RecordEvent } from '../src/types'
|
|
3
4
|
|
|
@@ -25,7 +26,7 @@ export const createRecordEvent = (
|
|
|
25
26
|
rkey: 'abc123',
|
|
26
27
|
action: 'create',
|
|
27
28
|
record: { text: 'hello' },
|
|
28
|
-
cid: '
|
|
29
|
+
cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
|
|
29
30
|
live: true,
|
|
30
31
|
...overrides,
|
|
31
32
|
})
|
|
@@ -38,3 +39,25 @@ export const createIdentityEvent = (): IdentityEvent => ({
|
|
|
38
39
|
isActive: true,
|
|
39
40
|
status: 'active',
|
|
40
41
|
})
|
|
42
|
+
|
|
43
|
+
export async function createWebSocketServer() {
|
|
44
|
+
return new Promise<WebSocketServer & AsyncDisposable>((resolve, reject) => {
|
|
45
|
+
const server = new WebSocketServer({ port: 0 }, () => {
|
|
46
|
+
server.off('error', reject)
|
|
47
|
+
resolve(
|
|
48
|
+
Object.defineProperty(server, Symbol.asyncDispose, {
|
|
49
|
+
value: disposeWebSocketServer,
|
|
50
|
+
}) as WebSocketServer & AsyncDisposable,
|
|
51
|
+
)
|
|
52
|
+
}).once('error', reject)
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function disposeWebSocketServer(this: WebSocketServer) {
|
|
57
|
+
return new Promise<void>((resolve, reject) => {
|
|
58
|
+
this.close((err) => {
|
|
59
|
+
if (err) reject(err)
|
|
60
|
+
else resolve()
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
}
|
package/tests/channel.test.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { AddressInfo } from 'ws'
|
|
3
3
|
import { TapChannel, TapHandler } from '../src/channel'
|
|
4
4
|
import { TapEvent } from '../src/types'
|
|
5
|
+
import { createWebSocketServer } from './_util'
|
|
5
6
|
|
|
6
7
|
const createRecordEvent = (id: number) => ({
|
|
7
8
|
id,
|
|
@@ -13,7 +14,7 @@ const createRecordEvent = (id: number) => ({
|
|
|
13
14
|
rkey: 'abc123',
|
|
14
15
|
action: 'create' as const,
|
|
15
16
|
record: { text: 'hello' },
|
|
16
|
-
cid: '
|
|
17
|
+
cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
|
|
17
18
|
live: true,
|
|
18
19
|
},
|
|
19
20
|
})
|
|
@@ -32,8 +33,9 @@ const createIdentityEvent = (id: number) => ({
|
|
|
32
33
|
describe('TapChannel', () => {
|
|
33
34
|
describe('receiving events', () => {
|
|
34
35
|
it('receives and parses record events', async () => {
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
await using server = await createWebSocketServer()
|
|
37
|
+
|
|
38
|
+
const { port } = server.address() as AddressInfo
|
|
37
39
|
|
|
38
40
|
const receivedEvents: TapEvent[] = []
|
|
39
41
|
|
|
@@ -57,7 +59,7 @@ describe('TapChannel', () => {
|
|
|
57
59
|
},
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
await using channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
61
63
|
await channel.start()
|
|
62
64
|
|
|
63
65
|
expect(receivedEvents).toHaveLength(1)
|
|
@@ -67,13 +69,12 @@ describe('TapChannel', () => {
|
|
|
67
69
|
expect(receivedEvents[0].collection).toBe('com.example.post')
|
|
68
70
|
expect(receivedEvents[0].action).toBe('create')
|
|
69
71
|
}
|
|
70
|
-
|
|
71
|
-
server.close()
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
it('receives and parses identity events', async () => {
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
await using server = await createWebSocketServer()
|
|
76
|
+
|
|
77
|
+
const { port } = server.address() as AddressInfo
|
|
77
78
|
|
|
78
79
|
const receivedEvents: TapEvent[] = []
|
|
79
80
|
|
|
@@ -97,7 +98,7 @@ describe('TapChannel', () => {
|
|
|
97
98
|
},
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
|
|
101
|
+
await using channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
101
102
|
await channel.start()
|
|
102
103
|
|
|
103
104
|
expect(receivedEvents).toHaveLength(1)
|
|
@@ -107,15 +108,14 @@ describe('TapChannel', () => {
|
|
|
107
108
|
expect(receivedEvents[0].handle).toBe('alice.test')
|
|
108
109
|
expect(receivedEvents[0].status).toBe('active')
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
-
server.close()
|
|
112
111
|
})
|
|
113
112
|
})
|
|
114
113
|
|
|
115
114
|
describe('ack behavior', () => {
|
|
116
115
|
it('sends ack when handler calls ack()', async () => {
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
await using server = await createWebSocketServer()
|
|
117
|
+
|
|
118
|
+
const { port } = server.address() as AddressInfo
|
|
119
119
|
|
|
120
120
|
const receivedAcks: number[] = []
|
|
121
121
|
|
|
@@ -139,17 +139,16 @@ describe('TapChannel', () => {
|
|
|
139
139
|
},
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
await using channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
143
143
|
await channel.start()
|
|
144
144
|
|
|
145
145
|
expect(receivedAcks).toEqual([42])
|
|
146
|
-
|
|
147
|
-
server.close()
|
|
148
146
|
})
|
|
149
147
|
|
|
150
148
|
it('does not send ack if handler throws', async () => {
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
await using server = await createWebSocketServer()
|
|
150
|
+
|
|
151
|
+
const { port } = server.address() as AddressInfo
|
|
153
152
|
|
|
154
153
|
const receivedAcks: number[] = []
|
|
155
154
|
const errors: Error[] = []
|
|
@@ -175,19 +174,18 @@ describe('TapChannel', () => {
|
|
|
175
174
|
},
|
|
176
175
|
}
|
|
177
176
|
|
|
178
|
-
|
|
177
|
+
await using channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
179
178
|
await channel.start()
|
|
180
179
|
|
|
181
180
|
expect(receivedAcks).toHaveLength(0)
|
|
182
181
|
expect(errors).toHaveLength(1)
|
|
183
182
|
expect(errors[0].message).toContain('Failed to process event')
|
|
184
|
-
|
|
185
|
-
server.close()
|
|
186
183
|
})
|
|
187
184
|
|
|
188
185
|
it('does not send ack if handler does not call ack()', async () => {
|
|
189
|
-
|
|
190
|
-
|
|
186
|
+
await using server = await createWebSocketServer()
|
|
187
|
+
|
|
188
|
+
const { port } = server.address() as AddressInfo
|
|
191
189
|
|
|
192
190
|
const receivedAcks: number[] = []
|
|
193
191
|
|
|
@@ -212,17 +210,16 @@ describe('TapChannel', () => {
|
|
|
212
210
|
},
|
|
213
211
|
}
|
|
214
212
|
|
|
215
|
-
|
|
213
|
+
await using channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
216
214
|
await channel.start()
|
|
217
215
|
|
|
218
216
|
expect(receivedAcks).toHaveLength(0)
|
|
219
|
-
|
|
220
|
-
server.close()
|
|
221
217
|
})
|
|
222
218
|
|
|
223
219
|
it('handles reconnection and receives events from new connection', async () => {
|
|
224
|
-
|
|
225
|
-
|
|
220
|
+
await using server = await createWebSocketServer()
|
|
221
|
+
|
|
222
|
+
const { port } = server.address() as AddressInfo
|
|
226
223
|
|
|
227
224
|
const receivedEvents: TapEvent[] = []
|
|
228
225
|
const receivedAcks: number[] = []
|
|
@@ -256,7 +253,7 @@ describe('TapChannel', () => {
|
|
|
256
253
|
onError: () => {},
|
|
257
254
|
}
|
|
258
255
|
|
|
259
|
-
|
|
256
|
+
await using channel = new TapChannel(`ws://localhost:${port}`, handler, {
|
|
260
257
|
maxReconnectSeconds: 1,
|
|
261
258
|
})
|
|
262
259
|
|
|
@@ -269,15 +266,14 @@ describe('TapChannel', () => {
|
|
|
269
266
|
expect(receivedEvents[1].id).toBe(2)
|
|
270
267
|
expect(receivedAcks).toContain(1)
|
|
271
268
|
expect(receivedAcks).toContain(2)
|
|
272
|
-
|
|
273
|
-
server.close()
|
|
274
269
|
})
|
|
275
270
|
})
|
|
276
271
|
|
|
277
272
|
describe('multiple events', () => {
|
|
278
273
|
it('processes multiple events in sequence', async () => {
|
|
279
|
-
|
|
280
|
-
|
|
274
|
+
await using server = await createWebSocketServer()
|
|
275
|
+
|
|
276
|
+
const { port } = server.address() as AddressInfo
|
|
281
277
|
|
|
282
278
|
const receivedEvents: TapEvent[] = []
|
|
283
279
|
const receivedAcks: number[] = []
|
|
@@ -307,7 +303,7 @@ describe('TapChannel', () => {
|
|
|
307
303
|
},
|
|
308
304
|
}
|
|
309
305
|
|
|
310
|
-
|
|
306
|
+
await using channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
311
307
|
await channel.start()
|
|
312
308
|
|
|
313
309
|
expect(receivedEvents).toHaveLength(3)
|
|
@@ -315,15 +311,14 @@ describe('TapChannel', () => {
|
|
|
315
311
|
expect(receivedEvents[1].id).toBe(2)
|
|
316
312
|
expect(receivedEvents[2].id).toBe(3)
|
|
317
313
|
expect(receivedAcks).toEqual([1, 2, 3])
|
|
318
|
-
|
|
319
|
-
server.close()
|
|
320
314
|
})
|
|
321
315
|
})
|
|
322
316
|
|
|
323
317
|
describe('auth', () => {
|
|
324
318
|
it('includes auth header when adminPassword is provided', async () => {
|
|
325
|
-
|
|
326
|
-
|
|
319
|
+
await using server = await createWebSocketServer()
|
|
320
|
+
|
|
321
|
+
const { port } = server.address() as AddressInfo
|
|
327
322
|
|
|
328
323
|
let receivedAuthHeader: string | undefined
|
|
329
324
|
|
|
@@ -337,21 +332,20 @@ describe('TapChannel', () => {
|
|
|
337
332
|
onError: () => {},
|
|
338
333
|
}
|
|
339
334
|
|
|
340
|
-
|
|
335
|
+
await using channel = new TapChannel(`ws://localhost:${port}`, handler, {
|
|
341
336
|
adminPassword: 'secret',
|
|
342
337
|
})
|
|
343
338
|
await channel.start()
|
|
344
339
|
|
|
345
340
|
expect(receivedAuthHeader).toBe('Basic YWRtaW46c2VjcmV0')
|
|
346
|
-
|
|
347
|
-
server.close()
|
|
348
341
|
})
|
|
349
342
|
})
|
|
350
343
|
|
|
351
344
|
describe('error handling', () => {
|
|
352
345
|
it('calls onError for malformed messages', async () => {
|
|
353
|
-
|
|
354
|
-
|
|
346
|
+
await using server = await createWebSocketServer()
|
|
347
|
+
|
|
348
|
+
const { port } = server.address() as AddressInfo
|
|
355
349
|
|
|
356
350
|
const errors: Error[] = []
|
|
357
351
|
|
|
@@ -367,13 +361,11 @@ describe('TapChannel', () => {
|
|
|
367
361
|
},
|
|
368
362
|
}
|
|
369
363
|
|
|
370
|
-
|
|
364
|
+
await using channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
371
365
|
await channel.start()
|
|
372
366
|
|
|
373
367
|
expect(errors).toHaveLength(1)
|
|
374
368
|
expect(errors[0].message).toBe('Failed to parse message')
|
|
375
|
-
|
|
376
|
-
server.close()
|
|
377
369
|
})
|
|
378
370
|
})
|
|
379
371
|
})
|
package/tests/client.test.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { once } from 'node:events'
|
|
2
2
|
import * as http from 'node:http'
|
|
3
|
+
import { AddressInfo } from 'node:net'
|
|
3
4
|
import { default as express } from 'express'
|
|
4
|
-
import
|
|
5
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'
|
|
5
6
|
import { Tap } from '../src/client'
|
|
6
7
|
|
|
7
8
|
describe('Tap client', () => {
|
|
@@ -40,7 +41,6 @@ describe('Tap client', () => {
|
|
|
40
41
|
}[]
|
|
41
42
|
|
|
42
43
|
beforeAll(async () => {
|
|
43
|
-
const port = await getPort()
|
|
44
44
|
const app = express()
|
|
45
45
|
app.use(express.json())
|
|
46
46
|
|
|
@@ -99,8 +99,9 @@ describe('Tap client', () => {
|
|
|
99
99
|
})
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
-
server = app.listen(
|
|
102
|
+
server = app.listen()
|
|
103
103
|
await once(server, 'listening')
|
|
104
|
+
const { port } = server.address() as AddressInfo
|
|
104
105
|
tap = new Tap(`http://localhost:${port}`, { adminPassword: 'secret' })
|
|
105
106
|
})
|
|
106
107
|
|
|
@@ -172,7 +173,6 @@ describe('Tap client', () => {
|
|
|
172
173
|
let tap: Tap
|
|
173
174
|
|
|
174
175
|
beforeAll(async () => {
|
|
175
|
-
const port = await getPort()
|
|
176
176
|
const app = express()
|
|
177
177
|
app.use(express.json())
|
|
178
178
|
|
|
@@ -184,8 +184,9 @@ describe('Tap client', () => {
|
|
|
184
184
|
res.status(500).send('Internal Server Error')
|
|
185
185
|
})
|
|
186
186
|
|
|
187
|
-
server = app.listen(
|
|
187
|
+
server = app.listen()
|
|
188
188
|
await once(server, 'listening')
|
|
189
|
+
const { port } = server.address() as AddressInfo
|
|
189
190
|
tap = new Tap(`http://localhost:${port}`)
|
|
190
191
|
})
|
|
191
192
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
1
2
|
import { l } from '@atproto/lex'
|
|
2
3
|
import {
|
|
3
4
|
CreateEvent,
|
|
@@ -58,7 +59,9 @@ describe('LexIndexer', () => {
|
|
|
58
59
|
expect(received).toHaveLength(1)
|
|
59
60
|
expect(received[0].action).toBe('create')
|
|
60
61
|
expect(received[0].record.text).toBe('hello')
|
|
61
|
-
expect(received[0].cid).toBe(
|
|
62
|
+
expect(received[0].cid).toBe(
|
|
63
|
+
'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
|
|
64
|
+
)
|
|
62
65
|
})
|
|
63
66
|
|
|
64
67
|
it('registers update handler', async () => {
|
package/tests/util.test.ts
CHANGED
package/tsconfig.json
CHANGED