@atproto/tap 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +47 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +18 -4
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lex-indexer.d.ts +46 -0
- package/dist/lex-indexer.d.ts.map +1 -0
- package/dist/lex-indexer.js +115 -0
- package/dist/lex-indexer.js.map +1 -0
- package/dist/simple-indexer.d.ts +3 -3
- package/dist/simple-indexer.d.ts.map +1 -1
- package/dist/simple-indexer.js +3 -0
- package/dist/simple-indexer.js.map +1 -1
- package/package.json +3 -2
- package/src/client.ts +10 -4
- package/src/index.ts +1 -0
- package/src/lex-indexer.ts +187 -0
- package/src/simple-indexer.ts +8 -3
- package/tests/_util.ts +40 -0
- package/tests/lex-indexer.test.ts +340 -0
- package/tests/simple-indexer.test.ts +1 -33
- package/tsconfig.build.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @atproto/tap
|
|
2
2
|
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#4483](https://github.com/bluesky-social/atproto/pull/4483) [`e3357e9`](https://github.com/bluesky-social/atproto/commit/e3357e9c781ff84430556f4c891639acfcef3486) Thanks [@dholms](https://github.com/dholms)! - Add LexIndexer
|
|
8
|
+
|
|
9
|
+
## 0.0.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#4481](https://github.com/bluesky-social/atproto/pull/4481) [`e15e7cf`](https://github.com/bluesky-social/atproto/commit/e15e7cf1a07d6a4bdd7a9a3c591690613f5414b5) Thanks [@dholms](https://github.com/dholms)! - Fix URL formatting with trailing slash
|
|
14
|
+
|
|
15
|
+
- Updated dependencies []:
|
|
16
|
+
- @atproto/common@0.5.4
|
|
17
|
+
|
|
3
18
|
## 0.0.2
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -105,6 +105,53 @@ indexer.error((err: Error) => { ... })
|
|
|
105
105
|
|
|
106
106
|
If no error handler is registered, errors will throw as unhandled exceptions.
|
|
107
107
|
|
|
108
|
+
### `LexIndexer`
|
|
109
|
+
|
|
110
|
+
A typed indexer that uses `@atproto/lex` schemas for type-safe event handling. Register handlers for specific record types and actions:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { LexIndexer } from '@atproto/tap'
|
|
114
|
+
import * as com from './lexicons/com'
|
|
115
|
+
|
|
116
|
+
const indexer = new LexIndexer()
|
|
117
|
+
|
|
118
|
+
// Handle creates for a specific record type
|
|
119
|
+
indexer.create(com.example.post, async (evt) => {
|
|
120
|
+
// evt.record is fully typed as com.example.post.Main
|
|
121
|
+
console.log(`New post: ${evt.record.text}`)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Handle updates
|
|
125
|
+
indexer.update(com.example.post, async (evt) => {
|
|
126
|
+
console.log(`Updated post: ${evt.record.text}`)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// Handle deletes (no record on delete events)
|
|
130
|
+
indexer.delete(com.example.post, async (evt) => {
|
|
131
|
+
console.log(`Deleted: at://${evt.did}/${evt.collection}/${evt.rkey}`)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Handle both creates and updates with put()
|
|
135
|
+
indexer.put(com.example.like, async (evt) => {
|
|
136
|
+
console.log(`Like ${evt.action}: ${evt.record.subject.uri}`)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Fallback for unhandled record types/actions
|
|
140
|
+
indexer.other(async (evt) => {
|
|
141
|
+
console.log(`Unhandled: ${evt.action}, ${evt.collection}`)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// Identity and error handlers
|
|
145
|
+
indexer.identity(async (evt) => { ... })
|
|
146
|
+
indexer.error((err) => { ... })
|
|
147
|
+
|
|
148
|
+
const channel = tap.channel(indexer)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Records are validated against their schemas before handlers are called. If validation fails, an error is thrown which will be picked up through the `error` handler..
|
|
152
|
+
|
|
153
|
+
Duplicate handler registration throws an error, including conflicts between `put()` and `create()`/`update()` for the same schema.
|
|
154
|
+
|
|
108
155
|
### `TapHandler`
|
|
109
156
|
|
|
110
157
|
You can create your own custom handler by creating a class that implements the `TapHandler` interface:
|
package/dist/client.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare class Tap {
|
|
|
8
8
|
url: string;
|
|
9
9
|
private adminPassword?;
|
|
10
10
|
private authHeader?;
|
|
11
|
+
private addReposUrl;
|
|
12
|
+
private removeReposUrl;
|
|
11
13
|
constructor(url: string, config?: TapConfig);
|
|
12
14
|
private getHeaders;
|
|
13
15
|
channel(handler: TapHandler, opts?: TapWebsocketOptions): TapChannel;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
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;IAE3B,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,cAAc,CAAK;gBAEf,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,SAAc;IAc/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
CHANGED
|
@@ -25,6 +25,18 @@ class Tap {
|
|
|
25
25
|
writable: true,
|
|
26
26
|
value: void 0
|
|
27
27
|
});
|
|
28
|
+
Object.defineProperty(this, "addReposUrl", {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
writable: true,
|
|
32
|
+
value: void 0
|
|
33
|
+
});
|
|
34
|
+
Object.defineProperty(this, "removeReposUrl", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value: void 0
|
|
39
|
+
});
|
|
28
40
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
29
41
|
throw new Error('Invalid URL, expected http:// or https://');
|
|
30
42
|
}
|
|
@@ -33,6 +45,8 @@ class Tap {
|
|
|
33
45
|
if (this.adminPassword) {
|
|
34
46
|
this.authHeader = (0, util_1.formatAdminAuthHeader)(this.adminPassword);
|
|
35
47
|
}
|
|
48
|
+
this.addReposUrl = new URL('/repos/add', this.url);
|
|
49
|
+
this.removeReposUrl = new URL('/repos/remove', this.url);
|
|
36
50
|
}
|
|
37
51
|
getHeaders() {
|
|
38
52
|
const headers = {
|
|
@@ -53,7 +67,7 @@ class Tap {
|
|
|
53
67
|
});
|
|
54
68
|
}
|
|
55
69
|
async addRepos(dids) {
|
|
56
|
-
const response = await fetch(
|
|
70
|
+
const response = await fetch(this.addReposUrl, {
|
|
57
71
|
method: 'POST',
|
|
58
72
|
headers: this.getHeaders(),
|
|
59
73
|
body: JSON.stringify({ dids }),
|
|
@@ -64,7 +78,7 @@ class Tap {
|
|
|
64
78
|
}
|
|
65
79
|
}
|
|
66
80
|
async removeRepos(dids) {
|
|
67
|
-
const response = await fetch(
|
|
81
|
+
const response = await fetch(this.removeReposUrl, {
|
|
68
82
|
method: 'POST',
|
|
69
83
|
headers: this.getHeaders(),
|
|
70
84
|
body: JSON.stringify({ dids }),
|
|
@@ -75,7 +89,7 @@ class Tap {
|
|
|
75
89
|
}
|
|
76
90
|
}
|
|
77
91
|
async resolveDid(did) {
|
|
78
|
-
const response = await fetch(
|
|
92
|
+
const response = await fetch(new URL(`/resolve/${did}`, this.url), {
|
|
79
93
|
method: 'GET',
|
|
80
94
|
headers: this.getHeaders(),
|
|
81
95
|
});
|
|
@@ -89,7 +103,7 @@ class Tap {
|
|
|
89
103
|
return common_1.didDocument.parse(await response.json());
|
|
90
104
|
}
|
|
91
105
|
async getRepoInfo(did) {
|
|
92
|
-
const response = await fetch(
|
|
106
|
+
const response = await fetch(new URL(`/info/${did}`, this.url), {
|
|
93
107
|
method: 'GET',
|
|
94
108
|
headers: this.getHeaders(),
|
|
95
109
|
});
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";;;AAAA,4CAA0D;AAC1D,uCAAuE;AACvE,mCAAkD;AAClD,iCAA8C;AAM9C,MAAa,GAAG;
|
|
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 private addReposUrl: URL\n private removeReposUrl: URL\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 this.addReposUrl = new URL('/repos/add', this.url)\n this.removeReposUrl = new URL('/repos/remove', this.url)\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.addReposUrl, {\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.removeReposUrl, {\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(new URL(`/resolve/${did}`, this.url), {\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(new URL(`/info/${did}`, this.url), {\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
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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"}
|
|
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,eAAe,CAAA;AAC7B,cAAc,QAAQ,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -18,5 +18,6 @@ __exportStar(require("./types"), exports);
|
|
|
18
18
|
__exportStar(require("./client"), exports);
|
|
19
19
|
__exportStar(require("./channel"), exports);
|
|
20
20
|
__exportStar(require("./simple-indexer"), exports);
|
|
21
|
+
__exportStar(require("./lex-indexer"), exports);
|
|
21
22
|
__exportStar(require("./util"), exports);
|
|
22
23
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,0CAAuB;AACvB,2CAAwB;AACxB,4CAAyB;AACzB,mDAAgC;AAChC,gDAA6B;AAC7B,yCAAsB","sourcesContent":["export * from './types'\nexport * from './client'\nexport * from './channel'\nexport * from './simple-indexer'\nexport * from './lex-indexer'\nexport * from './util'\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Infer, Main, RecordSchema } from '@atproto/lex';
|
|
2
|
+
import { HandlerOpts, TapHandler } from './channel';
|
|
3
|
+
import { IdentityEvent, RecordEvent, TapEvent } from './types';
|
|
4
|
+
type BaseRecordEvent = Omit<RecordEvent, 'record' | 'action' | 'cid'>;
|
|
5
|
+
export type CreateEvent<R> = BaseRecordEvent & {
|
|
6
|
+
action: 'create';
|
|
7
|
+
record: R;
|
|
8
|
+
cid: string;
|
|
9
|
+
};
|
|
10
|
+
export type UpdateEvent<R> = BaseRecordEvent & {
|
|
11
|
+
action: 'update';
|
|
12
|
+
record: R;
|
|
13
|
+
cid: string;
|
|
14
|
+
};
|
|
15
|
+
export type PutEvent<R> = CreateEvent<R> | UpdateEvent<R>;
|
|
16
|
+
export type DeleteEvent = BaseRecordEvent & {
|
|
17
|
+
action: 'delete';
|
|
18
|
+
};
|
|
19
|
+
export type CreateHandler<R> = (evt: CreateEvent<R>, opts: HandlerOpts) => Promise<void>;
|
|
20
|
+
export type UpdateHandler<R> = (evt: UpdateEvent<R>, opts: HandlerOpts) => Promise<void>;
|
|
21
|
+
export type PutHandler<R> = (evt: PutEvent<R>, opts: HandlerOpts) => Promise<void>;
|
|
22
|
+
export type DeleteHandler = (evt: DeleteEvent, opts: HandlerOpts) => Promise<void>;
|
|
23
|
+
export type UntypedHandler = (evt: RecordEvent, opts: HandlerOpts) => Promise<void>;
|
|
24
|
+
export type IdentityHandler = (evt: IdentityEvent, opts: HandlerOpts) => Promise<void>;
|
|
25
|
+
export type ErrorHandler = (err: Error) => void;
|
|
26
|
+
export type RecordHandler<R> = CreateHandler<R> | UpdateHandler<R> | PutHandler<R> | DeleteHandler;
|
|
27
|
+
export declare class LexIndexer implements TapHandler {
|
|
28
|
+
private handlers;
|
|
29
|
+
private otherHandler;
|
|
30
|
+
private identityHandler;
|
|
31
|
+
private errorHandler;
|
|
32
|
+
private handlerKey;
|
|
33
|
+
private register;
|
|
34
|
+
create<const T extends RecordSchema>(ns: Main<T>, handler: CreateHandler<Infer<T>>): this;
|
|
35
|
+
update<const T extends RecordSchema>(ns: Main<T>, handler: UpdateHandler<Infer<T>>): this;
|
|
36
|
+
delete<const T extends RecordSchema>(ns: Main<T>, handler: DeleteHandler): this;
|
|
37
|
+
put<const T extends RecordSchema>(ns: Main<T>, handler: PutHandler<Infer<T>>): this;
|
|
38
|
+
other(fn: UntypedHandler): this;
|
|
39
|
+
identity(fn: IdentityHandler): this;
|
|
40
|
+
error(fn: ErrorHandler): this;
|
|
41
|
+
onEvent(evt: TapEvent, opts: HandlerOpts): Promise<void>;
|
|
42
|
+
private handleRecordEvent;
|
|
43
|
+
onError(err: Error): void;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=lex-indexer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lex-indexer.d.ts","sourceRoot":"","sources":["../src/lex-indexer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,EAAW,MAAM,cAAc,CAAA;AAEjE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAE9D,KAAK,eAAe,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,GAAG,QAAQ,GAAG,KAAK,CAAC,CAAA;AAErE,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,eAAe,GAAG;IAC7C,MAAM,EAAE,QAAQ,CAAA;IAChB,MAAM,EAAE,CAAC,CAAA;IACT,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,eAAe,GAAG;IAC7C,MAAM,EAAE,QAAQ,CAAA;IAChB,MAAM,EAAE,CAAC,CAAA;IACT,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;AAEzD,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG;IAC1C,MAAM,EAAE,QAAQ,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAC7B,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,EACnB,IAAI,EAAE,WAAW,KACd,OAAO,CAAC,IAAI,CAAC,CAAA;AAElB,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAC7B,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,EACnB,IAAI,EAAE,WAAW,KACd,OAAO,CAAC,IAAI,CAAC,CAAA;AAElB,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAC1B,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,EAChB,IAAI,EAAE,WAAW,KACd,OAAO,CAAC,IAAI,CAAC,CAAA;AAElB,MAAM,MAAM,aAAa,GAAG,CAC1B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,WAAW,KACd,OAAO,CAAC,IAAI,CAAC,CAAA;AAElB,MAAM,MAAM,cAAc,GAAG,CAC3B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,WAAW,KACd,OAAO,CAAC,IAAI,CAAC,CAAA;AAElB,MAAM,MAAM,eAAe,GAAG,CAC5B,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,WAAW,KACd,OAAO,CAAC,IAAI,CAAC,CAAA;AAElB,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;AAE/C,MAAM,MAAM,aAAa,CAAC,CAAC,IACvB,aAAa,CAAC,CAAC,CAAC,GAChB,aAAa,CAAC,CAAC,CAAC,GAChB,UAAU,CAAC,CAAC,CAAC,GACb,aAAa,CAAA;AAQjB,qBAAa,UAAW,YAAW,UAAU;IAC3C,OAAO,CAAC,QAAQ,CAAuC;IACvD,OAAO,CAAC,YAAY,CAA4B;IAChD,OAAO,CAAC,eAAe,CAA6B;IACpD,OAAO,CAAC,YAAY,CAA0B;IAE9C,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,QAAQ;IAchB,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,YAAY,EACjC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,EACX,OAAO,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAC/B,IAAI;IAIP,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,YAAY,EACjC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,EACX,OAAO,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAC/B,IAAI;IAIP,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,YAAY,EACjC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,EACX,OAAO,EAAE,aAAa,GACrB,IAAI;IAIP,GAAG,CAAC,KAAK,CAAC,CAAC,SAAS,YAAY,EAC9B,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,EACX,OAAO,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAC5B,IAAI;IAKP,KAAK,CAAC,EAAE,EAAE,cAAc,GAAG,IAAI;IAQ/B,QAAQ,CAAC,EAAE,EAAE,eAAe,GAAG,IAAI;IAQnC,KAAK,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI;IAQvB,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;YAShD,iBAAiB;IAwB/B,OAAO,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI;CAO1B"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LexIndexer = void 0;
|
|
4
|
+
const lex_1 = require("@atproto/lex");
|
|
5
|
+
const syntax_1 = require("@atproto/syntax");
|
|
6
|
+
class LexIndexer {
|
|
7
|
+
constructor() {
|
|
8
|
+
Object.defineProperty(this, "handlers", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: new Map()
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(this, "otherHandler", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: void 0
|
|
19
|
+
});
|
|
20
|
+
Object.defineProperty(this, "identityHandler", {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: true,
|
|
23
|
+
writable: true,
|
|
24
|
+
value: void 0
|
|
25
|
+
});
|
|
26
|
+
Object.defineProperty(this, "errorHandler", {
|
|
27
|
+
enumerable: true,
|
|
28
|
+
configurable: true,
|
|
29
|
+
writable: true,
|
|
30
|
+
value: void 0
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
handlerKey(collection, action) {
|
|
34
|
+
return `${collection}:${action}`;
|
|
35
|
+
}
|
|
36
|
+
register(action, ns, handler) {
|
|
37
|
+
const schema = (0, lex_1.getMain)(ns);
|
|
38
|
+
const key = this.handlerKey(schema.$type, action);
|
|
39
|
+
if (this.handlers.has(key)) {
|
|
40
|
+
throw new Error(`Handler already registered for ${key}`);
|
|
41
|
+
}
|
|
42
|
+
this.handlers.set(key, { schema, handler });
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
create(ns, handler) {
|
|
46
|
+
return this.register('create', ns, handler);
|
|
47
|
+
}
|
|
48
|
+
update(ns, handler) {
|
|
49
|
+
return this.register('update', ns, handler);
|
|
50
|
+
}
|
|
51
|
+
delete(ns, handler) {
|
|
52
|
+
return this.register('delete', ns, handler);
|
|
53
|
+
}
|
|
54
|
+
put(ns, handler) {
|
|
55
|
+
this.register('create', ns, handler);
|
|
56
|
+
return this.register('update', ns, handler);
|
|
57
|
+
}
|
|
58
|
+
other(fn) {
|
|
59
|
+
if (this.otherHandler) {
|
|
60
|
+
throw new Error(`Handler already registered for "other"`);
|
|
61
|
+
}
|
|
62
|
+
this.otherHandler = fn;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
identity(fn) {
|
|
66
|
+
if (this.identityHandler) {
|
|
67
|
+
throw new Error(`Handler already registered for "identity"`);
|
|
68
|
+
}
|
|
69
|
+
this.identityHandler = fn;
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
error(fn) {
|
|
73
|
+
if (this.errorHandler) {
|
|
74
|
+
throw new Error(`Handler already registered for "error"`);
|
|
75
|
+
}
|
|
76
|
+
this.errorHandler = fn;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
async onEvent(evt, opts) {
|
|
80
|
+
if (evt.type === 'identity') {
|
|
81
|
+
await this.identityHandler?.(evt, opts);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
await this.handleRecordEvent(evt, opts);
|
|
85
|
+
}
|
|
86
|
+
await opts.ack();
|
|
87
|
+
}
|
|
88
|
+
async handleRecordEvent(evt, opts) {
|
|
89
|
+
const { collection, action } = evt;
|
|
90
|
+
const key = this.handlerKey(collection, action);
|
|
91
|
+
const registered = this.handlers.get(key);
|
|
92
|
+
if (!registered) {
|
|
93
|
+
await this.otherHandler?.(evt, opts);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (action === 'create' || action === 'update') {
|
|
97
|
+
const match = registered.schema.matches(evt.record);
|
|
98
|
+
if (!match) {
|
|
99
|
+
const uriStr = syntax_1.AtUri.make(evt.did, evt.collection, evt.rkey).toString();
|
|
100
|
+
throw new Error(`Record validation failed for ${uriStr}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
await registered.handler(evt, opts);
|
|
104
|
+
}
|
|
105
|
+
onError(err) {
|
|
106
|
+
if (this.errorHandler) {
|
|
107
|
+
this.errorHandler(err);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.LexIndexer = LexIndexer;
|
|
115
|
+
//# sourceMappingURL=lex-indexer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lex-indexer.js","sourceRoot":"","sources":["../src/lex-indexer.ts"],"names":[],"mappings":"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","sourcesContent":["import { Infer, Main, RecordSchema, getMain } from '@atproto/lex'\nimport { AtUri } from '@atproto/syntax'\nimport { HandlerOpts, TapHandler } from './channel'\nimport { IdentityEvent, RecordEvent, TapEvent } from './types'\n\ntype BaseRecordEvent = Omit<RecordEvent, 'record' | 'action' | 'cid'>\n\nexport type CreateEvent<R> = BaseRecordEvent & {\n action: 'create'\n record: R\n cid: string\n}\n\nexport type UpdateEvent<R> = BaseRecordEvent & {\n action: 'update'\n record: R\n cid: string\n}\n\nexport type PutEvent<R> = CreateEvent<R> | UpdateEvent<R>\n\nexport type DeleteEvent = BaseRecordEvent & {\n action: 'delete'\n}\n\nexport type CreateHandler<R> = (\n evt: CreateEvent<R>,\n opts: HandlerOpts,\n) => Promise<void>\n\nexport type UpdateHandler<R> = (\n evt: UpdateEvent<R>,\n opts: HandlerOpts,\n) => Promise<void>\n\nexport type PutHandler<R> = (\n evt: PutEvent<R>,\n opts: HandlerOpts,\n) => Promise<void>\n\nexport type DeleteHandler = (\n evt: DeleteEvent,\n opts: HandlerOpts,\n) => Promise<void>\n\nexport type UntypedHandler = (\n evt: RecordEvent,\n opts: HandlerOpts,\n) => Promise<void>\n\nexport type IdentityHandler = (\n evt: IdentityEvent,\n opts: HandlerOpts,\n) => Promise<void>\n\nexport type ErrorHandler = (err: Error) => void\n\nexport type RecordHandler<R> =\n | CreateHandler<R>\n | UpdateHandler<R>\n | PutHandler<R>\n | DeleteHandler\n\ninterface RegisteredHandler {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n handler: RecordHandler<any>\n schema: RecordSchema\n}\n\nexport class LexIndexer implements TapHandler {\n private handlers = new Map<string, RegisteredHandler>()\n private otherHandler: UntypedHandler | undefined\n private identityHandler: IdentityHandler | undefined\n private errorHandler: ErrorHandler | undefined\n\n private handlerKey(collection: string, action: string): string {\n return `${collection}:${action}`\n }\n\n private register<const T extends RecordSchema>(\n action: string,\n ns: Main<T>,\n handler: RecordHandler<Infer<T>>,\n ): this {\n const schema = getMain(ns)\n const key = this.handlerKey(schema.$type, action)\n if (this.handlers.has(key)) {\n throw new Error(`Handler already registered for ${key}`)\n }\n this.handlers.set(key, { schema, handler })\n return this\n }\n\n create<const T extends RecordSchema>(\n ns: Main<T>,\n handler: CreateHandler<Infer<T>>,\n ): this {\n return this.register('create', ns, handler)\n }\n\n update<const T extends RecordSchema>(\n ns: Main<T>,\n handler: UpdateHandler<Infer<T>>,\n ): this {\n return this.register('update', ns, handler)\n }\n\n delete<const T extends RecordSchema>(\n ns: Main<T>,\n handler: DeleteHandler,\n ): this {\n return this.register('delete', ns, handler)\n }\n\n put<const T extends RecordSchema>(\n ns: Main<T>,\n handler: PutHandler<Infer<T>>,\n ): this {\n this.register('create', ns, handler)\n return this.register('update', ns, handler)\n }\n\n other(fn: UntypedHandler): this {\n if (this.otherHandler) {\n throw new Error(`Handler already registered for \"other\"`)\n }\n this.otherHandler = fn\n return this\n }\n\n identity(fn: IdentityHandler): this {\n if (this.identityHandler) {\n throw new Error(`Handler already registered for \"identity\"`)\n }\n this.identityHandler = fn\n return this\n }\n\n error(fn: ErrorHandler): this {\n if (this.errorHandler) {\n throw new Error(`Handler already registered for \"error\"`)\n }\n this.errorHandler = fn\n return this\n }\n\n async onEvent(evt: TapEvent, opts: HandlerOpts): Promise<void> {\n if (evt.type === 'identity') {\n await this.identityHandler?.(evt, opts)\n } else {\n await this.handleRecordEvent(evt, opts)\n }\n await opts.ack()\n }\n\n private async handleRecordEvent(\n evt: RecordEvent,\n opts: HandlerOpts,\n ): Promise<void> {\n const { collection, action } = evt\n const key = this.handlerKey(collection, action)\n const registered = this.handlers.get(key)\n\n if (!registered) {\n await this.otherHandler?.(evt, opts)\n return\n }\n\n if (action === 'create' || action === 'update') {\n const match = registered.schema.matches(evt.record)\n if (!match) {\n const uriStr = AtUri.make(evt.did, evt.collection, evt.rkey).toString()\n throw new Error(`Record validation failed for ${uriStr}`)\n }\n }\n\n await (registered.handler as UntypedHandler)(evt, opts)\n }\n\n onError(err: Error): void {\n if (this.errorHandler) {\n this.errorHandler(err)\n } else {\n throw err\n }\n }\n}\n"]}
|
package/dist/simple-indexer.d.ts
CHANGED
|
@@ -7,9 +7,9 @@ export declare class SimpleIndexer implements TapHandler {
|
|
|
7
7
|
private identityHandler;
|
|
8
8
|
private recordHandler;
|
|
9
9
|
private errorHandler;
|
|
10
|
-
identity(fn: IdentityEventHandler):
|
|
11
|
-
record(fn: RecordEventHandler):
|
|
12
|
-
error(fn: ErrorHandler):
|
|
10
|
+
identity(fn: IdentityEventHandler): this;
|
|
11
|
+
record(fn: RecordEventHandler): this;
|
|
12
|
+
error(fn: ErrorHandler): this;
|
|
13
13
|
onEvent(evt: TapEvent, opts: HandlerOpts): Promise<void>;
|
|
14
14
|
onError(err: Error): void;
|
|
15
15
|
}
|
|
@@ -1 +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;
|
|
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;AAElB,KAAK,kBAAkB,GAAG,CACxB,GAAG,EAAE,WAAW,EAChB,IAAI,CAAC,EAAE,WAAW,KACf,OAAO,CAAC,IAAI,CAAC,CAAA;AAElB,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,GAAG,IAAI;IAKxC,MAAM,CAAC,EAAE,EAAE,kBAAkB,GAAG,IAAI;IAKpC,KAAK,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI;IAKvB,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAS9D,OAAO,CAAC,GAAG,EAAE,KAAK;CAOnB"}
|
package/dist/simple-indexer.js
CHANGED
|
@@ -24,12 +24,15 @@ class SimpleIndexer {
|
|
|
24
24
|
}
|
|
25
25
|
identity(fn) {
|
|
26
26
|
this.identityHandler = fn;
|
|
27
|
+
return this;
|
|
27
28
|
}
|
|
28
29
|
record(fn) {
|
|
29
30
|
this.recordHandler = fn;
|
|
31
|
+
return this;
|
|
30
32
|
}
|
|
31
33
|
error(fn) {
|
|
32
34
|
this.errorHandler = fn;
|
|
35
|
+
return this;
|
|
33
36
|
}
|
|
34
37
|
async onEvent(evt, opts) {
|
|
35
38
|
if (evt.type === 'record') {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"simple-indexer.js","sourceRoot":"","sources":["../src/simple-indexer.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"simple-indexer.js","sourceRoot":"","sources":["../src/simple-indexer.ts"],"names":[],"mappings":";;;AAeA,MAAa,aAAa;IAA1B;QACU;;;;;WAAiD;QACjD;;;;;WAA6C;QAC7C;;;;;WAAsC;IAiChD,CAAC;IA/BC,QAAQ,CAAC,EAAwB;QAC/B,IAAI,CAAC,eAAe,GAAG,EAAE,CAAA;QACzB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,CAAC,EAAsB;QAC3B,IAAI,CAAC,aAAa,GAAG,EAAE,CAAA;QACvB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,KAAK,CAAC,EAAgB;QACpB,IAAI,CAAC,YAAY,GAAG,EAAE,CAAA;QACtB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,GAAa,EAAE,IAAiB;QAC5C,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC1B,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QACvC,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QACzC,CAAC;QACD,MAAM,IAAI,CAAC,GAAG,EAAE,CAAA;IAClB,CAAC;IAED,OAAO,CAAC,GAAU;QAChB,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAA;QACxB,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,CAAA;QACX,CAAC;IACH,CAAC;CACF;AApCD,sCAoCC","sourcesContent":["import { HandlerOpts, TapHandler } from './channel'\nimport { IdentityEvent, RecordEvent, TapEvent } from './types'\n\ntype IdentityEventHandler = (\n evt: IdentityEvent,\n opts?: HandlerOpts,\n) => Promise<void>\n\ntype RecordEventHandler = (\n evt: RecordEvent,\n opts?: HandlerOpts,\n) => Promise<void>\n\ntype ErrorHandler = (err: Error) => void\n\nexport class SimpleIndexer implements TapHandler {\n private identityHandler: IdentityEventHandler | undefined\n private recordHandler: RecordEventHandler | undefined\n private errorHandler: ErrorHandler | undefined\n\n identity(fn: IdentityEventHandler): this {\n this.identityHandler = fn\n return this\n }\n\n record(fn: RecordEventHandler): this {\n this.recordHandler = fn\n return this\n }\n\n error(fn: ErrorHandler): this {\n this.errorHandler = fn\n return this\n }\n\n async onEvent(evt: TapEvent, opts: HandlerOpts): Promise<void> {\n if (evt.type === 'record') {\n await this.recordHandler?.(evt, opts)\n } else {\n await this.identityHandler?.(evt, opts)\n }\n await opts.ack()\n }\n\n onError(err: Error) {\n if (this.errorHandler) {\n this.errorHandler(err)\n } else {\n throw err\n }\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/tap",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "atproto tap client",
|
|
6
6
|
"keywords": [
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"ws": "^8.12.0",
|
|
26
26
|
"zod": "^3.23.8",
|
|
27
|
-
"@atproto/common": "^0.5.
|
|
27
|
+
"@atproto/common": "^0.5.5",
|
|
28
|
+
"@atproto/lex": "^0.0.8",
|
|
28
29
|
"@atproto/syntax": "^0.4.2",
|
|
29
30
|
"@atproto/ws-client": "^0.0.4"
|
|
30
31
|
},
|
package/src/client.ts
CHANGED
|
@@ -12,6 +12,9 @@ export class Tap {
|
|
|
12
12
|
private adminPassword?: string
|
|
13
13
|
private authHeader?: string
|
|
14
14
|
|
|
15
|
+
private addReposUrl: URL
|
|
16
|
+
private removeReposUrl: URL
|
|
17
|
+
|
|
15
18
|
constructor(url: string, config: TapConfig = {}) {
|
|
16
19
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
17
20
|
throw new Error('Invalid URL, expected http:// or https://')
|
|
@@ -21,6 +24,9 @@ export class Tap {
|
|
|
21
24
|
if (this.adminPassword) {
|
|
22
25
|
this.authHeader = formatAdminAuthHeader(this.adminPassword)
|
|
23
26
|
}
|
|
27
|
+
|
|
28
|
+
this.addReposUrl = new URL('/repos/add', this.url)
|
|
29
|
+
this.removeReposUrl = new URL('/repos/remove', this.url)
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
private getHeaders(): Record<string, string> {
|
|
@@ -44,7 +50,7 @@ export class Tap {
|
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
async addRepos(dids: string[]): Promise<void> {
|
|
47
|
-
const response = await fetch(
|
|
53
|
+
const response = await fetch(this.addReposUrl, {
|
|
48
54
|
method: 'POST',
|
|
49
55
|
headers: this.getHeaders(),
|
|
50
56
|
body: JSON.stringify({ dids }),
|
|
@@ -57,7 +63,7 @@ export class Tap {
|
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
async removeRepos(dids: string[]): Promise<void> {
|
|
60
|
-
const response = await fetch(
|
|
66
|
+
const response = await fetch(this.removeReposUrl, {
|
|
61
67
|
method: 'POST',
|
|
62
68
|
headers: this.getHeaders(),
|
|
63
69
|
body: JSON.stringify({ dids }),
|
|
@@ -70,7 +76,7 @@ export class Tap {
|
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
async resolveDid(did: string): Promise<DidDocument | null> {
|
|
73
|
-
const response = await fetch(
|
|
79
|
+
const response = await fetch(new URL(`/resolve/${did}`, this.url), {
|
|
74
80
|
method: 'GET',
|
|
75
81
|
headers: this.getHeaders(),
|
|
76
82
|
})
|
|
@@ -85,7 +91,7 @@ export class Tap {
|
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
async getRepoInfo(did: string): Promise<RepoInfo> {
|
|
88
|
-
const response = await fetch(
|
|
94
|
+
const response = await fetch(new URL(`/info/${did}`, this.url), {
|
|
89
95
|
method: 'GET',
|
|
90
96
|
headers: this.getHeaders(),
|
|
91
97
|
})
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Infer, Main, RecordSchema, getMain } from '@atproto/lex'
|
|
2
|
+
import { AtUri } from '@atproto/syntax'
|
|
3
|
+
import { HandlerOpts, TapHandler } from './channel'
|
|
4
|
+
import { IdentityEvent, RecordEvent, TapEvent } from './types'
|
|
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(collection: string, action: string): string {
|
|
77
|
+
return `${collection}:${action}`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private register<const T extends RecordSchema>(
|
|
81
|
+
action: string,
|
|
82
|
+
ns: Main<T>,
|
|
83
|
+
handler: RecordHandler<Infer<T>>,
|
|
84
|
+
): this {
|
|
85
|
+
const schema = getMain(ns)
|
|
86
|
+
const key = this.handlerKey(schema.$type, action)
|
|
87
|
+
if (this.handlers.has(key)) {
|
|
88
|
+
throw new Error(`Handler already registered for ${key}`)
|
|
89
|
+
}
|
|
90
|
+
this.handlers.set(key, { schema, handler })
|
|
91
|
+
return this
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
create<const T extends RecordSchema>(
|
|
95
|
+
ns: Main<T>,
|
|
96
|
+
handler: CreateHandler<Infer<T>>,
|
|
97
|
+
): this {
|
|
98
|
+
return this.register('create', ns, handler)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
update<const T extends RecordSchema>(
|
|
102
|
+
ns: Main<T>,
|
|
103
|
+
handler: UpdateHandler<Infer<T>>,
|
|
104
|
+
): this {
|
|
105
|
+
return this.register('update', ns, handler)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
delete<const T extends RecordSchema>(
|
|
109
|
+
ns: Main<T>,
|
|
110
|
+
handler: DeleteHandler,
|
|
111
|
+
): this {
|
|
112
|
+
return this.register('delete', ns, handler)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
put<const T extends RecordSchema>(
|
|
116
|
+
ns: Main<T>,
|
|
117
|
+
handler: PutHandler<Infer<T>>,
|
|
118
|
+
): this {
|
|
119
|
+
this.register('create', ns, handler)
|
|
120
|
+
return this.register('update', ns, handler)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
other(fn: UntypedHandler): this {
|
|
124
|
+
if (this.otherHandler) {
|
|
125
|
+
throw new Error(`Handler already registered for "other"`)
|
|
126
|
+
}
|
|
127
|
+
this.otherHandler = fn
|
|
128
|
+
return this
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
identity(fn: IdentityHandler): this {
|
|
132
|
+
if (this.identityHandler) {
|
|
133
|
+
throw new Error(`Handler already registered for "identity"`)
|
|
134
|
+
}
|
|
135
|
+
this.identityHandler = fn
|
|
136
|
+
return this
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
error(fn: ErrorHandler): this {
|
|
140
|
+
if (this.errorHandler) {
|
|
141
|
+
throw new Error(`Handler already registered for "error"`)
|
|
142
|
+
}
|
|
143
|
+
this.errorHandler = fn
|
|
144
|
+
return this
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async onEvent(evt: TapEvent, opts: HandlerOpts): Promise<void> {
|
|
148
|
+
if (evt.type === 'identity') {
|
|
149
|
+
await this.identityHandler?.(evt, opts)
|
|
150
|
+
} else {
|
|
151
|
+
await this.handleRecordEvent(evt, opts)
|
|
152
|
+
}
|
|
153
|
+
await opts.ack()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async handleRecordEvent(
|
|
157
|
+
evt: RecordEvent,
|
|
158
|
+
opts: HandlerOpts,
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
const { collection, action } = evt
|
|
161
|
+
const key = this.handlerKey(collection, action)
|
|
162
|
+
const registered = this.handlers.get(key)
|
|
163
|
+
|
|
164
|
+
if (!registered) {
|
|
165
|
+
await this.otherHandler?.(evt, opts)
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
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
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await (registered.handler as UntypedHandler)(evt, opts)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
onError(err: Error): void {
|
|
181
|
+
if (this.errorHandler) {
|
|
182
|
+
this.errorHandler(err)
|
|
183
|
+
} else {
|
|
184
|
+
throw err
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/simple-indexer.ts
CHANGED
|
@@ -5,10 +5,12 @@ type IdentityEventHandler = (
|
|
|
5
5
|
evt: IdentityEvent,
|
|
6
6
|
opts?: HandlerOpts,
|
|
7
7
|
) => Promise<void>
|
|
8
|
+
|
|
8
9
|
type RecordEventHandler = (
|
|
9
10
|
evt: RecordEvent,
|
|
10
11
|
opts?: HandlerOpts,
|
|
11
12
|
) => Promise<void>
|
|
13
|
+
|
|
12
14
|
type ErrorHandler = (err: Error) => void
|
|
13
15
|
|
|
14
16
|
export class SimpleIndexer implements TapHandler {
|
|
@@ -16,16 +18,19 @@ export class SimpleIndexer implements TapHandler {
|
|
|
16
18
|
private recordHandler: RecordEventHandler | undefined
|
|
17
19
|
private errorHandler: ErrorHandler | undefined
|
|
18
20
|
|
|
19
|
-
identity(fn: IdentityEventHandler) {
|
|
21
|
+
identity(fn: IdentityEventHandler): this {
|
|
20
22
|
this.identityHandler = fn
|
|
23
|
+
return this
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
record(fn: RecordEventHandler) {
|
|
26
|
+
record(fn: RecordEventHandler): this {
|
|
24
27
|
this.recordHandler = fn
|
|
28
|
+
return this
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
error(fn: ErrorHandler) {
|
|
31
|
+
error(fn: ErrorHandler): this {
|
|
28
32
|
this.errorHandler = fn
|
|
33
|
+
return this
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
async onEvent(evt: TapEvent, opts: HandlerOpts): Promise<void> {
|
package/tests/_util.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { HandlerOpts } from '../src/channel'
|
|
2
|
+
import { IdentityEvent, RecordEvent } from '../src/types'
|
|
3
|
+
|
|
4
|
+
export type MockOpts = HandlerOpts & { acked: boolean }
|
|
5
|
+
|
|
6
|
+
export const createMockOpts = (): MockOpts => {
|
|
7
|
+
const opts = {
|
|
8
|
+
signal: new AbortController().signal,
|
|
9
|
+
acked: false,
|
|
10
|
+
ack: async () => {
|
|
11
|
+
opts.acked = true
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
return opts
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const createRecordEvent = (
|
|
18
|
+
overrides: Partial<RecordEvent> = {},
|
|
19
|
+
): RecordEvent => ({
|
|
20
|
+
id: 1,
|
|
21
|
+
type: 'record',
|
|
22
|
+
did: 'did:example:alice',
|
|
23
|
+
rev: 'abc123',
|
|
24
|
+
collection: 'com.example.post',
|
|
25
|
+
rkey: 'abc123',
|
|
26
|
+
action: 'create',
|
|
27
|
+
record: { text: 'hello' },
|
|
28
|
+
cid: 'bafyabc',
|
|
29
|
+
live: true,
|
|
30
|
+
...overrides,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export const createIdentityEvent = (): IdentityEvent => ({
|
|
34
|
+
id: 2,
|
|
35
|
+
type: 'identity',
|
|
36
|
+
did: 'did:example:alice',
|
|
37
|
+
handle: 'alice.test',
|
|
38
|
+
isActive: true,
|
|
39
|
+
status: 'active',
|
|
40
|
+
})
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { l } from '@atproto/lex'
|
|
2
|
+
import {
|
|
3
|
+
CreateEvent,
|
|
4
|
+
DeleteEvent,
|
|
5
|
+
LexIndexer,
|
|
6
|
+
UpdateEvent,
|
|
7
|
+
} from '../src/lex-indexer'
|
|
8
|
+
import { IdentityEvent, RecordEvent } from '../src/types'
|
|
9
|
+
import {
|
|
10
|
+
createIdentityEvent,
|
|
11
|
+
createMockOpts,
|
|
12
|
+
createRecordEvent as baseCreateRecordEvent,
|
|
13
|
+
} from './_util'
|
|
14
|
+
|
|
15
|
+
// Test lexicon definitions
|
|
16
|
+
const postNsid = 'com.example.post'
|
|
17
|
+
type Post = {
|
|
18
|
+
$type: 'com.example.post'
|
|
19
|
+
text: string
|
|
20
|
+
}
|
|
21
|
+
const post = {
|
|
22
|
+
main: l.record<'tid', Post>('tid', postNsid, l.object({ text: l.string() })),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const likeNsid = 'com.example.like'
|
|
26
|
+
type Like = {
|
|
27
|
+
$type: 'com.example.like'
|
|
28
|
+
subject: string
|
|
29
|
+
}
|
|
30
|
+
const like = {
|
|
31
|
+
main: l.record<'tid', Like>(
|
|
32
|
+
'tid',
|
|
33
|
+
likeNsid,
|
|
34
|
+
l.object({ subject: l.string() }),
|
|
35
|
+
),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const createRecordEvent = (overrides: Partial<RecordEvent> = {}): RecordEvent =>
|
|
39
|
+
baseCreateRecordEvent({
|
|
40
|
+
collection: postNsid,
|
|
41
|
+
record: { $type: postNsid, text: 'hello' },
|
|
42
|
+
...overrides,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('LexIndexer', () => {
|
|
46
|
+
describe('handler registration', () => {
|
|
47
|
+
it('registers create handler', async () => {
|
|
48
|
+
const indexer = new LexIndexer()
|
|
49
|
+
const received: CreateEvent<Post>[] = []
|
|
50
|
+
|
|
51
|
+
indexer.create(post, async (evt) => {
|
|
52
|
+
received.push(evt)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const opts = createMockOpts()
|
|
56
|
+
await indexer.onEvent(createRecordEvent(), opts)
|
|
57
|
+
|
|
58
|
+
expect(received).toHaveLength(1)
|
|
59
|
+
expect(received[0].action).toBe('create')
|
|
60
|
+
expect(received[0].record.text).toBe('hello')
|
|
61
|
+
expect(received[0].cid).toBe('bafyabc')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('registers update handler', async () => {
|
|
65
|
+
const indexer = new LexIndexer()
|
|
66
|
+
const received: UpdateEvent<Post>[] = []
|
|
67
|
+
|
|
68
|
+
indexer.update(post, async (evt) => {
|
|
69
|
+
received.push(evt)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const opts = createMockOpts()
|
|
73
|
+
await indexer.onEvent(
|
|
74
|
+
createRecordEvent({
|
|
75
|
+
action: 'update',
|
|
76
|
+
record: { $type: postNsid, text: 'updated' },
|
|
77
|
+
}),
|
|
78
|
+
opts,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
expect(received).toHaveLength(1)
|
|
82
|
+
expect(received[0].action).toBe('update')
|
|
83
|
+
expect(received[0].record.text).toBe('updated')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('registers delete handler', async () => {
|
|
87
|
+
const indexer = new LexIndexer()
|
|
88
|
+
const received: DeleteEvent[] = []
|
|
89
|
+
|
|
90
|
+
indexer.delete(post, async (evt) => {
|
|
91
|
+
received.push(evt)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const opts = createMockOpts()
|
|
95
|
+
await indexer.onEvent(
|
|
96
|
+
createRecordEvent({
|
|
97
|
+
action: 'delete',
|
|
98
|
+
record: undefined,
|
|
99
|
+
cid: undefined,
|
|
100
|
+
}),
|
|
101
|
+
opts,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
expect(received).toHaveLength(1)
|
|
105
|
+
expect(received[0].action).toBe('delete')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('registers put handler for both create and update', async () => {
|
|
109
|
+
const indexer = new LexIndexer()
|
|
110
|
+
const received: Array<{ action: string; text: string }> = []
|
|
111
|
+
|
|
112
|
+
indexer.put(post, async (evt) => {
|
|
113
|
+
received.push({ action: evt.action, text: evt.record.text })
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const opts1 = createMockOpts()
|
|
117
|
+
await indexer.onEvent(createRecordEvent({ action: 'create' }), opts1)
|
|
118
|
+
|
|
119
|
+
const opts2 = createMockOpts()
|
|
120
|
+
await indexer.onEvent(
|
|
121
|
+
createRecordEvent({
|
|
122
|
+
action: 'update',
|
|
123
|
+
record: { $type: postNsid, text: 'updated' },
|
|
124
|
+
}),
|
|
125
|
+
opts2,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
expect(received).toHaveLength(2)
|
|
129
|
+
expect(received[0].action).toBe('create')
|
|
130
|
+
expect(received[1].action).toBe('update')
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('handler routing', () => {
|
|
135
|
+
it('routes to correct handler by collection', async () => {
|
|
136
|
+
const indexer = new LexIndexer()
|
|
137
|
+
const postEvents: CreateEvent<Post>[] = []
|
|
138
|
+
const likeEvents: CreateEvent<Like>[] = []
|
|
139
|
+
|
|
140
|
+
indexer.create(post, async (evt) => {
|
|
141
|
+
postEvents.push(evt)
|
|
142
|
+
})
|
|
143
|
+
indexer.create(like, async (evt) => {
|
|
144
|
+
likeEvents.push(evt)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const opts1 = createMockOpts()
|
|
148
|
+
await indexer.onEvent(createRecordEvent(), opts1)
|
|
149
|
+
|
|
150
|
+
const opts2 = createMockOpts()
|
|
151
|
+
await indexer.onEvent(
|
|
152
|
+
createRecordEvent({
|
|
153
|
+
collection: likeNsid,
|
|
154
|
+
record: { $type: likeNsid, subject: 'at://did:example:bob/post/123' },
|
|
155
|
+
}),
|
|
156
|
+
opts2,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
expect(postEvents).toHaveLength(1)
|
|
160
|
+
expect(likeEvents).toHaveLength(1)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('routes to other handler for unregistered collections', async () => {
|
|
164
|
+
const indexer = new LexIndexer()
|
|
165
|
+
const otherEvents: RecordEvent[] = []
|
|
166
|
+
|
|
167
|
+
indexer.create(post, async () => {})
|
|
168
|
+
indexer.other(async (evt) => {
|
|
169
|
+
otherEvents.push(evt)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const opts = createMockOpts()
|
|
173
|
+
await indexer.onEvent(
|
|
174
|
+
createRecordEvent({ collection: 'com.example.unknown' }),
|
|
175
|
+
opts,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
expect(otherEvents).toHaveLength(1)
|
|
179
|
+
expect(otherEvents[0].collection).toBe('com.example.unknown')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('routes to other handler for unregistered actions', async () => {
|
|
183
|
+
const indexer = new LexIndexer()
|
|
184
|
+
const otherEvents: RecordEvent[] = []
|
|
185
|
+
|
|
186
|
+
indexer.create(post, async () => {})
|
|
187
|
+
indexer.other(async (evt) => {
|
|
188
|
+
otherEvents.push(evt)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const opts = createMockOpts()
|
|
192
|
+
await indexer.onEvent(createRecordEvent({ action: 'delete' }), opts)
|
|
193
|
+
|
|
194
|
+
expect(otherEvents).toHaveLength(1)
|
|
195
|
+
expect(otherEvents[0].action).toBe('delete')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('routes identity events to identity handler', async () => {
|
|
199
|
+
const indexer = new LexIndexer()
|
|
200
|
+
const received: IdentityEvent[] = []
|
|
201
|
+
|
|
202
|
+
indexer.identity(async (evt) => {
|
|
203
|
+
received.push(evt)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const opts = createMockOpts()
|
|
207
|
+
await indexer.onEvent(createIdentityEvent(), opts)
|
|
208
|
+
|
|
209
|
+
expect(received).toHaveLength(1)
|
|
210
|
+
expect(received[0].handle).toBe('alice.test')
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('duplicate registration', () => {
|
|
215
|
+
it('throws on duplicate create handler', () => {
|
|
216
|
+
const indexer = new LexIndexer()
|
|
217
|
+
indexer.create(post, async () => {})
|
|
218
|
+
|
|
219
|
+
expect(() => indexer.create(post, async () => {})).toThrow(
|
|
220
|
+
'Handler already registered',
|
|
221
|
+
)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('throws on duplicate update handler', () => {
|
|
225
|
+
const indexer = new LexIndexer()
|
|
226
|
+
indexer.update(post, async () => {})
|
|
227
|
+
|
|
228
|
+
expect(() => indexer.update(post, async () => {})).toThrow(
|
|
229
|
+
'Handler already registered',
|
|
230
|
+
)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('throws on duplicate delete handler', () => {
|
|
234
|
+
const indexer = new LexIndexer()
|
|
235
|
+
indexer.delete(post, async () => {})
|
|
236
|
+
|
|
237
|
+
expect(() => indexer.delete(post, async () => {})).toThrow(
|
|
238
|
+
'Handler already registered',
|
|
239
|
+
)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('throws when put conflicts with create', () => {
|
|
243
|
+
const indexer = new LexIndexer()
|
|
244
|
+
indexer.create(post, async () => {})
|
|
245
|
+
|
|
246
|
+
expect(() => indexer.put(post, async () => {})).toThrow(
|
|
247
|
+
'Handler already registered',
|
|
248
|
+
)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('throws when create conflicts with put', () => {
|
|
252
|
+
const indexer = new LexIndexer()
|
|
253
|
+
indexer.put(post, async () => {})
|
|
254
|
+
|
|
255
|
+
expect(() => indexer.create(post, async () => {})).toThrow(
|
|
256
|
+
'Handler already registered',
|
|
257
|
+
)
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('schema validation', () => {
|
|
262
|
+
it('validates record on create', async () => {
|
|
263
|
+
const indexer = new LexIndexer()
|
|
264
|
+
indexer.create(post, async () => {})
|
|
265
|
+
|
|
266
|
+
const opts = createMockOpts()
|
|
267
|
+
await expect(
|
|
268
|
+
indexer.onEvent(createRecordEvent({ record: { text: 123 } }), opts),
|
|
269
|
+
).rejects.toThrow('Record validation failed')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('validates record on update', async () => {
|
|
273
|
+
const indexer = new LexIndexer()
|
|
274
|
+
indexer.update(post, async () => {})
|
|
275
|
+
|
|
276
|
+
const opts = createMockOpts()
|
|
277
|
+
await expect(
|
|
278
|
+
indexer.onEvent(
|
|
279
|
+
createRecordEvent({ action: 'update', record: { invalid: true } }),
|
|
280
|
+
opts,
|
|
281
|
+
),
|
|
282
|
+
).rejects.toThrow('Record validation failed')
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
describe('ack behavior', () => {
|
|
287
|
+
it('calls ack after handler completes', async () => {
|
|
288
|
+
const indexer = new LexIndexer()
|
|
289
|
+
indexer.create(post, async () => {})
|
|
290
|
+
|
|
291
|
+
const opts = createMockOpts()
|
|
292
|
+
await indexer.onEvent(createRecordEvent(), opts)
|
|
293
|
+
|
|
294
|
+
expect(opts.acked).toBe(true)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('calls ack when routed to other handler', async () => {
|
|
298
|
+
const indexer = new LexIndexer()
|
|
299
|
+
indexer.other(async () => {})
|
|
300
|
+
|
|
301
|
+
const opts = createMockOpts()
|
|
302
|
+
await indexer.onEvent(createRecordEvent(), opts)
|
|
303
|
+
|
|
304
|
+
expect(opts.acked).toBe(true)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('calls ack even when no handler matches', async () => {
|
|
308
|
+
const indexer = new LexIndexer()
|
|
309
|
+
|
|
310
|
+
const opts = createMockOpts()
|
|
311
|
+
await indexer.onEvent(createRecordEvent(), opts)
|
|
312
|
+
|
|
313
|
+
expect(opts.acked).toBe(true)
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
describe('error handling', () => {
|
|
318
|
+
it('calls error handler when provided', () => {
|
|
319
|
+
const indexer = new LexIndexer()
|
|
320
|
+
const errors: Error[] = []
|
|
321
|
+
|
|
322
|
+
indexer.error((err) => {
|
|
323
|
+
errors.push(err)
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
const testError = new Error('test error')
|
|
327
|
+
indexer.onError(testError)
|
|
328
|
+
|
|
329
|
+
expect(errors).toHaveLength(1)
|
|
330
|
+
expect(errors[0]).toBe(testError)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('throws when no error handler is registered', () => {
|
|
334
|
+
const indexer = new LexIndexer()
|
|
335
|
+
const testError = new Error('test error')
|
|
336
|
+
|
|
337
|
+
expect(() => indexer.onError(testError)).toThrow('test error')
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
})
|
|
@@ -1,39 +1,7 @@
|
|
|
1
1
|
import { HandlerOpts } from '../src/channel'
|
|
2
2
|
import { SimpleIndexer } from '../src/simple-indexer'
|
|
3
3
|
import { IdentityEvent, RecordEvent } from '../src/types'
|
|
4
|
-
|
|
5
|
-
const createMockOpts = (): HandlerOpts & { acked: boolean } => {
|
|
6
|
-
const opts = {
|
|
7
|
-
signal: new AbortController().signal,
|
|
8
|
-
acked: false,
|
|
9
|
-
ack: async () => {
|
|
10
|
-
opts.acked = true
|
|
11
|
-
},
|
|
12
|
-
}
|
|
13
|
-
return opts
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const createRecordEvent = (): RecordEvent => ({
|
|
17
|
-
id: 1,
|
|
18
|
-
type: 'record',
|
|
19
|
-
did: 'did:example:alice',
|
|
20
|
-
rev: 'abc123',
|
|
21
|
-
collection: 'com.example.post',
|
|
22
|
-
rkey: 'abc123',
|
|
23
|
-
action: 'create',
|
|
24
|
-
record: { text: 'hello' },
|
|
25
|
-
cid: 'bafyabc',
|
|
26
|
-
live: true,
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
const createIdentityEvent = (): IdentityEvent => ({
|
|
30
|
-
id: 2,
|
|
31
|
-
type: 'identity',
|
|
32
|
-
did: 'did:example:alice',
|
|
33
|
-
handle: 'alice.test',
|
|
34
|
-
isActive: true,
|
|
35
|
-
status: 'active',
|
|
36
|
-
})
|
|
4
|
+
import { createIdentityEvent, createMockOpts, createRecordEvent } from './_util'
|
|
37
5
|
|
|
38
6
|
describe('SimpleIndexer', () => {
|
|
39
7
|
describe('event routing', () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/channel.ts","./src/client.ts","./src/index.ts","./src/simple-indexer.ts","./src/types.ts","./src/util.ts"],"version":"5.8.3"}
|
|
1
|
+
{"root":["./src/channel.ts","./src/client.ts","./src/index.ts","./src/lex-indexer.ts","./src/simple-indexer.ts","./src/types.ts","./src/util.ts"],"version":"5.8.3"}
|