@atproto/tap 0.3.3 → 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 +23 -0
- package/package.json +17 -12
- package/src/channel.ts +0 -161
- package/src/client.ts +0 -106
- package/src/index.ts +0 -6
- package/src/lex-indexer.ts +0 -193
- package/src/simple-indexer.ts +0 -52
- package/src/types.ts +0 -113
- package/src/util.ts +0 -42
- package/tests/_util.ts +0 -63
- package/tests/channel.test.ts +0 -371
- package/tests/client.test.ts +0 -207
- package/tests/lex-indexer.test.ts +0 -343
- package/tests/simple-indexer.test.ts +0 -161
- package/tests/util.test.ts +0 -89
- package/tsconfig.build.json +0 -9
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -8
- package/vitest.config.ts +0 -5
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>
|
package/src/util.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
export const formatAdminAuthHeader = (password: string) => {
|
|
2
|
-
return 'Basic ' + Buffer.from(`admin:${password}`).toString('base64')
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export const parseAdminAuthHeader = (header: string) => {
|
|
6
|
-
const noPrefix = header.startsWith('Basic ') ? header.slice(6) : header
|
|
7
|
-
const [username, password] = Buffer.from(noPrefix, 'base64')
|
|
8
|
-
.toString()
|
|
9
|
-
.split(':')
|
|
10
|
-
if (username !== 'admin') {
|
|
11
|
-
throw new Error("Unexpected username in admin headers. Expected 'admin'")
|
|
12
|
-
}
|
|
13
|
-
return password
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const assureAdminAuth = (expectedPassword: string, header: string) => {
|
|
17
|
-
const headerPassword = parseAdminAuthHeader(header)
|
|
18
|
-
const passEqual = timingSafeEqual(headerPassword, expectedPassword)
|
|
19
|
-
if (!passEqual) {
|
|
20
|
-
throw new Error('Invalid admin password')
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const timingSafeEqual = (a: string, b: string): boolean => {
|
|
25
|
-
const bufA = Buffer.from(a)
|
|
26
|
-
const bufB = Buffer.from(b)
|
|
27
|
-
if (bufA.length !== bufB.length) {
|
|
28
|
-
// Compare against self to maintain constant time even with length mismatch
|
|
29
|
-
Buffer.from(a).compare(Buffer.from(a))
|
|
30
|
-
return false
|
|
31
|
-
}
|
|
32
|
-
return bufA.compare(bufB) === 0
|
|
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
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { WebSocketServer } from 'ws'
|
|
2
|
-
import { HandlerOpts } from '../src/channel.js'
|
|
3
|
-
import { IdentityEvent, RecordEvent } from '../src/types.js'
|
|
4
|
-
|
|
5
|
-
export type MockOpts = HandlerOpts & { acked: boolean }
|
|
6
|
-
|
|
7
|
-
export const createMockOpts = (): MockOpts => {
|
|
8
|
-
const opts = {
|
|
9
|
-
signal: new AbortController().signal,
|
|
10
|
-
acked: false,
|
|
11
|
-
ack: async () => {
|
|
12
|
-
opts.acked = true
|
|
13
|
-
},
|
|
14
|
-
}
|
|
15
|
-
return opts
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const createRecordEvent = (
|
|
19
|
-
overrides: Partial<RecordEvent> = {},
|
|
20
|
-
): RecordEvent => ({
|
|
21
|
-
id: 1,
|
|
22
|
-
type: 'record',
|
|
23
|
-
did: 'did:example:alice',
|
|
24
|
-
rev: 'abc123',
|
|
25
|
-
collection: 'com.example.post',
|
|
26
|
-
rkey: 'abc123',
|
|
27
|
-
action: 'create',
|
|
28
|
-
record: { text: 'hello' },
|
|
29
|
-
cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
|
|
30
|
-
live: true,
|
|
31
|
-
...overrides,
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
export const createIdentityEvent = (): IdentityEvent => ({
|
|
35
|
-
id: 2,
|
|
36
|
-
type: 'identity',
|
|
37
|
-
did: 'did:example:alice',
|
|
38
|
-
handle: 'alice.test',
|
|
39
|
-
isActive: true,
|
|
40
|
-
status: 'active',
|
|
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
DELETED
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { AddressInfo } from 'ws'
|
|
3
|
-
import { TapChannel, TapHandler } from '../src/channel.js'
|
|
4
|
-
import { TapEvent } from '../src/types.js'
|
|
5
|
-
import { createWebSocketServer } from './_util.js'
|
|
6
|
-
|
|
7
|
-
const createRecordEvent = (id: number) => ({
|
|
8
|
-
id,
|
|
9
|
-
type: 'record' as const,
|
|
10
|
-
record: {
|
|
11
|
-
did: 'did:example:alice',
|
|
12
|
-
rev: '3abc123',
|
|
13
|
-
collection: 'com.example.post',
|
|
14
|
-
rkey: 'abc123',
|
|
15
|
-
action: 'create' as const,
|
|
16
|
-
record: { text: 'hello' },
|
|
17
|
-
cid: 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
|
|
18
|
-
live: true,
|
|
19
|
-
},
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
const createIdentityEvent = (id: number) => ({
|
|
23
|
-
id,
|
|
24
|
-
type: 'identity' as const,
|
|
25
|
-
identity: {
|
|
26
|
-
did: 'did:example:alice',
|
|
27
|
-
handle: 'alice.test',
|
|
28
|
-
is_active: true,
|
|
29
|
-
status: 'active' as const,
|
|
30
|
-
},
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
describe('TapChannel', () => {
|
|
34
|
-
describe('receiving events', () => {
|
|
35
|
-
it('receives and parses record events', async () => {
|
|
36
|
-
await using server = await createWebSocketServer()
|
|
37
|
-
|
|
38
|
-
const { port } = server.address() as AddressInfo
|
|
39
|
-
|
|
40
|
-
const receivedEvents: TapEvent[] = []
|
|
41
|
-
|
|
42
|
-
server.on('connection', (socket) => {
|
|
43
|
-
socket.send(JSON.stringify(createRecordEvent(1)))
|
|
44
|
-
socket.on('message', (data) => {
|
|
45
|
-
const msg = JSON.parse(data.toString())
|
|
46
|
-
if (msg.type === 'ack') {
|
|
47
|
-
socket.close()
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
const handler: TapHandler = {
|
|
53
|
-
onEvent: async (evt, opts) => {
|
|
54
|
-
receivedEvents.push(evt)
|
|
55
|
-
await opts.ack()
|
|
56
|
-
},
|
|
57
|
-
onError: (err) => {
|
|
58
|
-
throw err
|
|
59
|
-
},
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
await using channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
63
|
-
await channel.start()
|
|
64
|
-
|
|
65
|
-
expect(receivedEvents).toHaveLength(1)
|
|
66
|
-
expect(receivedEvents[0].type).toBe('record')
|
|
67
|
-
expect(receivedEvents[0].did).toBe('did:example:alice')
|
|
68
|
-
if (receivedEvents[0].type === 'record') {
|
|
69
|
-
expect(receivedEvents[0].collection).toBe('com.example.post')
|
|
70
|
-
expect(receivedEvents[0].action).toBe('create')
|
|
71
|
-
}
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('receives and parses identity events', async () => {
|
|
75
|
-
await using server = await createWebSocketServer()
|
|
76
|
-
|
|
77
|
-
const { port } = server.address() as AddressInfo
|
|
78
|
-
|
|
79
|
-
const receivedEvents: TapEvent[] = []
|
|
80
|
-
|
|
81
|
-
server.on('connection', (socket) => {
|
|
82
|
-
socket.send(JSON.stringify(createIdentityEvent(1)))
|
|
83
|
-
socket.on('message', (data) => {
|
|
84
|
-
const msg = JSON.parse(data.toString())
|
|
85
|
-
if (msg.type === 'ack') {
|
|
86
|
-
socket.close()
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
const handler: TapHandler = {
|
|
92
|
-
onEvent: async (evt, opts) => {
|
|
93
|
-
receivedEvents.push(evt)
|
|
94
|
-
await opts.ack()
|
|
95
|
-
},
|
|
96
|
-
onError: (err) => {
|
|
97
|
-
throw err
|
|
98
|
-
},
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
await using channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
102
|
-
await channel.start()
|
|
103
|
-
|
|
104
|
-
expect(receivedEvents).toHaveLength(1)
|
|
105
|
-
expect(receivedEvents[0].type).toBe('identity')
|
|
106
|
-
expect(receivedEvents[0].did).toBe('did:example:alice')
|
|
107
|
-
if (receivedEvents[0].type === 'identity') {
|
|
108
|
-
expect(receivedEvents[0].handle).toBe('alice.test')
|
|
109
|
-
expect(receivedEvents[0].status).toBe('active')
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
describe('ack behavior', () => {
|
|
115
|
-
it('sends ack when handler calls ack()', async () => {
|
|
116
|
-
await using server = await createWebSocketServer()
|
|
117
|
-
|
|
118
|
-
const { port } = server.address() as AddressInfo
|
|
119
|
-
|
|
120
|
-
const receivedAcks: number[] = []
|
|
121
|
-
|
|
122
|
-
server.on('connection', (socket) => {
|
|
123
|
-
socket.send(JSON.stringify(createRecordEvent(42)))
|
|
124
|
-
socket.on('message', (data) => {
|
|
125
|
-
const msg = JSON.parse(data.toString())
|
|
126
|
-
if (msg.type === 'ack') {
|
|
127
|
-
receivedAcks.push(msg.id)
|
|
128
|
-
socket.close()
|
|
129
|
-
}
|
|
130
|
-
})
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
const handler: TapHandler = {
|
|
134
|
-
onEvent: async (_evt, opts) => {
|
|
135
|
-
await opts.ack()
|
|
136
|
-
},
|
|
137
|
-
onError: (err) => {
|
|
138
|
-
throw err
|
|
139
|
-
},
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
await using channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
143
|
-
await channel.start()
|
|
144
|
-
|
|
145
|
-
expect(receivedAcks).toEqual([42])
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('does not send ack if handler throws', async () => {
|
|
149
|
-
await using server = await createWebSocketServer()
|
|
150
|
-
|
|
151
|
-
const { port } = server.address() as AddressInfo
|
|
152
|
-
|
|
153
|
-
const receivedAcks: number[] = []
|
|
154
|
-
const errors: Error[] = []
|
|
155
|
-
|
|
156
|
-
server.on('connection', (socket) => {
|
|
157
|
-
socket.send(JSON.stringify(createRecordEvent(1)))
|
|
158
|
-
socket.on('message', (data) => {
|
|
159
|
-
const msg = JSON.parse(data.toString())
|
|
160
|
-
if (msg.type === 'ack') {
|
|
161
|
-
receivedAcks.push(msg.id)
|
|
162
|
-
}
|
|
163
|
-
})
|
|
164
|
-
// Close after a short delay to let error propagate
|
|
165
|
-
setTimeout(() => socket.close(), 100)
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
const handler: TapHandler = {
|
|
169
|
-
onEvent: async () => {
|
|
170
|
-
throw new Error('Handler failed')
|
|
171
|
-
},
|
|
172
|
-
onError: (err) => {
|
|
173
|
-
errors.push(err)
|
|
174
|
-
},
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
await using channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
178
|
-
await channel.start()
|
|
179
|
-
|
|
180
|
-
expect(receivedAcks).toHaveLength(0)
|
|
181
|
-
expect(errors).toHaveLength(1)
|
|
182
|
-
expect(errors[0].message).toContain('Failed to process event')
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it('does not send ack if handler does not call ack()', async () => {
|
|
186
|
-
await using server = await createWebSocketServer()
|
|
187
|
-
|
|
188
|
-
const { port } = server.address() as AddressInfo
|
|
189
|
-
|
|
190
|
-
const receivedAcks: number[] = []
|
|
191
|
-
|
|
192
|
-
server.on('connection', (socket) => {
|
|
193
|
-
socket.send(JSON.stringify(createRecordEvent(1)))
|
|
194
|
-
socket.on('message', (data) => {
|
|
195
|
-
const msg = JSON.parse(data.toString())
|
|
196
|
-
if (msg.type === 'ack') {
|
|
197
|
-
receivedAcks.push(msg.id)
|
|
198
|
-
}
|
|
199
|
-
})
|
|
200
|
-
// Close after a short delay
|
|
201
|
-
setTimeout(() => socket.close(), 100)
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
const handler: TapHandler = {
|
|
205
|
-
onEvent: async () => {
|
|
206
|
-
// Don't call ack
|
|
207
|
-
},
|
|
208
|
-
onError: (err) => {
|
|
209
|
-
throw err
|
|
210
|
-
},
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
await using channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
214
|
-
await channel.start()
|
|
215
|
-
|
|
216
|
-
expect(receivedAcks).toHaveLength(0)
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
it('handles reconnection and receives events from new connection', async () => {
|
|
220
|
-
await using server = await createWebSocketServer()
|
|
221
|
-
|
|
222
|
-
const { port } = server.address() as AddressInfo
|
|
223
|
-
|
|
224
|
-
const receivedEvents: TapEvent[] = []
|
|
225
|
-
const receivedAcks: number[] = []
|
|
226
|
-
let connectionCount = 0
|
|
227
|
-
|
|
228
|
-
server.on('connection', (socket) => {
|
|
229
|
-
connectionCount++
|
|
230
|
-
// Send a different event each connection
|
|
231
|
-
const eventId = connectionCount
|
|
232
|
-
socket.send(JSON.stringify(createRecordEvent(eventId)))
|
|
233
|
-
socket.on('message', (data) => {
|
|
234
|
-
const msg = JSON.parse(data.toString())
|
|
235
|
-
if (msg.type === 'ack') {
|
|
236
|
-
receivedAcks.push(msg.id)
|
|
237
|
-
if (connectionCount === 1) {
|
|
238
|
-
// After first ack, terminate to trigger reconnect
|
|
239
|
-
socket.terminate()
|
|
240
|
-
} else {
|
|
241
|
-
// After second ack, close cleanly
|
|
242
|
-
socket.close()
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
})
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
const handler: TapHandler = {
|
|
249
|
-
onEvent: async (evt, opts) => {
|
|
250
|
-
receivedEvents.push(evt)
|
|
251
|
-
await opts.ack()
|
|
252
|
-
},
|
|
253
|
-
onError: () => {},
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
await using channel = new TapChannel(`ws://localhost:${port}`, handler, {
|
|
257
|
-
maxReconnectSeconds: 1,
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
await channel.start()
|
|
261
|
-
|
|
262
|
-
// Should have connected twice and received two events
|
|
263
|
-
expect(connectionCount).toBe(2)
|
|
264
|
-
expect(receivedEvents).toHaveLength(2)
|
|
265
|
-
expect(receivedEvents[0].id).toBe(1)
|
|
266
|
-
expect(receivedEvents[1].id).toBe(2)
|
|
267
|
-
expect(receivedAcks).toContain(1)
|
|
268
|
-
expect(receivedAcks).toContain(2)
|
|
269
|
-
})
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
describe('multiple events', () => {
|
|
273
|
-
it('processes multiple events in sequence', async () => {
|
|
274
|
-
await using server = await createWebSocketServer()
|
|
275
|
-
|
|
276
|
-
const { port } = server.address() as AddressInfo
|
|
277
|
-
|
|
278
|
-
const receivedEvents: TapEvent[] = []
|
|
279
|
-
const receivedAcks: number[] = []
|
|
280
|
-
|
|
281
|
-
server.on('connection', (socket) => {
|
|
282
|
-
socket.send(JSON.stringify(createRecordEvent(1)))
|
|
283
|
-
socket.send(JSON.stringify(createRecordEvent(2)))
|
|
284
|
-
socket.send(JSON.stringify(createIdentityEvent(3)))
|
|
285
|
-
socket.on('message', (data) => {
|
|
286
|
-
const msg = JSON.parse(data.toString())
|
|
287
|
-
if (msg.type === 'ack') {
|
|
288
|
-
receivedAcks.push(msg.id)
|
|
289
|
-
if (receivedAcks.length === 3) {
|
|
290
|
-
socket.close()
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
})
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
const handler: TapHandler = {
|
|
297
|
-
onEvent: async (evt, opts) => {
|
|
298
|
-
receivedEvents.push(evt)
|
|
299
|
-
await opts.ack()
|
|
300
|
-
},
|
|
301
|
-
onError: (err) => {
|
|
302
|
-
throw err
|
|
303
|
-
},
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
await using channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
307
|
-
await channel.start()
|
|
308
|
-
|
|
309
|
-
expect(receivedEvents).toHaveLength(3)
|
|
310
|
-
expect(receivedEvents[0].id).toBe(1)
|
|
311
|
-
expect(receivedEvents[1].id).toBe(2)
|
|
312
|
-
expect(receivedEvents[2].id).toBe(3)
|
|
313
|
-
expect(receivedAcks).toEqual([1, 2, 3])
|
|
314
|
-
})
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
describe('auth', () => {
|
|
318
|
-
it('includes auth header when adminPassword is provided', async () => {
|
|
319
|
-
await using server = await createWebSocketServer()
|
|
320
|
-
|
|
321
|
-
const { port } = server.address() as AddressInfo
|
|
322
|
-
|
|
323
|
-
let receivedAuthHeader: string | undefined
|
|
324
|
-
|
|
325
|
-
server.on('connection', (socket, request) => {
|
|
326
|
-
receivedAuthHeader = request.headers.authorization
|
|
327
|
-
socket.close()
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
const handler: TapHandler = {
|
|
331
|
-
onEvent: async () => {},
|
|
332
|
-
onError: () => {},
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
await using channel = new TapChannel(`ws://localhost:${port}`, handler, {
|
|
336
|
-
adminPassword: 'secret',
|
|
337
|
-
})
|
|
338
|
-
await channel.start()
|
|
339
|
-
|
|
340
|
-
expect(receivedAuthHeader).toBe('Basic YWRtaW46c2VjcmV0')
|
|
341
|
-
})
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
describe('error handling', () => {
|
|
345
|
-
it('calls onError for malformed messages', async () => {
|
|
346
|
-
await using server = await createWebSocketServer()
|
|
347
|
-
|
|
348
|
-
const { port } = server.address() as AddressInfo
|
|
349
|
-
|
|
350
|
-
const errors: Error[] = []
|
|
351
|
-
|
|
352
|
-
server.on('connection', (socket) => {
|
|
353
|
-
socket.send('not valid json')
|
|
354
|
-
setTimeout(() => socket.close(), 100)
|
|
355
|
-
})
|
|
356
|
-
|
|
357
|
-
const handler: TapHandler = {
|
|
358
|
-
onEvent: async () => {},
|
|
359
|
-
onError: (err) => {
|
|
360
|
-
errors.push(err)
|
|
361
|
-
},
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
await using channel = new TapChannel(`ws://localhost:${port}`, handler)
|
|
365
|
-
await channel.start()
|
|
366
|
-
|
|
367
|
-
expect(errors).toHaveLength(1)
|
|
368
|
-
expect(errors[0].message).toBe('Failed to parse message')
|
|
369
|
-
})
|
|
370
|
-
})
|
|
371
|
-
})
|