@atproto/tap 0.0.0-spaces-alpha-20260818163953

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.
@@ -0,0 +1,57 @@
1
+ import { type HeadersInit } from '@atproto/ws-client';
2
+ import { type TapEvent } from './types.js';
3
+ export interface HandlerOpts {
4
+ signal: AbortSignal;
5
+ ack: () => Promise<void>;
6
+ }
7
+ export interface TapHandler {
8
+ onEvent: (evt: TapEvent, opts: HandlerOpts) => void | Promise<void>;
9
+ onError: (err: Error) => void;
10
+ }
11
+ export type TapWebsocketOptions = {
12
+ adminPassword?: string;
13
+ maxReconnectSeconds?: number;
14
+ heartbeatIntervalMs?: number;
15
+ /** Applied to the connection's upgrade request (Node.js only). */
16
+ headers?: HeadersInit;
17
+ onReconnectError?: (error: unknown, n: number, initialSetup: boolean) => void;
18
+ };
19
+ export declare class TapChannel implements AsyncDisposable {
20
+ private ws;
21
+ private handler;
22
+ private readonly abortController;
23
+ private readonly destroyDefer;
24
+ /** The current connection's sender, or undefined between connections. */
25
+ private sender?;
26
+ /**
27
+ * Acks accepted while there was no connection to send them on, flushed by the
28
+ * `onConnect` hook below.
29
+ *
30
+ * Not a list of promises the caller awaits, deliberately: a handler runs inside
31
+ * the iteration, and the reconnect that would flush it only happens when the
32
+ * iteration advances — so awaiting delivery from a handler would block the very
33
+ * pull that makes delivery possible. Acks are recorded and the handler
34
+ * continues; Tap redelivers anything it never sees acked.
35
+ */
36
+ private readonly pendingAcks;
37
+ constructor(url: string, handler: TapHandler, wsOpts?: TapWebsocketOptions);
38
+ /**
39
+ * Records an ack for delivery. Resolves once the ack has been sent, or accepted
40
+ * for the next connection — not once the peer has processed it.
41
+ *
42
+ * It can't mean more than that: a handler runs inside the iteration, and a
43
+ * reconnect only happens when the iteration advances, so awaiting confirmed
44
+ * delivery here would block the pull delivery depends on. Tap's own
45
+ * at-least-once redelivery covers an ack lost with its connection.
46
+ *
47
+ * Acks are per-event, idempotent, and order-insensitive server-side (see
48
+ * Outbox.AckEvent), so a duplicate or out-of-order ack is harmless.
49
+ */
50
+ ackEvent(id: number): Promise<void>;
51
+ private flushPendingAcks;
52
+ start(): Promise<void>;
53
+ private processWsEvent;
54
+ destroy(): Promise<void>;
55
+ [Symbol.asyncDispose](): Promise<void>;
56
+ }
57
+ //# sourceMappingURL=channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,WAAW,EAOjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,KAAK,QAAQ,EAAiB,MAAM,YAAY,CAAA;AAsBzD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,WAAW,CAAA;IACnB,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnE,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;CAC9B;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,kEAAkE;IAClE,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,KAAK,IAAI,CAAA;CAC9E,CAAA;AAMD,qBAAa,UAAW,YAAW,eAAe;IAChD,OAAO,CAAC,EAAE,CAA2B;IACrC,OAAO,CAAC,OAAO,CAAY;IAE3B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyC;IACzE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiC;IAE9D,yEAAyE;IACzE,OAAO,CAAC,MAAM,CAAC,CAAgB;IAC/B;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoB;IAEhD,YACE,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,UAAU,EACnB,MAAM,GAAE,mBAAwB,EAiCjC;IAED;;;;;;;;;;;OAWG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAaxC;YAIa,gBAAgB;IAiBxB,KAAK,kBAoBV;YAEa,cAAc;IA6BtB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAG7B;IAEK,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3C;CACF"}
@@ -0,0 +1,175 @@
1
+ import { createDeferrable } from '@atproto/common';
2
+ import { lexParse } from '@atproto/lex';
3
+ import { CloseCode, CloseError, HeartbeatTimeoutError, IdleTimeoutError, SocketError, websocket, } from '@atproto/ws-client';
4
+ import { parseTapEvent } from './types.js';
5
+ import { formatAdminAuthHeader, isCausedBySignal } from './util.js';
6
+ // Matches the classification the legacy errno-matching client had: reconnect only
7
+ // on a genuine transport failure or an abnormal close (1006). Any other close
8
+ // code, including the synthetic 1005 a bare socket.close() produces, ends the
9
+ // session as before.
10
+ function shouldReconnect(error) {
11
+ if (error instanceof CloseError) {
12
+ return error.code === CloseCode.Abnormal;
13
+ }
14
+ if (error instanceof SocketError ||
15
+ error instanceof HeartbeatTimeoutError ||
16
+ error instanceof IdleTimeoutError) {
17
+ // Defer to each error's own answer so this can't drift from the taxonomy.
18
+ return error.shouldRetry();
19
+ }
20
+ return false;
21
+ }
22
+ function ackMessage(id) {
23
+ return JSON.stringify({ type: 'ack', id });
24
+ }
25
+ export class TapChannel {
26
+ constructor(url, handler, wsOpts = {}) {
27
+ this.abortController = new AbortController();
28
+ this.destroyDefer = createDeferrable();
29
+ /**
30
+ * Acks accepted while there was no connection to send them on, flushed by the
31
+ * `onConnect` hook below.
32
+ *
33
+ * Not a list of promises the caller awaits, deliberately: a handler runs inside
34
+ * the iteration, and the reconnect that would flush it only happens when the
35
+ * iteration advances — so awaiting delivery from a handler would block the very
36
+ * pull that makes delivery possible. Acks are recorded and the handler
37
+ * continues; Tap redelivers anything it never sees acked.
38
+ */
39
+ this.pendingAcks = new Set();
40
+ this.handler = handler;
41
+ const { adminPassword, onReconnectError } = wsOpts;
42
+ const headers = new Headers(wsOpts.headers);
43
+ if (adminPassword) {
44
+ headers.set('Authorization', formatAdminAuthHeader(adminPassword));
45
+ }
46
+ this.ws = websocket(url, {
47
+ dataMode: 'text',
48
+ headers,
49
+ maxReconnectSeconds: wsOpts.maxReconnectSeconds,
50
+ // `{}` means "on at the default interval"; see Subscription.
51
+ heartbeat: wsOpts.heartbeatIntervalMs
52
+ ? { intervalMs: wsOpts.heartbeatIntervalMs }
53
+ : {},
54
+ signal: this.abortController.signal,
55
+ shouldReconnect,
56
+ // Fires for every connection, the first included, so an ack recorded before
57
+ // anything ever connected is flushed by whichever connection comes up.
58
+ onConnect: (sender) => {
59
+ this.sender = sender;
60
+ void this.flushPendingAcks();
61
+ },
62
+ onDisconnect: () => {
63
+ this.sender = undefined;
64
+ },
65
+ // Only retries are reported here; a fatal error reaches `start()` as the
66
+ // iterator's rejection instead.
67
+ onReconnect: (error, { attempt }) => {
68
+ onReconnectError?.(error, attempt, attempt === 0);
69
+ },
70
+ });
71
+ }
72
+ /**
73
+ * Records an ack for delivery. Resolves once the ack has been sent, or accepted
74
+ * for the next connection — not once the peer has processed it.
75
+ *
76
+ * It can't mean more than that: a handler runs inside the iteration, and a
77
+ * reconnect only happens when the iteration advances, so awaiting confirmed
78
+ * delivery here would block the pull delivery depends on. Tap's own
79
+ * at-least-once redelivery covers an ack lost with its connection.
80
+ *
81
+ * Acks are per-event, idempotent, and order-insensitive server-side (see
82
+ * Outbox.AckEvent), so a duplicate or out-of-order ack is harmless.
83
+ */
84
+ async ackEvent(id) {
85
+ const sender = this.sender;
86
+ if (!sender) {
87
+ this.pendingAcks.add(id);
88
+ return;
89
+ }
90
+ try {
91
+ await sender.send(ackMessage(id));
92
+ }
93
+ catch {
94
+ // The connection died under us: hand the ack to the next one rather than
95
+ // reporting a failure the caller can do nothing about.
96
+ this.pendingAcks.add(id);
97
+ }
98
+ }
99
+ // Driven by `onConnect`, never by a handler: it runs off the socket's own event,
100
+ // so it doesn't depend on the iteration making progress.
101
+ async flushPendingAcks() {
102
+ const sender = this.sender;
103
+ if (!sender)
104
+ return;
105
+ for (const id of [...this.pendingAcks]) {
106
+ try {
107
+ await sender.send(ackMessage(id));
108
+ this.pendingAcks.delete(id);
109
+ }
110
+ catch (cause) {
111
+ // Still unsendable; leave it queued for the connection after this one.
112
+ this.handler.onError(new Error(`failed to send ack for event ${id}`, { cause }));
113
+ return;
114
+ }
115
+ }
116
+ }
117
+ async start() {
118
+ this.abortController.signal.throwIfAborted();
119
+ try {
120
+ for await (const chunk of this.ws) {
121
+ await this.processWsEvent(chunk);
122
+ }
123
+ }
124
+ catch (err) {
125
+ // A CloseError only reaches here once `shouldReconnect` has declined to
126
+ // retry — i.e. the peer ended the session with an ordinary close frame.
127
+ // That's an expected end, matching the previous client's behavior of
128
+ // ending silently on any non-abnormal close.
129
+ if (!isCausedBySignal(err, this.abortController.signal) &&
130
+ !(err instanceof CloseError)) {
131
+ throw err;
132
+ }
133
+ }
134
+ finally {
135
+ this.destroyDefer.resolve();
136
+ }
137
+ }
138
+ async processWsEvent(chunk) {
139
+ let evt;
140
+ try {
141
+ const data = lexParse(chunk, {
142
+ // Reject invalid CIDs and blobs
143
+ strict: true,
144
+ });
145
+ evt = parseTapEvent(data);
146
+ }
147
+ catch (cause) {
148
+ const error = new Error(`Failed to parse message`, { cause });
149
+ this.handler.onError(error);
150
+ return;
151
+ }
152
+ try {
153
+ await this.handler.onEvent(evt, {
154
+ signal: this.abortController.signal,
155
+ ack: async () => {
156
+ await this.ackEvent(evt.id);
157
+ },
158
+ });
159
+ }
160
+ catch (cause) {
161
+ // Don't ack on error - let Tap retry
162
+ const error = new Error(`Failed to process event ${evt.id}`, { cause });
163
+ this.handler.onError(error);
164
+ return;
165
+ }
166
+ }
167
+ async destroy() {
168
+ this.abortController.abort();
169
+ await this.destroyDefer.complete;
170
+ }
171
+ async [Symbol.asyncDispose]() {
172
+ await this.destroy();
173
+ }
174
+ }
175
+ //# sourceMappingURL=channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AACnE,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EACL,SAAS,EACT,UAAU,EAEV,qBAAqB,EACrB,gBAAgB,EAEhB,WAAW,EAEX,SAAS,GACV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAiB,aAAa,EAAE,MAAM,YAAY,CAAA;AACzD,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAEnE,kFAAkF;AAClF,8EAA8E;AAC9E,8EAA8E;AAC9E,qBAAqB;AACrB,SAAS,eAAe,CAAC,KAAc;IACrC,IAAI,KAAK,YAAY,UAAU,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,QAAQ,CAAA;IAC1C,CAAC;IACD,IACE,KAAK,YAAY,WAAW;QAC5B,KAAK,YAAY,qBAAqB;QACtC,KAAK,YAAY,gBAAgB,EACjC,CAAC;QACD,0EAA0E;QAC1E,OAAO,KAAK,CAAC,WAAW,EAAE,CAAA;IAC5B,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAqBD,SAAS,UAAU,CAAC,EAAU;IAC5B,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAA;AAC5C,CAAC;AAED,MAAM,OAAO,UAAU;IAqBrB,YACE,GAAW,EACX,OAAmB,EACnB,MAAM,GAAwB,EAAE;QApBjB,oBAAe,GAAoB,IAAI,eAAe,EAAE,CAAA;QACxD,iBAAY,GAAe,gBAAgB,EAAE,CAAA;QAI9D;;;;;;;;;WASG;QACc,gBAAW,GAAG,IAAI,GAAG,EAAU,CAAA;QAO9C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,MAAM,EAAE,aAAa,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAAA;QAClD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QAC3C,IAAI,aAAa,EAAE,CAAC;YAClB,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAC,CAAA;QACpE,CAAC;QACD,IAAI,CAAC,EAAE,GAAG,SAAS,CAAC,GAAG,EAAE;YACvB,QAAQ,EAAE,MAAM;YAChB,OAAO;YACP,mBAAmB,EAAE,MAAM,CAAC,mBAAmB;YAC/C,6DAA6D;YAC7D,SAAS,EAAE,MAAM,CAAC,mBAAmB;gBACnC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,mBAAmB,EAAE;gBAC5C,CAAC,CAAC,EAAE;YACN,MAAM,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM;YACnC,eAAe;YACf,4EAA4E;YAC5E,uEAAuE;YACvE,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE;gBACpB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;gBACpB,KAAK,IAAI,CAAC,gBAAgB,EAAE,CAAA;YAC9B,CAAC;YACD,YAAY,EAAE,GAAG,EAAE;gBACjB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;YACzB,CAAC;YACD,yEAAyE;YACzE,gCAAgC;YAChC,WAAW,EAAE,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;gBAClC,gBAAgB,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,KAAK,CAAC,CAAC,CAAA;YACnD,CAAC;SACF,CAAC,CAAA;IACJ,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,QAAQ,CAAC,EAAU;QACvB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;QAC1B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YACxB,OAAM;QACR,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAA;QACnC,CAAC;QAAC,MAAM,CAAC;YACP,yEAAyE;YACzE,uDAAuD;YACvD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAC1B,CAAC;IACH,CAAC;IAED,iFAAiF;IACjF,yDAAyD;IACjD,KAAK,CAAC,gBAAgB;QAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;QAC1B,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;YACvC,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAA;gBACjC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;YAC7B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,uEAAuE;gBACvE,IAAI,CAAC,OAAO,CAAC,OAAO,CAClB,IAAI,KAAK,CAAC,gCAAgC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAC3D,CAAA;gBACD,OAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,cAAc,EAAE,CAAA;QAC5C,IAAI,CAAC;YACH,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;gBAClC,MAAM,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAA;YAClC,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,wEAAwE;YACxE,wEAAwE;YACxE,qEAAqE;YACrE,6CAA6C;YAC7C,IACE,CAAC,gBAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC;gBACnD,CAAC,CAAC,GAAG,YAAY,UAAU,CAAC,EAC5B,CAAC;gBACD,MAAM,GAAG,CAAA;YACX,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAA;QAC7B,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,cAAc,CAAC,KAAa;QACxC,IAAI,GAAa,CAAA;QACjB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,EAAE;gBAC3B,gCAAgC;gBAChC,MAAM,EAAE,IAAI;aACb,CAAC,CAAA;YACF,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,CAAA;QAC3B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,yBAAyB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;YAC7D,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;YAC3B,OAAM;QACR,CAAC;QAED,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE;gBAC9B,MAAM,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM;gBACnC,GAAG,EAAE,KAAK,IAAI,EAAE;oBACd,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;gBAC7B,CAAC;aACF,CAAC,CAAA;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,qCAAqC;YACrC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,2BAA2B,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;YACvE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;YAC3B,OAAM;QACR,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAA;QAC5B,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAA;IAClC,CAAC;IAED,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;QACzB,MAAM,IAAI,CAAC,OAAO,EAAE,CAAA;IACtB,CAAC;CACF","sourcesContent":["import { type Deferrable, createDeferrable } from '@atproto/common'\nimport { lexParse } from '@atproto/lex'\nimport {\n CloseCode,\n CloseError,\n type HeadersInit,\n HeartbeatTimeoutError,\n IdleTimeoutError,\n type Sender,\n SocketError,\n type WebSocketIterable,\n websocket,\n} from '@atproto/ws-client'\nimport { type TapEvent, parseTapEvent } from './types.js'\nimport { formatAdminAuthHeader, isCausedBySignal } from './util.js'\n\n// Matches the classification the legacy errno-matching client had: reconnect only\n// on a genuine transport failure or an abnormal close (1006). Any other close\n// code, including the synthetic 1005 a bare socket.close() produces, ends the\n// session as before.\nfunction shouldReconnect(error: unknown): boolean {\n if (error instanceof CloseError) {\n return error.code === CloseCode.Abnormal\n }\n if (\n error instanceof SocketError ||\n error instanceof HeartbeatTimeoutError ||\n error instanceof IdleTimeoutError\n ) {\n // Defer to each error's own answer so this can't drift from the taxonomy.\n return error.shouldRetry()\n }\n return false\n}\n\nexport interface HandlerOpts {\n signal: AbortSignal\n ack: () => Promise<void>\n}\n\nexport interface TapHandler {\n onEvent: (evt: TapEvent, opts: HandlerOpts) => void | Promise<void>\n onError: (err: Error) => void\n}\n\nexport type TapWebsocketOptions = {\n adminPassword?: string\n maxReconnectSeconds?: number\n heartbeatIntervalMs?: number\n /** Applied to the connection's upgrade request (Node.js only). */\n headers?: HeadersInit\n onReconnectError?: (error: unknown, n: number, initialSetup: boolean) => void\n}\n\nfunction ackMessage(id: number): string {\n return JSON.stringify({ type: 'ack', id })\n}\n\nexport class TapChannel implements AsyncDisposable {\n private ws: WebSocketIterable<'text'>\n private handler: TapHandler\n\n private readonly abortController: AbortController = new AbortController()\n private readonly destroyDefer: Deferrable = createDeferrable()\n\n /** The current connection's sender, or undefined between connections. */\n private sender?: Sender<'text'>\n /**\n * Acks accepted while there was no connection to send them on, flushed by the\n * `onConnect` hook below.\n *\n * Not a list of promises the caller awaits, deliberately: a handler runs inside\n * the iteration, and the reconnect that would flush it only happens when the\n * iteration advances — so awaiting delivery from a handler would block the very\n * pull that makes delivery possible. Acks are recorded and the handler\n * continues; Tap redelivers anything it never sees acked.\n */\n private readonly pendingAcks = new Set<number>()\n\n constructor(\n url: string,\n handler: TapHandler,\n wsOpts: TapWebsocketOptions = {},\n ) {\n this.handler = handler\n const { adminPassword, onReconnectError } = wsOpts\n const headers = new Headers(wsOpts.headers)\n if (adminPassword) {\n headers.set('Authorization', formatAdminAuthHeader(adminPassword))\n }\n this.ws = websocket(url, {\n dataMode: 'text',\n headers,\n maxReconnectSeconds: wsOpts.maxReconnectSeconds,\n // `{}` means \"on at the default interval\"; see Subscription.\n heartbeat: wsOpts.heartbeatIntervalMs\n ? { intervalMs: wsOpts.heartbeatIntervalMs }\n : {},\n signal: this.abortController.signal,\n shouldReconnect,\n // Fires for every connection, the first included, so an ack recorded before\n // anything ever connected is flushed by whichever connection comes up.\n onConnect: (sender) => {\n this.sender = sender\n void this.flushPendingAcks()\n },\n onDisconnect: () => {\n this.sender = undefined\n },\n // Only retries are reported here; a fatal error reaches `start()` as the\n // iterator's rejection instead.\n onReconnect: (error, { attempt }) => {\n onReconnectError?.(error, attempt, attempt === 0)\n },\n })\n }\n\n /**\n * Records an ack for delivery. Resolves once the ack has been sent, or accepted\n * for the next connection — not once the peer has processed it.\n *\n * It can't mean more than that: a handler runs inside the iteration, and a\n * reconnect only happens when the iteration advances, so awaiting confirmed\n * delivery here would block the pull delivery depends on. Tap's own\n * at-least-once redelivery covers an ack lost with its connection.\n *\n * Acks are per-event, idempotent, and order-insensitive server-side (see\n * Outbox.AckEvent), so a duplicate or out-of-order ack is harmless.\n */\n async ackEvent(id: number): Promise<void> {\n const sender = this.sender\n if (!sender) {\n this.pendingAcks.add(id)\n return\n }\n try {\n await sender.send(ackMessage(id))\n } catch {\n // The connection died under us: hand the ack to the next one rather than\n // reporting a failure the caller can do nothing about.\n this.pendingAcks.add(id)\n }\n }\n\n // Driven by `onConnect`, never by a handler: it runs off the socket's own event,\n // so it doesn't depend on the iteration making progress.\n private async flushPendingAcks(): Promise<void> {\n const sender = this.sender\n if (!sender) return\n for (const id of [...this.pendingAcks]) {\n try {\n await sender.send(ackMessage(id))\n this.pendingAcks.delete(id)\n } catch (cause) {\n // Still unsendable; leave it queued for the connection after this one.\n this.handler.onError(\n new Error(`failed to send ack for event ${id}`, { cause }),\n )\n return\n }\n }\n }\n\n async start() {\n this.abortController.signal.throwIfAborted()\n try {\n for await (const chunk of this.ws) {\n await this.processWsEvent(chunk)\n }\n } catch (err) {\n // A CloseError only reaches here once `shouldReconnect` has declined to\n // retry — i.e. the peer ended the session with an ordinary close frame.\n // That's an expected end, matching the previous client's behavior of\n // ending silently on any non-abnormal close.\n if (\n !isCausedBySignal(err, this.abortController.signal) &&\n !(err instanceof CloseError)\n ) {\n throw err\n }\n } finally {\n this.destroyDefer.resolve()\n }\n }\n\n private async processWsEvent(chunk: string) {\n let evt: TapEvent\n try {\n const data = lexParse(chunk, {\n // Reject invalid CIDs and blobs\n strict: true,\n })\n evt = parseTapEvent(data)\n } catch (cause) {\n const error = new Error(`Failed to parse message`, { cause })\n this.handler.onError(error)\n return\n }\n\n try {\n await this.handler.onEvent(evt, {\n signal: this.abortController.signal,\n ack: async () => {\n await this.ackEvent(evt.id)\n },\n })\n } catch (cause) {\n // Don't ack on error - let Tap retry\n const error = new Error(`Failed to process event ${evt.id}`, { cause })\n this.handler.onError(error)\n return\n }\n }\n\n async destroy(): Promise<void> {\n this.abortController.abort()\n await this.destroyDefer.complete\n }\n\n async [Symbol.asyncDispose](): Promise<void> {\n await this.destroy()\n }\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import { type DidDocument } from '@atproto/common';
2
+ import { TapChannel, type TapHandler, type TapWebsocketOptions } from './channel.js';
3
+ import { type RepoInfo } from './types.js';
4
+ export interface TapConfig {
5
+ adminPassword?: string;
6
+ }
7
+ export declare class Tap {
8
+ url: string;
9
+ private adminPassword?;
10
+ private authHeader?;
11
+ private addReposUrl;
12
+ private removeReposUrl;
13
+ constructor(url: string, config?: TapConfig);
14
+ private getHeaders;
15
+ channel(handler: TapHandler, opts?: TapWebsocketOptions): TapChannel;
16
+ addRepos(dids: string[]): Promise<void>;
17
+ removeRepos(dids: string[]): Promise<void>;
18
+ resolveDid(did: string): Promise<DidDocument | null>;
19
+ getRepoInfo(did: string): Promise<RepoInfo>;
20
+ }
21
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAe,MAAM,iBAAiB,CAAA;AAC/D,OAAO,EACL,UAAU,EACV,KAAK,UAAU,EACf,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,KAAK,QAAQ,EAAkB,MAAM,YAAY,CAAA;AAG1D,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;IAE3B,YAAY,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,SAAc,EAY9C;IAED,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,mBAAmB,GAAG,UAAU,CAQnE;IAEK,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAW5C;IAEK,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/C;IAEK,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAazD;IAEK,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAYhD;CACF"}
package/dist/client.js ADDED
@@ -0,0 +1,84 @@
1
+ import { didDocument } from '@atproto/common';
2
+ import { TapChannel, } from './channel.js';
3
+ import { repoInfoSchema } from './types.js';
4
+ import { formatAdminAuthHeader } from './util.js';
5
+ export class Tap {
6
+ constructor(url, config = {}) {
7
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
8
+ throw new Error('Invalid URL, expected http:// or https://');
9
+ }
10
+ this.url = url;
11
+ this.adminPassword = config.adminPassword;
12
+ if (this.adminPassword) {
13
+ this.authHeader = formatAdminAuthHeader(this.adminPassword);
14
+ }
15
+ this.addReposUrl = new URL('/repos/add', this.url);
16
+ this.removeReposUrl = new URL('/repos/remove', this.url);
17
+ }
18
+ getHeaders() {
19
+ const headers = {
20
+ 'Content-Type': 'application/json',
21
+ };
22
+ if (this.authHeader) {
23
+ headers['Authorization'] = this.authHeader;
24
+ }
25
+ return headers;
26
+ }
27
+ channel(handler, opts) {
28
+ const url = new URL(this.url);
29
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
30
+ url.pathname = '/channel';
31
+ return new TapChannel(url.toString(), handler, {
32
+ adminPassword: this.adminPassword,
33
+ ...opts,
34
+ });
35
+ }
36
+ async addRepos(dids) {
37
+ const response = await fetch(this.addReposUrl, {
38
+ method: 'POST',
39
+ headers: this.getHeaders(),
40
+ body: JSON.stringify({ dids }),
41
+ });
42
+ await response.body?.cancel(); // expect empty body
43
+ if (!response.ok) {
44
+ throw new Error(`Failed to add repos: ${response.statusText}`);
45
+ }
46
+ }
47
+ async removeRepos(dids) {
48
+ const response = await fetch(this.removeReposUrl, {
49
+ method: 'POST',
50
+ headers: this.getHeaders(),
51
+ body: JSON.stringify({ dids }),
52
+ });
53
+ await response.body?.cancel(); // expect empty body
54
+ if (!response.ok) {
55
+ throw new Error(`Failed to remove repos: ${response.statusText}`);
56
+ }
57
+ }
58
+ async resolveDid(did) {
59
+ const response = await fetch(new URL(`/resolve/${did}`, this.url), {
60
+ method: 'GET',
61
+ headers: this.getHeaders(),
62
+ });
63
+ if (response.status === 404) {
64
+ return null;
65
+ }
66
+ else if (!response.ok) {
67
+ await response.body?.cancel();
68
+ throw new Error(`Failed to resolve DID: ${response.statusText}`);
69
+ }
70
+ return didDocument.parse(await response.json());
71
+ }
72
+ async getRepoInfo(did) {
73
+ const response = await fetch(new URL(`/info/${did}`, this.url), {
74
+ method: 'GET',
75
+ headers: this.getHeaders(),
76
+ });
77
+ if (!response.ok) {
78
+ await response.body?.cancel();
79
+ throw new Error(`Failed to get repo info: ${response.statusText}`);
80
+ }
81
+ return repoInfoSchema.parse(await response.json());
82
+ }
83
+ }
84
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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","sourcesContent":["import { type DidDocument, didDocument } from '@atproto/common'\nimport {\n TapChannel,\n type TapHandler,\n type TapWebsocketOptions,\n} from './channel.js'\nimport { type RepoInfo, repoInfoSchema } from './types.js'\nimport { formatAdminAuthHeader } from './util.js'\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"]}
@@ -0,0 +1,7 @@
1
+ export * from './types.js';
2
+ export * from './client.js';
3
+ export * from './channel.js';
4
+ export * from './simple-indexer.js';
5
+ export * from './lex-indexer.js';
6
+ export * from './util.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,qBAAqB,CAAA;AACnC,cAAc,kBAAkB,CAAA;AAChC,cAAc,WAAW,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export * from './types.js';
2
+ export * from './client.js';
3
+ export * from './channel.js';
4
+ export * from './simple-indexer.js';
5
+ export * from './lex-indexer.js';
6
+ export * from './util.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,qBAAqB,CAAA;AACnC,cAAc,kBAAkB,CAAA;AAChC,cAAc,WAAW,CAAA","sourcesContent":["export * from './types.js'\nexport * from './client.js'\nexport * from './channel.js'\nexport * from './simple-indexer.js'\nexport * from './lex-indexer.js'\nexport * from './util.js'\n"]}
@@ -0,0 +1,46 @@
1
+ import { type Infer, type Main, type RecordSchema } from '@atproto/lex';
2
+ import type { HandlerOpts, TapHandler } from './channel.js';
3
+ import type { IdentityEvent, RecordEvent, TapEvent } from './types.js';
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,90 @@
1
+ import { getMain } from '@atproto/lex';
2
+ export class LexIndexer {
3
+ constructor() {
4
+ this.handlers = new Map();
5
+ }
6
+ handlerKey(collection, action) {
7
+ return `${collection}:${action}`;
8
+ }
9
+ register(action, ns, handler) {
10
+ const schema = getMain(ns);
11
+ const key = this.handlerKey(schema.$type, action);
12
+ if (this.handlers.has(key)) {
13
+ throw new Error(`Handler already registered for ${key}`);
14
+ }
15
+ this.handlers.set(key, { schema, handler });
16
+ return this;
17
+ }
18
+ create(ns, handler) {
19
+ return this.register('create', ns, handler);
20
+ }
21
+ update(ns, handler) {
22
+ return this.register('update', ns, handler);
23
+ }
24
+ delete(ns, handler) {
25
+ return this.register('delete', ns, handler);
26
+ }
27
+ put(ns, handler) {
28
+ this.register('create', ns, handler);
29
+ this.register('update', ns, handler);
30
+ return this;
31
+ }
32
+ other(fn) {
33
+ if (this.otherHandler) {
34
+ throw new Error(`Handler already registered for "other"`);
35
+ }
36
+ this.otherHandler = fn;
37
+ return this;
38
+ }
39
+ identity(fn) {
40
+ if (this.identityHandler) {
41
+ throw new Error(`Handler already registered for "identity"`);
42
+ }
43
+ this.identityHandler = fn;
44
+ return this;
45
+ }
46
+ error(fn) {
47
+ if (this.errorHandler) {
48
+ throw new Error(`Handler already registered for "error"`);
49
+ }
50
+ this.errorHandler = fn;
51
+ return this;
52
+ }
53
+ async onEvent(evt, opts) {
54
+ if (evt.type === 'identity') {
55
+ await this.identityHandler?.(evt, opts);
56
+ }
57
+ else {
58
+ await this.handleRecordEvent(evt, opts);
59
+ }
60
+ await opts.ack();
61
+ }
62
+ async handleRecordEvent(evt, opts) {
63
+ const { collection, action } = evt;
64
+ const key = this.handlerKey(collection, action);
65
+ const registered = this.handlers.get(key);
66
+ if (!registered) {
67
+ await this.otherHandler?.(evt, opts);
68
+ return;
69
+ }
70
+ if (action === 'create' || action === 'update') {
71
+ const match = registered.schema.safeValidate(evt.record);
72
+ if (!match.success) {
73
+ const uriStr = `at://${evt.did}/${evt.collection}/${evt.rkey}`;
74
+ throw new Error(`Record validation failed for ${uriStr}`, {
75
+ cause: match.reason,
76
+ });
77
+ }
78
+ }
79
+ await registered.handler(evt, opts);
80
+ }
81
+ onError(err) {
82
+ if (this.errorHandler) {
83
+ this.errorHandler(err);
84
+ }
85
+ else {
86
+ throw err;
87
+ }
88
+ }
89
+ }
90
+ //# sourceMappingURL=lex-indexer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lex-indexer.js","sourceRoot":"","sources":["../src/lex-indexer.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4C,OAAO,EAAE,MAAM,cAAc,CAAA;AAiEhF,MAAM,OAAO,UAAU;IAAvB;QACU,aAAQ,GAAG,IAAI,GAAG,EAA6B,CAAA;IA0HzD,CAAC;IArHS,UAAU,CAChB,UAAsB,EACtB,MAA6B;QAE7B,OAAO,GAAG,UAAU,IAAI,MAAM,EAAE,CAAA;IAClC,CAAC;IAEO,QAAQ,CACd,MAA6B,EAC7B,EAAW,EACX,OAAgC;QAEhC,MAAM,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,CAAA;QAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;QACjD,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,kCAAkC,GAAG,EAAE,CAAC,CAAA;QAC1D,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAA;QAC3C,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,CACJ,EAAW,EACX,OAAgC;QAEhC,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;IAC7C,CAAC;IAED,MAAM,CACJ,EAAW,EACX,OAAgC;QAEhC,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;IAC7C,CAAC;IAED,MAAM,CACJ,EAAW,EACX,OAAsB;QAEtB,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;IAC7C,CAAC;IAED,GAAG,CACD,EAAW,EACX,OAA6B;QAE7B,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;QACpC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;QACpC,OAAO,IAAI,CAAA;IACb,CAAC;IAED,KAAK,CAAC,EAAkB;QACtB,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAA;QAC3D,CAAC;QACD,IAAI,CAAC,YAAY,GAAG,EAAE,CAAA;QACtB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,QAAQ,CAAC,EAAmB;QAC1B,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAA;QAC9D,CAAC;QACD,IAAI,CAAC,eAAe,GAAG,EAAE,CAAA;QACzB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,KAAK,CAAC,EAAgB;QACpB,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAA;QAC3D,CAAC;QACD,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,UAAU,EAAE,CAAC;YAC5B,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QACzC,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,CAAC,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QACzC,CAAC;QACD,MAAM,IAAI,CAAC,GAAG,EAAE,CAAA;IAClB,CAAC;IAEO,KAAK,CAAC,iBAAiB,CAC7B,GAAgB,EAChB,IAAiB;QAEjB,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,GAAG,CAAA;QAClC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;QAC/C,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAEzC,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;YACpC,OAAM;QACR,CAAC;QAED,IAAI,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC/C,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YACxD,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;gBACnB,MAAM,MAAM,GAAgB,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,IAAI,EAAE,CAAA;gBAC3E,MAAM,IAAI,KAAK,CAAC,gCAAgC,MAAM,EAAE,EAAE;oBACxD,KAAK,EAAE,KAAK,CAAC,MAAM;iBACpB,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;QAED,MAAO,UAAU,CAAC,OAA0B,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;IACzD,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","sourcesContent":["import { type Infer, type Main, type RecordSchema, getMain } from '@atproto/lex'\nimport type { AtUriString, NsidString } from '@atproto/syntax'\nimport type { HandlerOpts, TapHandler } from './channel.js'\nimport type { IdentityEvent, RecordEvent, TapEvent } from './types.js'\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> | UpdateHandler<R> | PutHandler<R> | DeleteHandler\n\ninterface RegisteredHandler {\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(\n collection: NsidString,\n action: RecordEvent['action'],\n ): string {\n return `${collection}:${action}`\n }\n\n private register<const T extends RecordSchema>(\n action: RecordEvent['action'],\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 this.register('update', ns, handler)\n return this\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.safeValidate(evt.record)\n if (!match.success) {\n const uriStr: AtUriString = `at://${evt.did}/${evt.collection}/${evt.rkey}`\n throw new Error(`Record validation failed for ${uriStr}`, {\n cause: match.reason,\n })\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"]}
@@ -0,0 +1,17 @@
1
+ import type { HandlerOpts, TapHandler } from './channel.js';
2
+ import type { IdentityEvent, RecordEvent, TapEvent } from './types.js';
3
+ type IdentityEventHandler = (evt: IdentityEvent, opts?: HandlerOpts) => Promise<void>;
4
+ type RecordEventHandler = (evt: RecordEvent, opts?: HandlerOpts) => Promise<void>;
5
+ type ErrorHandler = (err: Error) => void;
6
+ export declare class SimpleIndexer implements TapHandler {
7
+ private identityHandler;
8
+ private recordHandler;
9
+ private errorHandler;
10
+ identity(fn: IdentityEventHandler): this;
11
+ record(fn: RecordEventHandler): this;
12
+ error(fn: ErrorHandler): this;
13
+ onEvent(evt: TapEvent, opts: HandlerOpts): Promise<void>;
14
+ onError(err: Error): void;
15
+ }
16
+ export {};
17
+ //# sourceMappingURL=simple-indexer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simple-indexer.d.ts","sourceRoot":"","sources":["../src/simple-indexer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAEtE,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,CAGvC;IAED,MAAM,CAAC,EAAE,EAAE,kBAAkB,GAAG,IAAI,CAGnC;IAED,KAAK,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAG5B;IAEK,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAO7D;IAED,OAAO,CAAC,GAAG,EAAE,KAAK,QAMjB;CACF"}
@@ -0,0 +1,32 @@
1
+ export class SimpleIndexer {
2
+ identity(fn) {
3
+ this.identityHandler = fn;
4
+ return this;
5
+ }
6
+ record(fn) {
7
+ this.recordHandler = fn;
8
+ return this;
9
+ }
10
+ error(fn) {
11
+ this.errorHandler = fn;
12
+ return this;
13
+ }
14
+ async onEvent(evt, opts) {
15
+ if (evt.type === 'record') {
16
+ await this.recordHandler?.(evt, opts);
17
+ }
18
+ else {
19
+ await this.identityHandler?.(evt, opts);
20
+ }
21
+ await opts.ack();
22
+ }
23
+ onError(err) {
24
+ if (this.errorHandler) {
25
+ this.errorHandler(err);
26
+ }
27
+ else {
28
+ throw err;
29
+ }
30
+ }
31
+ }
32
+ //# sourceMappingURL=simple-indexer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simple-indexer.js","sourceRoot":"","sources":["../src/simple-indexer.ts"],"names":[],"mappings":"AAeA,MAAM,OAAO,aAAa;IAKxB,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","sourcesContent":["import type { HandlerOpts, TapHandler } from './channel.js'\nimport type { IdentityEvent, RecordEvent, TapEvent } from './types.js'\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"]}