@atproto/tap 0.1.1 → 0.1.3

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,25 @@
1
1
  # @atproto/tap
2
2
 
3
+ ## 0.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#4534](https://github.com/bluesky-social/atproto/pull/4534) [`0193467`](https://github.com/bluesky-social/atproto/commit/0193467463a1a49c0c7f17d129c07b687a1f5057) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make sure the `destroy()` promise resolves, even in case of error
8
+
9
+ - [#4534](https://github.com/bluesky-social/atproto/pull/4534) [`0193467`](https://github.com/bluesky-social/atproto/commit/0193467463a1a49c0c7f17d129c07b687a1f5057) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make `TapChannel` implement `AsyncDisposable`, allowing use with `using` keyword
10
+
11
+ - Updated dependencies []:
12
+ - @atproto/common@0.5.8
13
+ - @atproto/lex@0.0.11
14
+
15
+ ## 0.1.2
16
+
17
+ ### Patch Changes
18
+
19
+ - Updated dependencies []:
20
+ - @atproto/common@0.5.7
21
+ - @atproto/lex@0.0.10
22
+
3
23
  ## 0.1.1
4
24
 
5
25
  ### Patch Changes
package/dist/channel.d.ts CHANGED
@@ -14,7 +14,7 @@ export type TapWebsocketOptions = ClientOptions & {
14
14
  heartbeatIntervalMs?: number;
15
15
  onReconnectError?: (error: unknown, n: number, initialSetup: boolean) => void;
16
16
  };
17
- export declare class TapChannel {
17
+ export declare class TapChannel implements AsyncDisposable {
18
18
  private ws;
19
19
  private handler;
20
20
  private readonly abortController;
@@ -28,5 +28,6 @@ export declare class TapChannel {
28
28
  start(): Promise<void>;
29
29
  private processWsEvent;
30
30
  destroy(): Promise<void>;
31
+ [Symbol.asyncDispose](): Promise<void>;
31
32
  }
32
33
  //# sourceMappingURL=channel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,IAAI,CAAA;AAGlC,OAAO,EAAE,QAAQ,EAAiB,MAAM,SAAS,CAAA;AAGjD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,WAAW,CAAA;IACnB,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnE,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;CAC9B;AAED,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG;IAChD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,KAAK,IAAI,CAAA;CAC9E,CAAA;AAOD,qBAAa,UAAU;IACrB,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,OAAO,CAAY;IAE3B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyC;IACzE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiC;IAE9D,OAAO,CAAC,YAAY,CAAoB;gBAGtC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,UAAU,EACnB,MAAM,GAAE,mBAAwB;IAoB5B,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAY3B,OAAO;YAKP,gBAAgB;YAShB,iBAAiB;IAqBzB,KAAK;YAcG,cAAc;IA0BtB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAI/B"}
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,IAAI,CAAA;AAGlC,OAAO,EAAE,QAAQ,EAAiB,MAAM,SAAS,CAAA;AAGjD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,WAAW,CAAA;IACnB,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnE,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;CAC9B;AAED,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG;IAChD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,KAAK,IAAI,CAAA;CAC9E,CAAA;AAOD,qBAAa,UAAW,YAAW,eAAe;IAChD,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,OAAO,CAAY;IAE3B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyC;IACzE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiC;IAE9D,OAAO,CAAC,YAAY,CAAoB;gBAGtC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,UAAU,EACnB,MAAM,GAAE,mBAAwB;IAoB5B,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAY3B,OAAO;YAKP,gBAAgB;YAShB,iBAAiB;IAqBzB,KAAK;YAeG,cAAc;IA0BtB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAKxB,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7C"}
package/dist/channel.js CHANGED
@@ -99,19 +99,20 @@ class TapChannel {
99
99
  }
100
100
  }
101
101
  async start() {
102
+ this.abortController.signal.throwIfAborted();
102
103
  try {
103
104
  for await (const chunk of this.ws) {
104
105
  await this.processWsEvent(chunk);
105
106
  }
106
107
  }
107
108
  catch (err) {
108
- if ((0, common_1.isErrnoException)(err) && err.name === 'AbortError') {
109
- this.destroyDefer.resolve();
110
- }
111
- else {
109
+ if (!(0, util_1.isCausedBySignal)(err, this.abortController.signal)) {
112
110
  throw err;
113
111
  }
114
112
  }
113
+ finally {
114
+ this.destroyDefer.resolve();
115
+ }
115
116
  }
116
117
  async processWsEvent(chunk) {
117
118
  let evt;
@@ -141,6 +142,9 @@ class TapChannel {
141
142
  this.abortController.abort();
142
143
  await this.destroyDefer.complete;
143
144
  }
145
+ async [Symbol.asyncDispose]() {
146
+ await this.destroy();
147
+ }
144
148
  }
145
149
  exports.TapChannel = TapChannel;
146
150
  //# sourceMappingURL=channel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":";;;AACA,4CAAgF;AAChF,kDAAuD;AACvD,mCAAiD;AACjD,iCAA8C;AAwB9C,MAAa,UAAU;IASrB,YACE,GAAW,EACX,OAAmB,EACnB,SAA8B,EAAE;QAX1B;;;;;WAAsB;QACtB;;;;;WAAmB;QAEV;;;;mBAAmC,IAAI,eAAe,EAAE;WAAA;QACxD;;;;mBAA2B,IAAA,yBAAgB,GAAE;WAAA;QAEtD;;;;mBAA8B,EAAE;WAAA;QAOtC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,MAAM,EAAE,aAAa,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAA;QACzC,IAAI,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;QAC1B,IAAI,aAAa,EAAE,CAAC;YAClB,OAAO,KAAP,OAAO,GAAK,EAAE,EAAA;YACd,OAAO,CAAC,eAAe,CAAC,GAAG,IAAA,4BAAqB,EAAC,aAAa,CAAC,CAAA;QACjE,CAAC;QACD,IAAI,CAAC,EAAE,GAAG,IAAI,8BAAkB,CAAC;YAC/B,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,GAAG;YACvB,WAAW,EAAE,GAAG,EAAE;gBAChB,IAAI,CAAC,iBAAiB,EAAE,CAAA;YAC1B,CAAC;YACD,MAAM,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM;YACnC,GAAG,IAAI;YACP,OAAO;SACR,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,EAAU;QACvB,IAAI,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;YAC1B,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;YACxB,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAA;YACjC,CAAC;QACH,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAA;QACjC,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,EAAU;QAC9B,MAAM,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;IACzD,CAAC;IAED,gDAAgD;IACxC,KAAK,CAAC,gBAAgB,CAAC,EAAU;QACvC,MAAM,KAAK,GAAG,IAAA,yBAAgB,GAAE,CAAA;QAChC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC;YACrB,EAAE;YACF,KAAK;SACN,CAAC,CAAA;QACF,MAAM,KAAK,CAAC,QAAQ,CAAA;IACtB,CAAC;IAEO,KAAK,CAAC,iBAAiB;QAC7B,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpC,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;gBACnC,IAAI,CAAC,GAAG,EAAE,CAAC;oBACT,OAAM;gBACR,CAAC;gBACD,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;gBAC1B,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,CAAA;gBACnB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YAChD,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,CAAC,OAAO,CAAC,OAAO,CAClB,IAAI,KAAK,CAAC,gCAAgC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE;oBAChE,KAAK,EAAE,GAAG;iBACX,CAAC,CACH,CAAA;gBACD,OAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC;YACH,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;gBAClC,MAAM,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAA;YAClC,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,IAAA,yBAAgB,EAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBACvD,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAA;YAC7B,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,CAAA;YACX,CAAC;QACH,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,cAAc,CAAC,KAAiB;QAC5C,IAAI,GAAa,CAAA;QACjB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAA;YAC7B,GAAG,GAAG,IAAA,qBAAa,EAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;QACvC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,yBAAyB,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,CAAA;YAC1E,OAAM;QACR,CAAC;QAED,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE;gBAC9B,MAAM,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM;gBACnC,GAAG,EAAE,KAAK,IAAI,EAAE;oBACd,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;gBAC7B,CAAC;aACF,CAAC,CAAA;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,qCAAqC;YACrC,IAAI,CAAC,OAAO,CAAC,OAAO,CAClB,IAAI,KAAK,CAAC,2BAA2B,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAC/D,CAAA;YACD,OAAM;QACR,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAA;QAC5B,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAA;IAClC,CAAC;CACF;AA3HD,gCA2HC","sourcesContent":["import { ClientOptions } from 'ws'\nimport { Deferrable, createDeferrable, isErrnoException } from '@atproto/common'\nimport { WebSocketKeepAlive } from '@atproto/ws-client'\nimport { TapEvent, parseTapEvent } from './types'\nimport { formatAdminAuthHeader } from './util'\n\nexport interface HandlerOpts {\n signal: AbortSignal\n ack: () => Promise<void>\n}\n\nexport interface TapHandler {\n onEvent: (evt: TapEvent, opts: HandlerOpts) => void | Promise<void>\n onError: (err: Error) => void\n}\n\nexport type TapWebsocketOptions = ClientOptions & {\n adminPassword?: string\n maxReconnectSeconds?: number\n heartbeatIntervalMs?: number\n onReconnectError?: (error: unknown, n: number, initialSetup: boolean) => void\n}\n\ntype BufferedAck = {\n id: number\n defer: Deferrable\n}\n\nexport class TapChannel {\n private ws: WebSocketKeepAlive\n private handler: TapHandler\n\n private readonly abortController: AbortController = new AbortController()\n private readonly destroyDefer: Deferrable = createDeferrable()\n\n private bufferedAcks: BufferedAck[] = []\n\n constructor(\n url: string,\n handler: TapHandler,\n wsOpts: TapWebsocketOptions = {},\n ) {\n this.handler = handler\n const { adminPassword, ...rest } = wsOpts\n let headers = rest.headers\n if (adminPassword) {\n headers ??= {}\n headers['Authorization'] = formatAdminAuthHeader(adminPassword)\n }\n this.ws = new WebSocketKeepAlive({\n getUrl: async () => url,\n onReconnect: () => {\n this.flushBufferedAcks()\n },\n signal: this.abortController.signal,\n ...rest,\n headers,\n })\n }\n\n async ackEvent(id: number): Promise<void> {\n if (this.ws.isConnected()) {\n try {\n await this.sendAck(id)\n } catch {\n await this.bufferAndSendAck(id)\n }\n } else {\n await this.bufferAndSendAck(id)\n }\n }\n\n private async sendAck(id: number): Promise<void> {\n await this.ws.send(JSON.stringify({ type: 'ack', id }))\n }\n\n // resolves after the ack has been actually sent\n private async bufferAndSendAck(id: number): Promise<void> {\n const defer = createDeferrable()\n this.bufferedAcks.push({\n id,\n defer,\n })\n await defer.complete\n }\n\n private async flushBufferedAcks(): Promise<void> {\n while (this.bufferedAcks.length > 0) {\n try {\n const ack = this.bufferedAcks.at(0)\n if (!ack) {\n return\n }\n await this.sendAck(ack.id)\n ack.defer.resolve()\n this.bufferedAcks = this.bufferedAcks.slice(1)\n } catch (err) {\n this.handler.onError(\n new Error(`failed to send ack for event ${this.bufferedAcks[0]}`, {\n cause: err,\n }),\n )\n return\n }\n }\n }\n\n async start() {\n try {\n for await (const chunk of this.ws) {\n await this.processWsEvent(chunk)\n }\n } catch (err) {\n if (isErrnoException(err) && err.name === 'AbortError') {\n this.destroyDefer.resolve()\n } else {\n throw err\n }\n }\n }\n\n private async processWsEvent(chunk: Uint8Array) {\n let evt: TapEvent\n try {\n const data = chunk.toString()\n evt = parseTapEvent(JSON.parse(data))\n } catch (err) {\n this.handler.onError(new Error('Failed to parse message', { cause: err }))\n return\n }\n\n try {\n await this.handler.onEvent(evt, {\n signal: this.abortController.signal,\n ack: async () => {\n await this.ackEvent(evt.id)\n },\n })\n } catch (err) {\n // Don't ack on error - let Tap retry\n this.handler.onError(\n new Error(`Failed to process event ${evt.id}`, { cause: err }),\n )\n return\n }\n }\n\n async destroy(): Promise<void> {\n this.abortController.abort()\n await this.destroyDefer.complete\n }\n}\n"]}
1
+ {"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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","sourcesContent":["import { ClientOptions } from 'ws'\nimport { Deferrable, createDeferrable } from '@atproto/common'\nimport { WebSocketKeepAlive } from '@atproto/ws-client'\nimport { TapEvent, parseTapEvent } from './types'\nimport { formatAdminAuthHeader, isCausedBySignal } from './util'\n\nexport interface HandlerOpts {\n signal: AbortSignal\n ack: () => Promise<void>\n}\n\nexport interface TapHandler {\n onEvent: (evt: TapEvent, opts: HandlerOpts) => void | Promise<void>\n onError: (err: Error) => void\n}\n\nexport type TapWebsocketOptions = ClientOptions & {\n adminPassword?: string\n maxReconnectSeconds?: number\n heartbeatIntervalMs?: number\n onReconnectError?: (error: unknown, n: number, initialSetup: boolean) => void\n}\n\ntype BufferedAck = {\n id: number\n defer: Deferrable\n}\n\nexport class TapChannel implements AsyncDisposable {\n private ws: WebSocketKeepAlive\n private handler: TapHandler\n\n private readonly abortController: AbortController = new AbortController()\n private readonly destroyDefer: Deferrable = createDeferrable()\n\n private bufferedAcks: BufferedAck[] = []\n\n constructor(\n url: string,\n handler: TapHandler,\n wsOpts: TapWebsocketOptions = {},\n ) {\n this.handler = handler\n const { adminPassword, ...rest } = wsOpts\n let headers = rest.headers\n if (adminPassword) {\n headers ??= {}\n headers['Authorization'] = formatAdminAuthHeader(adminPassword)\n }\n this.ws = new WebSocketKeepAlive({\n getUrl: async () => url,\n onReconnect: () => {\n this.flushBufferedAcks()\n },\n signal: this.abortController.signal,\n ...rest,\n headers,\n })\n }\n\n async ackEvent(id: number): Promise<void> {\n if (this.ws.isConnected()) {\n try {\n await this.sendAck(id)\n } catch {\n await this.bufferAndSendAck(id)\n }\n } else {\n await this.bufferAndSendAck(id)\n }\n }\n\n private async sendAck(id: number): Promise<void> {\n await this.ws.send(JSON.stringify({ type: 'ack', id }))\n }\n\n // resolves after the ack has been actually sent\n private async bufferAndSendAck(id: number): Promise<void> {\n const defer = createDeferrable()\n this.bufferedAcks.push({\n id,\n defer,\n })\n await defer.complete\n }\n\n private async flushBufferedAcks(): Promise<void> {\n while (this.bufferedAcks.length > 0) {\n try {\n const ack = this.bufferedAcks.at(0)\n if (!ack) {\n return\n }\n await this.sendAck(ack.id)\n ack.defer.resolve()\n this.bufferedAcks = this.bufferedAcks.slice(1)\n } catch (err) {\n this.handler.onError(\n new Error(`failed to send ack for event ${this.bufferedAcks[0]}`, {\n cause: err,\n }),\n )\n return\n }\n }\n }\n\n async start() {\n this.abortController.signal.throwIfAborted()\n try {\n for await (const chunk of this.ws) {\n await this.processWsEvent(chunk)\n }\n } catch (err) {\n if (!isCausedBySignal(err, this.abortController.signal)) {\n throw err\n }\n } finally {\n this.destroyDefer.resolve()\n }\n }\n\n private async processWsEvent(chunk: Uint8Array) {\n let evt: TapEvent\n try {\n const data = chunk.toString()\n evt = parseTapEvent(JSON.parse(data))\n } catch (err) {\n this.handler.onError(new Error('Failed to parse message', { cause: err }))\n return\n }\n\n try {\n await this.handler.onEvent(evt, {\n signal: this.abortController.signal,\n ack: async () => {\n await this.ackEvent(evt.id)\n },\n })\n } catch (err) {\n // Don't ack on error - let Tap retry\n this.handler.onError(\n new Error(`Failed to process event ${evt.id}`, { cause: err }),\n )\n return\n }\n }\n\n async destroy(): Promise<void> {\n this.abortController.abort()\n await this.destroyDefer.complete\n }\n\n async [Symbol.asyncDispose](): Promise<void> {\n await this.destroy()\n }\n}\n"]}
package/dist/util.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export declare const formatAdminAuthHeader: (password: string) => string;
2
2
  export declare const parseAdminAuthHeader: (header: string) => string;
3
3
  export declare const assureAdminAuth: (expectedPassword: string, header: string) => void;
4
+ export declare function isCausedBySignal(err: unknown, signal: AbortSignal): boolean;
4
5
  //# sourceMappingURL=util.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,GAAI,UAAU,MAAM,WAErD,CAAA;AAED,eAAO,MAAM,oBAAoB,GAAI,QAAQ,MAAM,WASlD,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,kBAAkB,MAAM,EAAE,QAAQ,MAAM,SAMvE,CAAA"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,GAAI,UAAU,MAAM,WAErD,CAAA;AAED,eAAO,MAAM,oBAAoB,GAAI,QAAQ,MAAM,WASlD,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,kBAAkB,MAAM,EAAE,QAAQ,MAAM,SAMvE,CAAA;AAaD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,WAOjE"}
package/dist/util.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.assureAdminAuth = exports.parseAdminAuthHeader = exports.formatAdminAuthHeader = void 0;
4
+ exports.isCausedBySignal = isCausedBySignal;
4
5
  const formatAdminAuthHeader = (password) => {
5
6
  return 'Basic ' + Buffer.from(`admin:${password}`).toString('base64');
6
7
  };
@@ -34,4 +35,12 @@ const timingSafeEqual = (a, b) => {
34
35
  }
35
36
  return bufA.compare(bufB) === 0;
36
37
  };
38
+ function isCausedBySignal(err, signal) {
39
+ if (!signal.aborted)
40
+ return false;
41
+ if (signal.reason == null)
42
+ return false; // Ignore nullish reasons
43
+ return (err === signal.reason ||
44
+ (err instanceof Error && err.cause === signal.reason));
45
+ }
37
46
  //# sourceMappingURL=util.js.map
package/dist/util.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;;AAAO,MAAM,qBAAqB,GAAG,CAAC,QAAgB,EAAE,EAAE;IACxD,OAAO,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;AACvE,CAAC,CAAA;AAFY,QAAA,qBAAqB,yBAEjC;AAEM,MAAM,oBAAoB,GAAG,CAAC,MAAc,EAAE,EAAE;IACrD,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;IACvE,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC;SACzD,QAAQ,EAAE;SACV,KAAK,CAAC,GAAG,CAAC,CAAA;IACb,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAA;IAC3E,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AATY,QAAA,oBAAoB,wBAShC;AAEM,MAAM,eAAe,GAAG,CAAC,gBAAwB,EAAE,MAAc,EAAE,EAAE;IAC1E,MAAM,cAAc,GAAG,IAAA,4BAAoB,EAAC,MAAM,CAAC,CAAA;IACnD,MAAM,SAAS,GAAG,eAAe,CAAC,cAAc,EAAE,gBAAgB,CAAC,CAAA;IACnE,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAA;IAC3C,CAAC;AACH,CAAC,CAAA;AANY,QAAA,eAAe,mBAM3B;AAED,MAAM,eAAe,GAAG,CAAC,CAAS,EAAE,CAAS,EAAW,EAAE;IACxD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3B,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;QAChC,2EAA2E;QAC3E,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;QACtC,OAAO,KAAK,CAAA;IACd,CAAC;IACD,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AACjC,CAAC,CAAA","sourcesContent":["export const formatAdminAuthHeader = (password: string) => {\n return 'Basic ' + Buffer.from(`admin:${password}`).toString('base64')\n}\n\nexport const parseAdminAuthHeader = (header: string) => {\n const noPrefix = header.startsWith('Basic ') ? header.slice(6) : header\n const [username, password] = Buffer.from(noPrefix, 'base64')\n .toString()\n .split(':')\n if (username !== 'admin') {\n throw new Error(\"Unexpected username in admin headers. Expected 'admin'\")\n }\n return password\n}\n\nexport const assureAdminAuth = (expectedPassword: string, header: string) => {\n const headerPassword = parseAdminAuthHeader(header)\n const passEqual = timingSafeEqual(headerPassword, expectedPassword)\n if (!passEqual) {\n throw new Error('Invalid admin password')\n }\n}\n\nconst timingSafeEqual = (a: string, b: string): boolean => {\n const bufA = Buffer.from(a)\n const bufB = Buffer.from(b)\n if (bufA.length !== bufB.length) {\n // Compare against self to maintain constant time even with length mismatch\n Buffer.from(a).compare(Buffer.from(a))\n return false\n }\n return bufA.compare(bufB) === 0\n}\n"]}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;;AAkCA,4CAOC;AAzCM,MAAM,qBAAqB,GAAG,CAAC,QAAgB,EAAE,EAAE;IACxD,OAAO,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;AACvE,CAAC,CAAA;AAFY,QAAA,qBAAqB,yBAEjC;AAEM,MAAM,oBAAoB,GAAG,CAAC,MAAc,EAAE,EAAE;IACrD,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;IACvE,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC;SACzD,QAAQ,EAAE;SACV,KAAK,CAAC,GAAG,CAAC,CAAA;IACb,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAA;IAC3E,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AATY,QAAA,oBAAoB,wBAShC;AAEM,MAAM,eAAe,GAAG,CAAC,gBAAwB,EAAE,MAAc,EAAE,EAAE;IAC1E,MAAM,cAAc,GAAG,IAAA,4BAAoB,EAAC,MAAM,CAAC,CAAA;IACnD,MAAM,SAAS,GAAG,eAAe,CAAC,cAAc,EAAE,gBAAgB,CAAC,CAAA;IACnE,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAA;IAC3C,CAAC;AACH,CAAC,CAAA;AANY,QAAA,eAAe,mBAM3B;AAED,MAAM,eAAe,GAAG,CAAC,CAAS,EAAE,CAAS,EAAW,EAAE;IACxD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC3B,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;QAChC,2EAA2E;QAC3E,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;QACtC,OAAO,KAAK,CAAA;IACd,CAAC;IACD,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AACjC,CAAC,CAAA;AAED,SAAgB,gBAAgB,CAAC,GAAY,EAAE,MAAmB;IAChE,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAO,KAAK,CAAA;IACjC,IAAI,MAAM,CAAC,MAAM,IAAI,IAAI;QAAE,OAAO,KAAK,CAAA,CAAC,yBAAyB;IACjE,OAAO,CACL,GAAG,KAAK,MAAM,CAAC,MAAM;QACrB,CAAC,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,CACtD,CAAA;AACH,CAAC","sourcesContent":["export const formatAdminAuthHeader = (password: string) => {\n return 'Basic ' + Buffer.from(`admin:${password}`).toString('base64')\n}\n\nexport const parseAdminAuthHeader = (header: string) => {\n const noPrefix = header.startsWith('Basic ') ? header.slice(6) : header\n const [username, password] = Buffer.from(noPrefix, 'base64')\n .toString()\n .split(':')\n if (username !== 'admin') {\n throw new Error(\"Unexpected username in admin headers. Expected 'admin'\")\n }\n return password\n}\n\nexport const assureAdminAuth = (expectedPassword: string, header: string) => {\n const headerPassword = parseAdminAuthHeader(header)\n const passEqual = timingSafeEqual(headerPassword, expectedPassword)\n if (!passEqual) {\n throw new Error('Invalid admin password')\n }\n}\n\nconst timingSafeEqual = (a: string, b: string): boolean => {\n const bufA = Buffer.from(a)\n const bufB = Buffer.from(b)\n if (bufA.length !== bufB.length) {\n // Compare against self to maintain constant time even with length mismatch\n Buffer.from(a).compare(Buffer.from(a))\n return false\n }\n return bufA.compare(bufB) === 0\n}\n\nexport function isCausedBySignal(err: unknown, signal: AbortSignal) {\n if (!signal.aborted) return false\n if (signal.reason == null) return false // Ignore nullish reasons\n return (\n err === signal.reason ||\n (err instanceof Error && err.cause === signal.reason)\n )\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/tap",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "license": "MIT",
5
5
  "description": "atproto tap client",
6
6
  "keywords": [
@@ -24,8 +24,8 @@
24
24
  "dependencies": {
25
25
  "ws": "^8.12.0",
26
26
  "zod": "^3.23.8",
27
- "@atproto/common": "^0.5.6",
28
- "@atproto/lex": "^0.0.9",
27
+ "@atproto/common": "^0.5.8",
28
+ "@atproto/lex": "^0.0.11",
29
29
  "@atproto/syntax": "^0.4.2",
30
30
  "@atproto/ws-client": "^0.0.4"
31
31
  },
package/src/channel.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { ClientOptions } from 'ws'
2
- import { Deferrable, createDeferrable, isErrnoException } from '@atproto/common'
2
+ import { Deferrable, createDeferrable } from '@atproto/common'
3
3
  import { WebSocketKeepAlive } from '@atproto/ws-client'
4
4
  import { TapEvent, parseTapEvent } from './types'
5
- import { formatAdminAuthHeader } from './util'
5
+ import { formatAdminAuthHeader, isCausedBySignal } from './util'
6
6
 
7
7
  export interface HandlerOpts {
8
8
  signal: AbortSignal
@@ -26,7 +26,7 @@ type BufferedAck = {
26
26
  defer: Deferrable
27
27
  }
28
28
 
29
- export class TapChannel {
29
+ export class TapChannel implements AsyncDisposable {
30
30
  private ws: WebSocketKeepAlive
31
31
  private handler: TapHandler
32
32
 
@@ -106,16 +106,17 @@ export class TapChannel {
106
106
  }
107
107
 
108
108
  async start() {
109
+ this.abortController.signal.throwIfAborted()
109
110
  try {
110
111
  for await (const chunk of this.ws) {
111
112
  await this.processWsEvent(chunk)
112
113
  }
113
114
  } catch (err) {
114
- if (isErrnoException(err) && err.name === 'AbortError') {
115
- this.destroyDefer.resolve()
116
- } else {
115
+ if (!isCausedBySignal(err, this.abortController.signal)) {
117
116
  throw err
118
117
  }
118
+ } finally {
119
+ this.destroyDefer.resolve()
119
120
  }
120
121
  }
121
122
 
@@ -149,4 +150,8 @@ export class TapChannel {
149
150
  this.abortController.abort()
150
151
  await this.destroyDefer.complete
151
152
  }
153
+
154
+ async [Symbol.asyncDispose](): Promise<void> {
155
+ await this.destroy()
156
+ }
152
157
  }
package/src/util.ts CHANGED
@@ -31,3 +31,12 @@ const timingSafeEqual = (a: string, b: string): boolean => {
31
31
  }
32
32
  return bufA.compare(bufB) === 0
33
33
  }
34
+
35
+ export function isCausedBySignal(err: unknown, signal: AbortSignal) {
36
+ if (!signal.aborted) return false
37
+ if (signal.reason == null) return false // Ignore nullish reasons
38
+ return (
39
+ err === signal.reason ||
40
+ (err instanceof Error && err.cause === signal.reason)
41
+ )
42
+ }