@atproto/tap 0.0.3 → 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 CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## 0.0.3
4
10
 
5
11
  ### 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/index.d.ts CHANGED
@@ -2,5 +2,6 @@ export * from './types';
2
2
  export * from './client';
3
3
  export * from './channel';
4
4
  export * from './simple-indexer';
5
+ export * from './lex-indexer';
5
6
  export * from './util';
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -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":"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"}
@@ -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"]}
@@ -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): void;
11
- record(fn: RecordEventHandler): void;
12
- error(fn: ErrorHandler): void;
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;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"}
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"}
@@ -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":";;;AAaA,MAAa,aAAa;IAA1B;QACU;;;;;WAAiD;QACjD;;;;;WAA6C;QAC7C;;;;;WAAsC;IA8BhD,CAAC;IA5BC,QAAQ,CAAC,EAAwB;QAC/B,IAAI,CAAC,eAAe,GAAG,EAAE,CAAA;IAC3B,CAAC;IAED,MAAM,CAAC,EAAsB;QAC3B,IAAI,CAAC,aAAa,GAAG,EAAE,CAAA;IACzB,CAAC;IAED,KAAK,CAAC,EAAgB;QACpB,IAAI,CAAC,YAAY,GAAG,EAAE,CAAA;IACxB,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;AAjCD,sCAiCC","sourcesContent":["import { HandlerOpts, TapHandler } from './channel'\nimport { IdentityEvent, RecordEvent, TapEvent } from './types'\n\ntype IdentityEventHandler = (\n evt: IdentityEvent,\n opts?: HandlerOpts,\n) => Promise<void>\ntype RecordEventHandler = (\n evt: RecordEvent,\n opts?: HandlerOpts,\n) => Promise<void>\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) {\n this.identityHandler = fn\n }\n\n record(fn: RecordEventHandler) {\n this.recordHandler = fn\n }\n\n error(fn: ErrorHandler) {\n this.errorHandler = fn\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"]}
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",
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.4",
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/index.ts CHANGED
@@ -2,4 +2,5 @@ export * from './types'
2
2
  export * from './client'
3
3
  export * from './channel'
4
4
  export * from './simple-indexer'
5
+ export * from './lex-indexer'
5
6
  export * from './util'
@@ -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
+ }
@@ -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"}