@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
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
|
+
|
|
19
|
+
## 0.3.4
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- Updated dependencies []:
|
|
24
|
+
- @atproto/lex@0.1.6
|
|
25
|
+
|
|
3
26
|
## 0.3.3
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/tap",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
25
|
-
"@atproto/syntax": "^0.6.
|
|
26
|
-
"@atproto/lex": "^0.1.
|
|
27
|
-
"@atproto/ws-client": "^0.1.
|
|
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
package/src/lex-indexer.ts
DELETED
|
@@ -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
|
-
}
|
package/src/simple-indexer.ts
DELETED
|
@@ -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
|
-
}
|