@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/src/channel.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { ClientOptions } from 'ws'
2
- import { Deferrable, createDeferrable, isErrnoException } from '@atproto/common'
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 (err) {
98
- this.handler.onError(
99
- new Error(`failed to send ack for event ${this.bufferedAcks[0]}`, {
100
- cause: err,
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 (isErrnoException(err) && err.name === 'AbortError') {
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
- evt = parseTapEvent(JSON.parse(data))
127
- } catch (err) {
128
- this.handler.onError(new Error('Failed to parse message', { cause: err }))
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 (err) {
145
+ } catch (cause) {
140
146
  // Don't ack on error - let Tap retry
141
- this.handler.onError(
142
- new Error(`Failed to process event ${evt.id}`, { cause: err }),
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
  }
@@ -1,5 +1,5 @@
1
1
  import { Infer, Main, RecordSchema, getMain } from '@atproto/lex'
2
- import { AtUri } from '@atproto/syntax'
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(collection: string, action: string): string {
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: string,
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
- return this.register('update', ns, handler)
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.matches(evt.record)
171
- if (!match) {
172
- const uriStr = AtUri.make(evt.did, evt.collection, evt.rkey).toString()
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 { z } from 'zod'
1
+ import { LexMap, LexValue, l } from '@atproto/lex'
2
+ import { DidString, HandleString, NsidString } from '@atproto/syntax'
2
3
 
3
- export const recordEventDataSchema = z.object({
4
- did: z.string(),
5
- rev: z.string(),
6
- collection: z.string(),
7
- rkey: z.string(),
8
- action: z.enum(['create', 'update', 'delete']),
9
- record: z.record(z.string(), z.unknown()).optional(),
10
- cid: z.string().optional(),
11
- live: z.boolean(),
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 = z.object({
15
- did: z.string(),
16
- handle: z.string(),
17
- is_active: z.boolean(),
18
- status: z.enum([
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 = z.object({
28
- id: z.number(),
29
- type: z.literal('record'),
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 = z.object({
34
- id: z.number(),
35
- type: z.literal('identity'),
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 = z.union([recordEventSchema, identityEventSchema])
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: string
49
+ did: DidString
46
50
  rev: string
47
- collection: string
51
+ collection: NsidString
48
52
  rkey: string
49
- record?: Record<string, unknown>
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: string
58
- handle: string
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: unknown): TapEvent => {
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 = z.object({
100
- did: z.string(),
101
- handle: z.string(),
102
- state: z.string(),
103
- rev: z.string(),
104
- records: z.number(),
105
- error: z.string().optional(),
106
- retries: z.number().optional(),
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 = z.infer<typeof repoInfoSchema>
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: 'bafyabc',
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
+ }
@@ -1,7 +1,8 @@
1
- import getPort from 'get-port'
2
- import { WebSocketServer } from 'ws'
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: 'bafyabc',
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
- const port = await getPort()
36
- const server = new WebSocketServer({ port })
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
- const channel = new TapChannel(`ws://localhost:${port}`, handler)
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
- const port = await getPort()
76
- const server = new WebSocketServer({ port })
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
- const channel = new TapChannel(`ws://localhost:${port}`, handler)
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
- const port = await getPort()
118
- const server = new WebSocketServer({ port })
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
- const channel = new TapChannel(`ws://localhost:${port}`, handler)
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
- const port = await getPort()
152
- const server = new WebSocketServer({ port })
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
- const channel = new TapChannel(`ws://localhost:${port}`, handler)
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
- const port = await getPort()
190
- const server = new WebSocketServer({ port })
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
- const channel = new TapChannel(`ws://localhost:${port}`, handler)
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
- const port = await getPort()
225
- const server = new WebSocketServer({ port })
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
- const channel = new TapChannel(`ws://localhost:${port}`, handler, {
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
- const port = await getPort()
280
- const server = new WebSocketServer({ port })
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
- const channel = new TapChannel(`ws://localhost:${port}`, handler)
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
- const port = await getPort()
326
- const server = new WebSocketServer({ port })
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
- const channel = new TapChannel(`ws://localhost:${port}`, handler, {
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
- const port = await getPort()
354
- const server = new WebSocketServer({ port })
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
- const channel = new TapChannel(`ws://localhost:${port}`, handler)
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
  })
@@ -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 getPort from 'get-port'
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(port) as http.Server
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(port) as http.Server
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('bafyabc')
62
+ expect(received[0].cid).toBe(
63
+ 'bafyreiclp443lavogvhj3d2ob2cxbfuscni2k5jk7bebjzg7khl3esabwq',
64
+ )
62
65
  })
63
66
 
64
67
  it('registers update handler', async () => {
@@ -1,3 +1,4 @@
1
+ import { describe, expect, it } from 'vitest'
1
2
  import { HandlerOpts } from '../src/channel'
2
3
  import { SimpleIndexer } from '../src/simple-indexer'
3
4
  import { IdentityEvent, RecordEvent } from '../src/types'
@@ -1,3 +1,4 @@
1
+ import { describe, expect, it } from 'vitest'
1
2
  import {
2
3
  assureAdminAuth,
3
4
  formatAdminAuthHeader,
package/tsconfig.json CHANGED
@@ -1,4 +1,7 @@
1
1
  {
2
2
  "include": [],
3
- "references": [{ "path": "./tsconfig.build.json" }]
3
+ "references": [
4
+ { "path": "./tsconfig.build.json" },
5
+ { "path": "./tsconfig.tests.json" }
6
+ ]
4
7
  }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig/vitest.json",
3
+ "include": ["./tests", "./src/**/*.test.ts"],
4
+ "compilerOptions": {
5
+ "noImplicitAny": true,
6
+ "rootDir": "./",
7
+ "baseUrl": "./"
8
+ }
9
+ }
@@ -0,0 +1,5 @@
1
+ import { defineProject } from 'vitest/config'
2
+
3
+ export default defineProject({
4
+ test: {},
5
+ })