@atproto/tap 0.0.2
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 +10 -0
- package/LICENSE.txt +7 -0
- package/README.md +221 -0
- package/dist/channel.d.ts +32 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +146 -0
- package/dist/channel.js.map +1 -0
- package/dist/client.d.ts +19 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +104 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/simple-indexer.d.ts +17 -0
- package/dist/simple-indexer.d.ts.map +1 -0
- package/dist/simple-indexer.js +53 -0
- package/dist/simple-indexer.js.map +1 -0
- package/dist/types.d.ts +286 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +75 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +4 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +37 -0
- package/dist/util.js.map +1 -0
- package/jest.config.js +10 -0
- package/package.json +43 -0
- package/src/channel.ts +152 -0
- package/src/client.ts +100 -0
- package/src/index.ts +5 -0
- package/src/simple-indexer.ts +47 -0
- package/src/types.ts +109 -0
- package/src/util.ts +33 -0
- package/tests/channel.test.ts +379 -0
- package/tests/client.test.ts +208 -0
- package/tests/simple-indexer.test.ts +188 -0
- package/tests/util.test.ts +88 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +4 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @atproto/tap
|
|
2
|
+
|
|
3
|
+
## 0.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#4290](https://github.com/bluesky-social/atproto/pull/4290) [`b4a76ba`](https://github.com/bluesky-social/atproto/commit/b4a76bae7bef1189302488d43ce49a03fd61f957) Thanks [@dholms](https://github.com/dholms)! - Initial version of package
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [[`b4a76ba`](https://github.com/bluesky-social/atproto/commit/b4a76bae7bef1189302488d43ce49a03fd61f957)]:
|
|
10
|
+
- @atproto/ws-client@0.0.4
|
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Dual MIT/Apache-2.0 License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2025 Bluesky Social PBC, and Contributors
|
|
4
|
+
|
|
5
|
+
Except as otherwise noted in individual files, this software is licensed under the MIT license (<http://opensource.org/licenses/MIT>), or the Apache License, Version 2.0 (<http://www.apache.org/licenses/LICENSE-2.0>).
|
|
6
|
+
|
|
7
|
+
Downstream projects and end users may chose either license individually, or both together, at their discretion. The motivation for this dual-licensing is the additional software patent assurance provided by Apache 2.0.
|
package/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# @atproto/tap
|
|
2
|
+
|
|
3
|
+
TypeScript client library for [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap/README.md), a sync utility for the AT Protocol network.
|
|
4
|
+
|
|
5
|
+
Tap handles firehose connections, cryptographic verification, backfill, and filtering. This client library lets you connect to a Tap instance and receive simple JSON events for the repos you care about.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@atproto/tap)
|
|
8
|
+
[](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @atproto/tap
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { Tap, SimpleIndexer } from '@atproto/tap'
|
|
18
|
+
|
|
19
|
+
const tap = new Tap('http://localhost:2480', { adminPassword: 'secret' })
|
|
20
|
+
|
|
21
|
+
const indexer = new SimpleIndexer()
|
|
22
|
+
|
|
23
|
+
indexer.identity(async (evt) => {
|
|
24
|
+
console.log(`${evt.did} updated identity: ${evt.handle} (${evt.status})`)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
indexer.record(async (evt) => {
|
|
28
|
+
const uri = `at://${evt.did}/${evt.collection}/${evt.rkey}`
|
|
29
|
+
if (evt.action === 'create' || evt.action === 'update') {
|
|
30
|
+
console.log(`${evt.action}: ${uri}`)
|
|
31
|
+
} else {
|
|
32
|
+
console.log(`deleted: ${uri}`)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
indexer.error((err) => console.error(err))
|
|
37
|
+
|
|
38
|
+
const channel = tap.channel(indexer)
|
|
39
|
+
channel.start()
|
|
40
|
+
|
|
41
|
+
await tap.addRepos(['did:plc:ewvi7nxzyoun6zhxrhs64oiz'])
|
|
42
|
+
|
|
43
|
+
// On shutdown
|
|
44
|
+
await channel.destroy()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Running Tap
|
|
48
|
+
|
|
49
|
+
See the [Tap README](https://github.com/bluesky-social/indigo/tree/main/cmd/tap/README.md) for details getting Tap up and running. Your app can communicate with it either locally or over the internet.
|
|
50
|
+
|
|
51
|
+
This library is intended to be used with Tap running in the default mode of "WebScoket with acks". In this mode, Tap provides:
|
|
52
|
+
|
|
53
|
+
- **At-least-once delivery**: Events may be redelivered if the connection drops before an ack is received
|
|
54
|
+
- **Per-repo ordering**: Events for the same repo are delivered in order
|
|
55
|
+
- **Backfill**: When you add a repo, historical events are delivered before live events
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
### `Tap`
|
|
60
|
+
|
|
61
|
+
The main client for interacting with a Tap server.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const tap = new Tap(url: string, config?: TapConfig)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Config options:**
|
|
68
|
+
|
|
69
|
+
- `adminPassword?: string` - Password for Basic auth (required if Tap server has auth enabled)
|
|
70
|
+
|
|
71
|
+
**Methods:**
|
|
72
|
+
|
|
73
|
+
- `channel(handler: TapHandler, opts?: TapWebsocketOptions): TapChannel` - Create a WebSocket channel to receive events
|
|
74
|
+
- `addRepos(dids: string[]): Promise<void>` - Add repos to track (triggers backfill)
|
|
75
|
+
- `removeRepos(dids: string[]): Promise<void>` - Stop tracking repos
|
|
76
|
+
- `resolveDid(did: string): Promise<DidDocument | null>` - Resolve a DID to its DID document
|
|
77
|
+
- `getRepoInfo(did: string): Promise<RepoInfo>` - Get info about a tracked repo
|
|
78
|
+
|
|
79
|
+
### `TapChannel`
|
|
80
|
+
|
|
81
|
+
WebSocket connection for receiving events. Created via `tap.channel()`.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const channel = tap.channel(handler, opts?)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Methods:**
|
|
88
|
+
|
|
89
|
+
- `start(): Promise<void>` - Start receiving events. Returns a promise that resolves when the connection is destroyed or errors.
|
|
90
|
+
- `destroy(): Promise<void>` - Close the connection
|
|
91
|
+
|
|
92
|
+
The channel automatically handles reconnection and keepalive. Events are automatically acknowledged after your handler completes successfully.
|
|
93
|
+
|
|
94
|
+
### `SimpleIndexer`
|
|
95
|
+
|
|
96
|
+
A convenience class for handling events by type. Passed into `tap.channel()` when opening a channel with Tap.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const indexer = new SimpleIndexer()
|
|
100
|
+
|
|
101
|
+
indexer.identity(async (evt: IdentityEvent) => { ... })
|
|
102
|
+
indexer.record(async (evt: RecordEvent) => { ... })
|
|
103
|
+
indexer.error((err: Error) => { ... })
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
If no error handler is registered, errors will throw as unhandled exceptions.
|
|
107
|
+
|
|
108
|
+
### `TapHandler`
|
|
109
|
+
|
|
110
|
+
You can create your own custom handler by creating a class that implements the `TapHandler` interface:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
interface TapHandler {
|
|
114
|
+
onEvent: (evt: TapEvent, opts: HandlerOpts) => void | Promise<void>
|
|
115
|
+
onError: (err: Error) => void
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface HandlerOpts {
|
|
119
|
+
signal: AbortSignal
|
|
120
|
+
ack: () => Promise<void>
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
When implementing a custom handler, be sure to call `ack()` when you're done processing the event.
|
|
125
|
+
|
|
126
|
+
## Event Types
|
|
127
|
+
|
|
128
|
+
### `RecordEvent`
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
type RecordEvent = {
|
|
132
|
+
id: number
|
|
133
|
+
type: 'record'
|
|
134
|
+
action: 'create' | 'update' | 'delete'
|
|
135
|
+
did: string
|
|
136
|
+
rev: string
|
|
137
|
+
collection: string
|
|
138
|
+
rkey: string
|
|
139
|
+
record?: Record<string, unknown> // present for create/update
|
|
140
|
+
cid?: string // present for create/update
|
|
141
|
+
live: boolean // true if from firehose, false if from backfill
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### `IdentityEvent`
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
type IdentityEvent = {
|
|
149
|
+
id: number
|
|
150
|
+
type: 'identity'
|
|
151
|
+
did: string
|
|
152
|
+
handle: string
|
|
153
|
+
isActive: boolean
|
|
154
|
+
status: 'active' | 'takendown' | 'suspended' | 'deactivated' | 'deleted'
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Webhook Mode
|
|
159
|
+
|
|
160
|
+
If your Tap server is configured for webhook delivery, you can use `parseTapEvent` to validate incoming webhook payloads:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import express from 'express'
|
|
164
|
+
import { parseTapEvent, assureAdminAuth } from '@atproto/tap'
|
|
165
|
+
|
|
166
|
+
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD
|
|
167
|
+
|
|
168
|
+
const app = express()
|
|
169
|
+
app.use(express.json())
|
|
170
|
+
|
|
171
|
+
app.post('/webhook', async (req, res) => {
|
|
172
|
+
try {
|
|
173
|
+
assureAdminAuth(ADMIN_PASSWORD, req.headers.authorization)
|
|
174
|
+
} catch {
|
|
175
|
+
return res.status(401).json({ error: 'Unauthorized' })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const evt = parseTapEvent(req.body)
|
|
180
|
+
// handle event...
|
|
181
|
+
res.sendStatus(200)
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error('Failed to process event:', err)
|
|
184
|
+
res.status(500).json({ error: 'Failed to process event' })
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Utilities
|
|
190
|
+
|
|
191
|
+
### Auth helpers
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import {
|
|
195
|
+
formatAdminAuthHeader,
|
|
196
|
+
parseAdminAuthHeader,
|
|
197
|
+
assureAdminAuth,
|
|
198
|
+
} from '@atproto/tap'
|
|
199
|
+
|
|
200
|
+
// Format a password into a Basic auth header value
|
|
201
|
+
const header = formatAdminAuthHeader('secret')
|
|
202
|
+
// => 'Basic YWRtaW46c2VjcmV0'
|
|
203
|
+
|
|
204
|
+
// Parse an auth header to extract the password (throws if invalid)
|
|
205
|
+
const password = parseAdminAuthHeader(header)
|
|
206
|
+
|
|
207
|
+
// Verify auth header matches expected password (timing-safe, throws if invalid)
|
|
208
|
+
assureAdminAuth('secret', req.headers.authorization)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Event parsing
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import { parseTapEvent } from '@atproto/tap'
|
|
215
|
+
|
|
216
|
+
const evt = parseTapEvent(jsonData) // validates and returns typed TapEvent
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ClientOptions } from 'ws';
|
|
2
|
+
import { TapEvent } from './types';
|
|
3
|
+
export interface HandlerOpts {
|
|
4
|
+
signal: AbortSignal;
|
|
5
|
+
ack: () => Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export interface TapHandler {
|
|
8
|
+
onEvent: (evt: TapEvent, opts: HandlerOpts) => void | Promise<void>;
|
|
9
|
+
onError: (err: Error) => void;
|
|
10
|
+
}
|
|
11
|
+
export type TapWebsocketOptions = ClientOptions & {
|
|
12
|
+
adminPassword?: string;
|
|
13
|
+
maxReconnectSeconds?: number;
|
|
14
|
+
heartbeatIntervalMs?: number;
|
|
15
|
+
onReconnectError?: (error: unknown, n: number, initialSetup: boolean) => void;
|
|
16
|
+
};
|
|
17
|
+
export declare class TapChannel {
|
|
18
|
+
private ws;
|
|
19
|
+
private handler;
|
|
20
|
+
private readonly abortController;
|
|
21
|
+
private readonly destroyDefer;
|
|
22
|
+
private bufferedAcks;
|
|
23
|
+
constructor(url: string, handler: TapHandler, wsOpts?: TapWebsocketOptions);
|
|
24
|
+
ackEvent(id: number): Promise<void>;
|
|
25
|
+
private sendAck;
|
|
26
|
+
private bufferAndSendAck;
|
|
27
|
+
private flushBufferedAcks;
|
|
28
|
+
start(): Promise<void>;
|
|
29
|
+
private processWsEvent;
|
|
30
|
+
destroy(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,IAAI,CAAA;AAGlC,OAAO,EAAE,QAAQ,EAAiB,MAAM,SAAS,CAAA;AAGjD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,WAAW,CAAA;IACnB,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnE,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;CAC9B;AAED,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG;IAChD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,KAAK,IAAI,CAAA;CAC9E,CAAA;AAOD,qBAAa,UAAU;IACrB,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,OAAO,CAAY;IAE3B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyC;IACzE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiC;IAE9D,OAAO,CAAC,YAAY,CAAoB;gBAGtC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,UAAU,EACnB,MAAM,GAAE,mBAAwB;IAoB5B,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAY3B,OAAO;YAKP,gBAAgB;YAShB,iBAAiB;IAqBzB,KAAK;YAcG,cAAc;IA0BtB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAI/B"}
|
package/dist/channel.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TapChannel = void 0;
|
|
4
|
+
const common_1 = require("@atproto/common");
|
|
5
|
+
const ws_client_1 = require("@atproto/ws-client");
|
|
6
|
+
const types_1 = require("./types");
|
|
7
|
+
const util_1 = require("./util");
|
|
8
|
+
class TapChannel {
|
|
9
|
+
constructor(url, handler, wsOpts = {}) {
|
|
10
|
+
Object.defineProperty(this, "ws", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
value: void 0
|
|
15
|
+
});
|
|
16
|
+
Object.defineProperty(this, "handler", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
value: void 0
|
|
21
|
+
});
|
|
22
|
+
Object.defineProperty(this, "abortController", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: new AbortController()
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(this, "destroyDefer", {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
writable: true,
|
|
32
|
+
value: (0, common_1.createDeferrable)()
|
|
33
|
+
});
|
|
34
|
+
Object.defineProperty(this, "bufferedAcks", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value: []
|
|
39
|
+
});
|
|
40
|
+
this.handler = handler;
|
|
41
|
+
const { adminPassword, ...rest } = wsOpts;
|
|
42
|
+
let headers = rest.headers;
|
|
43
|
+
if (adminPassword) {
|
|
44
|
+
headers ?? (headers = {});
|
|
45
|
+
headers['Authorization'] = (0, util_1.formatAdminAuthHeader)(adminPassword);
|
|
46
|
+
}
|
|
47
|
+
this.ws = new ws_client_1.WebSocketKeepAlive({
|
|
48
|
+
getUrl: async () => url,
|
|
49
|
+
onReconnect: () => {
|
|
50
|
+
this.flushBufferedAcks();
|
|
51
|
+
},
|
|
52
|
+
signal: this.abortController.signal,
|
|
53
|
+
...rest,
|
|
54
|
+
headers,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async ackEvent(id) {
|
|
58
|
+
if (this.ws.isConnected()) {
|
|
59
|
+
try {
|
|
60
|
+
await this.sendAck(id);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
await this.bufferAndSendAck(id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
await this.bufferAndSendAck(id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async sendAck(id) {
|
|
71
|
+
await this.ws.send(JSON.stringify({ type: 'ack', id }));
|
|
72
|
+
}
|
|
73
|
+
// resolves after the ack has been actually sent
|
|
74
|
+
async bufferAndSendAck(id) {
|
|
75
|
+
const defer = (0, common_1.createDeferrable)();
|
|
76
|
+
this.bufferedAcks.push({
|
|
77
|
+
id,
|
|
78
|
+
defer,
|
|
79
|
+
});
|
|
80
|
+
await defer.complete;
|
|
81
|
+
}
|
|
82
|
+
async flushBufferedAcks() {
|
|
83
|
+
while (this.bufferedAcks.length > 0) {
|
|
84
|
+
try {
|
|
85
|
+
const ack = this.bufferedAcks.at(0);
|
|
86
|
+
if (!ack) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
await this.sendAck(ack.id);
|
|
90
|
+
ack.defer.resolve();
|
|
91
|
+
this.bufferedAcks = this.bufferedAcks.slice(1);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
this.handler.onError(new Error(`failed to send ack for event ${this.bufferedAcks[0]}`, {
|
|
95
|
+
cause: err,
|
|
96
|
+
}));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async start() {
|
|
102
|
+
try {
|
|
103
|
+
for await (const chunk of this.ws) {
|
|
104
|
+
await this.processWsEvent(chunk);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
if ((0, common_1.isErrnoException)(err) && err.name === 'AbortError') {
|
|
109
|
+
this.destroyDefer.resolve();
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async processWsEvent(chunk) {
|
|
117
|
+
let evt;
|
|
118
|
+
try {
|
|
119
|
+
const data = chunk.toString();
|
|
120
|
+
evt = (0, types_1.parseTapEvent)(JSON.parse(data));
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
this.handler.onError(new Error('Failed to parse message', { cause: err }));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
await this.handler.onEvent(evt, {
|
|
128
|
+
signal: this.abortController.signal,
|
|
129
|
+
ack: async () => {
|
|
130
|
+
await this.ackEvent(evt.id);
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
// Don't ack on error - let Tap retry
|
|
136
|
+
this.handler.onError(new Error(`Failed to process event ${evt.id}`, { cause: err }));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async destroy() {
|
|
141
|
+
this.abortController.abort();
|
|
142
|
+
await this.destroyDefer.complete;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
exports.TapChannel = TapChannel;
|
|
146
|
+
//# sourceMappingURL=channel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":";;;AACA,4CAAgF;AAChF,kDAAuD;AACvD,mCAAiD;AACjD,iCAA8C;AAwB9C,MAAa,UAAU;IASrB,YACE,GAAW,EACX,OAAmB,EACnB,SAA8B,EAAE;QAX1B;;;;;WAAsB;QACtB;;;;;WAAmB;QAEV;;;;mBAAmC,IAAI,eAAe,EAAE;WAAA;QACxD;;;;mBAA2B,IAAA,yBAAgB,GAAE;WAAA;QAEtD;;;;mBAA8B,EAAE;WAAA;QAOtC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,MAAM,EAAE,aAAa,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAA;QACzC,IAAI,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;QAC1B,IAAI,aAAa,EAAE,CAAC;YAClB,OAAO,KAAP,OAAO,GAAK,EAAE,EAAA;YACd,OAAO,CAAC,eAAe,CAAC,GAAG,IAAA,4BAAqB,EAAC,aAAa,CAAC,CAAA;QACjE,CAAC;QACD,IAAI,CAAC,EAAE,GAAG,IAAI,8BAAkB,CAAC;YAC/B,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,GAAG;YACvB,WAAW,EAAE,GAAG,EAAE;gBAChB,IAAI,CAAC,iBAAiB,EAAE,CAAA;YAC1B,CAAC;YACD,MAAM,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM;YACnC,GAAG,IAAI;YACP,OAAO;SACR,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,EAAU;QACvB,IAAI,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;YAC1B,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;YACxB,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAA;YACjC,CAAC;QACH,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAA;QACjC,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,EAAU;QAC9B,MAAM,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;IACzD,CAAC;IAED,gDAAgD;IACxC,KAAK,CAAC,gBAAgB,CAAC,EAAU;QACvC,MAAM,KAAK,GAAG,IAAA,yBAAgB,GAAE,CAAA;QAChC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC;YACrB,EAAE;YACF,KAAK;SACN,CAAC,CAAA;QACF,MAAM,KAAK,CAAC,QAAQ,CAAA;IACtB,CAAC;IAEO,KAAK,CAAC,iBAAiB;QAC7B,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpC,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;gBACnC,IAAI,CAAC,GAAG,EAAE,CAAC;oBACT,OAAM;gBACR,CAAC;gBACD,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;gBAC1B,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,CAAA;gBACnB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YAChD,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,CAAC,OAAO,CAAC,OAAO,CAClB,IAAI,KAAK,CAAC,gCAAgC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE;oBAChE,KAAK,EAAE,GAAG;iBACX,CAAC,CACH,CAAA;gBACD,OAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC;YACH,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;gBAClC,MAAM,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAA;YAClC,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,IAAA,yBAAgB,EAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBACvD,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAA;YAC7B,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,CAAA;YACX,CAAC;QACH,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,cAAc,CAAC,KAAiB;QAC5C,IAAI,GAAa,CAAA;QACjB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAA;YAC7B,GAAG,GAAG,IAAA,qBAAa,EAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;QACvC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,yBAAyB,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,CAAA;YAC1E,OAAM;QACR,CAAC;QAED,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE;gBAC9B,MAAM,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM;gBACnC,GAAG,EAAE,KAAK,IAAI,EAAE;oBACd,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;gBAC7B,CAAC;aACF,CAAC,CAAA;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,qCAAqC;YACrC,IAAI,CAAC,OAAO,CAAC,OAAO,CAClB,IAAI,KAAK,CAAC,2BAA2B,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAC/D,CAAA;YACD,OAAM;QACR,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAA;QAC5B,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAA;IAClC,CAAC;CACF;AA3HD,gCA2HC","sourcesContent":["import { ClientOptions } from 'ws'\nimport { Deferrable, createDeferrable, isErrnoException } from '@atproto/common'\nimport { WebSocketKeepAlive } from '@atproto/ws-client'\nimport { TapEvent, parseTapEvent } from './types'\nimport { formatAdminAuthHeader } from './util'\n\nexport interface HandlerOpts {\n signal: AbortSignal\n ack: () => Promise<void>\n}\n\nexport interface TapHandler {\n onEvent: (evt: TapEvent, opts: HandlerOpts) => void | Promise<void>\n onError: (err: Error) => void\n}\n\nexport type TapWebsocketOptions = ClientOptions & {\n adminPassword?: string\n maxReconnectSeconds?: number\n heartbeatIntervalMs?: number\n onReconnectError?: (error: unknown, n: number, initialSetup: boolean) => void\n}\n\ntype BufferedAck = {\n id: number\n defer: Deferrable\n}\n\nexport class TapChannel {\n private ws: WebSocketKeepAlive\n private handler: TapHandler\n\n private readonly abortController: AbortController = new AbortController()\n private readonly destroyDefer: Deferrable = createDeferrable()\n\n private bufferedAcks: BufferedAck[] = []\n\n constructor(\n url: string,\n handler: TapHandler,\n wsOpts: TapWebsocketOptions = {},\n ) {\n this.handler = handler\n const { adminPassword, ...rest } = wsOpts\n let headers = rest.headers\n if (adminPassword) {\n headers ??= {}\n headers['Authorization'] = formatAdminAuthHeader(adminPassword)\n }\n this.ws = new WebSocketKeepAlive({\n getUrl: async () => url,\n onReconnect: () => {\n this.flushBufferedAcks()\n },\n signal: this.abortController.signal,\n ...rest,\n headers,\n })\n }\n\n async ackEvent(id: number): Promise<void> {\n if (this.ws.isConnected()) {\n try {\n await this.sendAck(id)\n } catch {\n await this.bufferAndSendAck(id)\n }\n } else {\n await this.bufferAndSendAck(id)\n }\n }\n\n private async sendAck(id: number): Promise<void> {\n await this.ws.send(JSON.stringify({ type: 'ack', id }))\n }\n\n // resolves after the ack has been actually sent\n private async bufferAndSendAck(id: number): Promise<void> {\n const defer = createDeferrable()\n this.bufferedAcks.push({\n id,\n defer,\n })\n await defer.complete\n }\n\n private async flushBufferedAcks(): Promise<void> {\n while (this.bufferedAcks.length > 0) {\n try {\n const ack = this.bufferedAcks.at(0)\n if (!ack) {\n return\n }\n await this.sendAck(ack.id)\n ack.defer.resolve()\n this.bufferedAcks = this.bufferedAcks.slice(1)\n } catch (err) {\n this.handler.onError(\n new Error(`failed to send ack for event ${this.bufferedAcks[0]}`, {\n cause: err,\n }),\n )\n return\n }\n }\n }\n\n async start() {\n try {\n for await (const chunk of this.ws) {\n await this.processWsEvent(chunk)\n }\n } catch (err) {\n if (isErrnoException(err) && err.name === 'AbortError') {\n this.destroyDefer.resolve()\n } else {\n throw err\n }\n }\n }\n\n private async processWsEvent(chunk: Uint8Array) {\n let evt: TapEvent\n try {\n const data = chunk.toString()\n evt = parseTapEvent(JSON.parse(data))\n } catch (err) {\n this.handler.onError(new Error('Failed to parse message', { cause: err }))\n return\n }\n\n try {\n await this.handler.onEvent(evt, {\n signal: this.abortController.signal,\n ack: async () => {\n await this.ackEvent(evt.id)\n },\n })\n } catch (err) {\n // Don't ack on error - let Tap retry\n this.handler.onError(\n new Error(`Failed to process event ${evt.id}`, { cause: err }),\n )\n return\n }\n }\n\n async destroy(): Promise<void> {\n this.abortController.abort()\n await this.destroyDefer.complete\n }\n}\n"]}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DidDocument } from '@atproto/common';
|
|
2
|
+
import { TapChannel, TapHandler, TapWebsocketOptions } from './channel';
|
|
3
|
+
import { RepoInfo } from './types';
|
|
4
|
+
export interface TapConfig {
|
|
5
|
+
adminPassword?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class Tap {
|
|
8
|
+
url: string;
|
|
9
|
+
private adminPassword?;
|
|
10
|
+
private authHeader?;
|
|
11
|
+
constructor(url: string, config?: TapConfig);
|
|
12
|
+
private getHeaders;
|
|
13
|
+
channel(handler: TapHandler, opts?: TapWebsocketOptions): TapChannel;
|
|
14
|
+
addRepos(dids: string[]): Promise<void>;
|
|
15
|
+
removeRepos(dids: string[]): Promise<void>;
|
|
16
|
+
resolveDid(did: string): Promise<DidDocument | null>;
|
|
17
|
+
getRepoInfo(did: string): Promise<RepoInfo>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAe,MAAM,iBAAiB,CAAA;AAC1D,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AACvE,OAAO,EAAE,QAAQ,EAAkB,MAAM,SAAS,CAAA;AAGlD,MAAM,WAAW,SAAS;IACxB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,qBAAa,GAAG;IACd,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,aAAa,CAAC,CAAQ;IAC9B,OAAO,CAAC,UAAU,CAAC,CAAQ;gBAEf,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,SAAc;IAW/C,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,mBAAmB,GAAG,UAAU;IAU9D,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAavC,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAa1C,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAepD,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;CAalD"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Tap = void 0;
|
|
4
|
+
const common_1 = require("@atproto/common");
|
|
5
|
+
const channel_1 = require("./channel");
|
|
6
|
+
const types_1 = require("./types");
|
|
7
|
+
const util_1 = require("./util");
|
|
8
|
+
class Tap {
|
|
9
|
+
constructor(url, config = {}) {
|
|
10
|
+
Object.defineProperty(this, "url", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
value: void 0
|
|
15
|
+
});
|
|
16
|
+
Object.defineProperty(this, "adminPassword", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
value: void 0
|
|
21
|
+
});
|
|
22
|
+
Object.defineProperty(this, "authHeader", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: void 0
|
|
27
|
+
});
|
|
28
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
29
|
+
throw new Error('Invalid URL, expected http:// or https://');
|
|
30
|
+
}
|
|
31
|
+
this.url = url;
|
|
32
|
+
this.adminPassword = config.adminPassword;
|
|
33
|
+
if (this.adminPassword) {
|
|
34
|
+
this.authHeader = (0, util_1.formatAdminAuthHeader)(this.adminPassword);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
getHeaders() {
|
|
38
|
+
const headers = {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
};
|
|
41
|
+
if (this.authHeader) {
|
|
42
|
+
headers['Authorization'] = this.authHeader;
|
|
43
|
+
}
|
|
44
|
+
return headers;
|
|
45
|
+
}
|
|
46
|
+
channel(handler, opts) {
|
|
47
|
+
const url = new URL(this.url);
|
|
48
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
49
|
+
url.pathname = '/channel';
|
|
50
|
+
return new channel_1.TapChannel(url.toString(), handler, {
|
|
51
|
+
adminPassword: this.adminPassword,
|
|
52
|
+
...opts,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async addRepos(dids) {
|
|
56
|
+
const response = await fetch(`${this.url}/repos/add`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: this.getHeaders(),
|
|
59
|
+
body: JSON.stringify({ dids }),
|
|
60
|
+
});
|
|
61
|
+
await response.body?.cancel(); // expect empty body
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`Failed to add repos: ${response.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async removeRepos(dids) {
|
|
67
|
+
const response = await fetch(`${this.url}/repos/remove`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: this.getHeaders(),
|
|
70
|
+
body: JSON.stringify({ dids }),
|
|
71
|
+
});
|
|
72
|
+
await response.body?.cancel(); // expect empty body
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error(`Failed to remove repos: ${response.statusText}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async resolveDid(did) {
|
|
78
|
+
const response = await fetch(`${this.url}/resolve/${did}`, {
|
|
79
|
+
method: 'GET',
|
|
80
|
+
headers: this.getHeaders(),
|
|
81
|
+
});
|
|
82
|
+
if (response.status === 404) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
else if (!response.ok) {
|
|
86
|
+
await response.body?.cancel();
|
|
87
|
+
throw new Error(`Failed to resolve DID: ${response.statusText}`);
|
|
88
|
+
}
|
|
89
|
+
return common_1.didDocument.parse(await response.json());
|
|
90
|
+
}
|
|
91
|
+
async getRepoInfo(did) {
|
|
92
|
+
const response = await fetch(`${this.url}/info/${did}`, {
|
|
93
|
+
method: 'GET',
|
|
94
|
+
headers: this.getHeaders(),
|
|
95
|
+
});
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
await response.body?.cancel();
|
|
98
|
+
throw new Error(`Failed to get repo info: ${response.statusText}`);
|
|
99
|
+
}
|
|
100
|
+
return types_1.repoInfoSchema.parse(await response.json());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.Tap = Tap;
|
|
104
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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","sourcesContent":["import { DidDocument, didDocument } from '@atproto/common'\nimport { TapChannel, TapHandler, TapWebsocketOptions } from './channel'\nimport { RepoInfo, repoInfoSchema } from './types'\nimport { formatAdminAuthHeader } from './util'\n\nexport interface TapConfig {\n adminPassword?: string\n}\n\nexport class Tap {\n url: string\n private adminPassword?: string\n private authHeader?: string\n\n constructor(url: string, config: TapConfig = {}) {\n if (!url.startsWith('http://') && !url.startsWith('https://')) {\n throw new Error('Invalid URL, expected http:// or https://')\n }\n this.url = url\n this.adminPassword = config.adminPassword\n if (this.adminPassword) {\n this.authHeader = formatAdminAuthHeader(this.adminPassword)\n }\n }\n\n private getHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n }\n if (this.authHeader) {\n headers['Authorization'] = this.authHeader\n }\n return headers\n }\n\n channel(handler: TapHandler, opts?: TapWebsocketOptions): TapChannel {\n const url = new URL(this.url)\n url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'\n url.pathname = '/channel'\n return new TapChannel(url.toString(), handler, {\n adminPassword: this.adminPassword,\n ...opts,\n })\n }\n\n async addRepos(dids: string[]): Promise<void> {\n const response = await fetch(`${this.url}/repos/add`, {\n method: 'POST',\n headers: this.getHeaders(),\n body: JSON.stringify({ dids }),\n })\n await response.body?.cancel() // expect empty body\n\n if (!response.ok) {\n throw new Error(`Failed to add repos: ${response.statusText}`)\n }\n }\n\n async removeRepos(dids: string[]): Promise<void> {\n const response = await fetch(`${this.url}/repos/remove`, {\n method: 'POST',\n headers: this.getHeaders(),\n body: JSON.stringify({ dids }),\n })\n await response.body?.cancel() // expect empty body\n\n if (!response.ok) {\n throw new Error(`Failed to remove repos: ${response.statusText}`)\n }\n }\n\n async resolveDid(did: string): Promise<DidDocument | null> {\n const response = await fetch(`${this.url}/resolve/${did}`, {\n method: 'GET',\n headers: this.getHeaders(),\n })\n\n if (response.status === 404) {\n return null\n } else if (!response.ok) {\n await response.body?.cancel()\n throw new Error(`Failed to resolve DID: ${response.statusText}`)\n }\n return didDocument.parse(await response.json())\n }\n\n async getRepoInfo(did: string): Promise<RepoInfo> {\n const response = await fetch(`${this.url}/info/${did}`, {\n method: 'GET',\n headers: this.getHeaders(),\n })\n\n if (!response.ok) {\n await response.body?.cancel()\n throw new Error(`Failed to get repo info: ${response.statusText}`)\n }\n\n return repoInfoSchema.parse(await response.json())\n }\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,kBAAkB,CAAA;AAChC,cAAc,QAAQ,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./client"), exports);
|
|
19
|
+
__exportStar(require("./channel"), exports);
|
|
20
|
+
__exportStar(require("./simple-indexer"), exports);
|
|
21
|
+
__exportStar(require("./util"), exports);
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,0CAAuB;AACvB,2CAAwB;AACxB,4CAAyB;AACzB,mDAAgC;AAChC,yCAAsB","sourcesContent":["export * from './types'\nexport * from './client'\nexport * from './channel'\nexport * from './simple-indexer'\nexport * from './util'\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { HandlerOpts, TapHandler } from './channel';
|
|
2
|
+
import { IdentityEvent, RecordEvent, TapEvent } from './types';
|
|
3
|
+
type IdentityEventHandler = (evt: IdentityEvent, opts?: HandlerOpts) => Promise<void>;
|
|
4
|
+
type RecordEventHandler = (evt: RecordEvent, opts?: HandlerOpts) => Promise<void>;
|
|
5
|
+
type ErrorHandler = (err: Error) => void;
|
|
6
|
+
export declare class SimpleIndexer implements TapHandler {
|
|
7
|
+
private identityHandler;
|
|
8
|
+
private recordHandler;
|
|
9
|
+
private errorHandler;
|
|
10
|
+
identity(fn: IdentityEventHandler): void;
|
|
11
|
+
record(fn: RecordEventHandler): void;
|
|
12
|
+
error(fn: ErrorHandler): void;
|
|
13
|
+
onEvent(evt: TapEvent, opts: HandlerOpts): Promise<void>;
|
|
14
|
+
onError(err: Error): void;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=simple-indexer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"simple-indexer.d.ts","sourceRoot":"","sources":["../src/simple-indexer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAE9D,KAAK,oBAAoB,GAAG,CAC1B,GAAG,EAAE,aAAa,EAClB,IAAI,CAAC,EAAE,WAAW,KACf,OAAO,CAAC,IAAI,CAAC,CAAA;AAClB,KAAK,kBAAkB,GAAG,CACxB,GAAG,EAAE,WAAW,EAChB,IAAI,CAAC,EAAE,WAAW,KACf,OAAO,CAAC,IAAI,CAAC,CAAA;AAClB,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;AAExC,qBAAa,aAAc,YAAW,UAAU;IAC9C,OAAO,CAAC,eAAe,CAAkC;IACzD,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,YAAY,CAA0B;IAE9C,QAAQ,CAAC,EAAE,EAAE,oBAAoB;IAIjC,MAAM,CAAC,EAAE,EAAE,kBAAkB;IAI7B,KAAK,CAAC,EAAE,EAAE,YAAY;IAIhB,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAS9D,OAAO,CAAC,GAAG,EAAE,KAAK;CAOnB"}
|