@atproto/xrpc-server 0.9.5 → 0.10.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +7 -8
  4. package/dist/auth.js.map +1 -1
  5. package/dist/errors.js.map +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/logger.js.map +1 -1
  8. package/dist/rate-limiter.js.map +1 -1
  9. package/dist/server.d.ts.map +1 -1
  10. package/dist/server.js +1 -2
  11. package/dist/server.js.map +1 -1
  12. package/dist/stream/frames.d.ts +7 -6
  13. package/dist/stream/frames.d.ts.map +1 -1
  14. package/dist/stream/frames.js +7 -54
  15. package/dist/stream/frames.js.map +1 -1
  16. package/dist/stream/index.d.ts +0 -1
  17. package/dist/stream/index.d.ts.map +1 -1
  18. package/dist/stream/index.js +0 -1
  19. package/dist/stream/index.js.map +1 -1
  20. package/dist/stream/logger.js.map +1 -1
  21. package/dist/stream/server.d.ts.map +1 -1
  22. package/dist/stream/server.js +4 -4
  23. package/dist/stream/server.js.map +1 -1
  24. package/dist/stream/stream.d.ts +3 -3
  25. package/dist/stream/stream.d.ts.map +1 -1
  26. package/dist/stream/stream.js.map +1 -1
  27. package/dist/stream/subscription.d.ts.map +1 -1
  28. package/dist/stream/subscription.js +13 -8
  29. package/dist/stream/subscription.js.map +1 -1
  30. package/dist/stream/types.d.ts +0 -10
  31. package/dist/stream/types.d.ts.map +1 -1
  32. package/dist/stream/types.js +1 -26
  33. package/dist/stream/types.js.map +1 -1
  34. package/dist/types.d.ts +2 -2
  35. package/dist/types.js.map +1 -1
  36. package/dist/util.js.map +1 -1
  37. package/package.json +7 -6
  38. package/src/auth.ts +7 -8
  39. package/src/server.ts +3 -3
  40. package/src/stream/frames.ts +19 -20
  41. package/src/stream/index.ts +0 -1
  42. package/src/stream/server.ts +1 -1
  43. package/src/stream/stream.ts +1 -1
  44. package/src/stream/subscription.ts +14 -7
  45. package/src/stream/types.ts +0 -16
  46. package/tests/auth.test.ts +5 -2
  47. package/tests/bodies.test.ts +2 -0
  48. package/tests/frames.test.ts +16 -18
  49. package/tests/subscriptions.test.ts +1 -57
  50. package/tsconfig.build.tsbuildinfo +1 -1
  51. package/dist/stream/websocket-keepalive.d.ts +0 -24
  52. package/dist/stream/websocket-keepalive.d.ts.map +0 -1
  53. package/dist/stream/websocket-keepalive.js +0 -160
  54. package/dist/stream/websocket-keepalive.js.map +0 -1
  55. package/src/stream/websocket-keepalive.ts +0 -152
@@ -25,19 +25,3 @@ export type ErrorFrameBody<T extends string = string> = { error: T } & z.infer<
25
25
 
26
26
  export const frameHeader = z.union([messageFrameHeader, errorFrameHeader])
27
27
  export type FrameHeader = z.infer<typeof frameHeader>
28
-
29
- export class DisconnectError extends Error {
30
- constructor(
31
- public wsCode: CloseCode = CloseCode.Policy,
32
- public xrpcCode?: string,
33
- ) {
34
- super()
35
- }
36
- }
37
-
38
- // https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1
39
- export enum CloseCode {
40
- Normal = 1000,
41
- Abnormal = 1006,
42
- Policy = 1008,
43
- }
@@ -3,7 +3,6 @@ import * as http from 'node:http'
3
3
  import { AddressInfo } from 'node:net'
4
4
  import * as jose from 'jose'
5
5
  import KeyEncoder from 'key-encoder'
6
- import * as ui8 from 'uint8arrays'
7
6
  import { MINUTE } from '@atproto/common'
8
7
  import { Secp256k1Keypair } from '@atproto/crypto'
9
8
  import { LexiconDoc } from '@atproto/lexicon'
@@ -322,6 +321,10 @@ const createPrivateKeyObject = async (
322
321
  ): Promise<KeyObject> => {
323
322
  const raw = await privateKey.export()
324
323
  const encoder = new KeyEncoder('secp256k1')
325
- const key = encoder.encodePrivate(ui8.toString(raw, 'hex'), 'raw', 'pem')
324
+ const key = encoder.encodePrivate(
325
+ Buffer.from(raw).toString('hex'),
326
+ 'raw',
327
+ 'pem',
328
+ )
326
329
  return createPrivateKey({ format: 'pem', key })
327
330
  }
@@ -1,3 +1,5 @@
1
+ /* eslint-disable import/no-deprecated */
2
+
1
3
  import assert from 'node:assert'
2
4
  import * as http from 'node:http'
3
5
  import { AddressInfo } from 'node:net'
@@ -1,5 +1,5 @@
1
- import * as cborx from 'cbor-x'
2
- import * as uint8arrays from 'uint8arrays'
1
+ import { encode } from '@atproto/lex-cbor'
2
+ import { ui8Equals } from '@atproto/lex-data'
3
3
  import { ErrorFrame, Frame, FrameType, MessageFrame } from '../src'
4
4
 
5
5
  describe('Frames', () => {
@@ -19,7 +19,7 @@ describe('Frames', () => {
19
19
 
20
20
  const bytes = messageFrame.toBytes()
21
21
  expect(
22
- uint8arrays.equals(
22
+ ui8Equals(
23
23
  bytes,
24
24
  new Uint8Array([
25
25
  /*header*/ 162, 97, 116, 98, 35, 100, 98, 111, 112, 1, /*body*/ 162,
@@ -56,7 +56,7 @@ describe('Frames', () => {
56
56
 
57
57
  const bytes = errorFrame.toBytes()
58
58
  expect(
59
- uint8arrays.equals(
59
+ ui8Equals(
60
60
  bytes,
61
61
  new Uint8Array([
62
62
  /*header*/ 161, 98, 111, 112, 32, /*body*/ 162, 101, 101, 114, 114,
@@ -81,17 +81,15 @@ describe('Frames', () => {
81
81
 
82
82
  it('parsing fails when frame is not CBOR.', async () => {
83
83
  const bytes = Buffer.from('some utf8 bytes')
84
- const emptyBytes = Buffer.from('')
85
- expect(() => Frame.fromBytes(bytes)).toThrow('Unexpected end of CBOR data')
86
- expect(() => Frame.fromBytes(emptyBytes)).toThrow(
87
- 'Unexpected end of CBOR data',
88
- )
84
+ const emptyBytes = Buffer.alloc(0)
85
+ expect(() => Frame.fromBytes(bytes)).toThrow()
86
+ expect(() => Frame.fromBytes(emptyBytes)).toThrow()
89
87
  })
90
88
 
91
89
  it('parsing fails when frame header is malformed.', async () => {
92
- const bytes = uint8arrays.concat([
93
- cborx.encode({ op: -2 }), // Unknown op
94
- cborx.encode({ a: 'b', c: [1, 2, 3] }),
90
+ const bytes = Buffer.concat([
91
+ encode({ op: -2 }), // Unknown op
92
+ encode({ a: 'b', c: [1, 2, 3] }),
95
93
  ])
96
94
 
97
95
  expect(() => Frame.fromBytes(bytes)).toThrow('Invalid frame header:')
@@ -103,7 +101,7 @@ describe('Frames', () => {
103
101
  { type: '#d' },
104
102
  )
105
103
 
106
- const headerBytes = cborx.encode(messageFrame.header)
104
+ const headerBytes = encode(messageFrame.header)
107
105
 
108
106
  expect(() => Frame.fromBytes(headerBytes)).toThrow('Missing frame body')
109
107
  })
@@ -114,9 +112,9 @@ describe('Frames', () => {
114
112
  { type: '#d' },
115
113
  )
116
114
 
117
- const bytes = uint8arrays.concat([
115
+ const bytes = Buffer.concat([
118
116
  messageFrame.toBytes(),
119
- cborx.encode({ d: 'e', f: [4, 5, 6] }),
117
+ encode({ d: 'e', f: [4, 5, 6] }),
120
118
  ])
121
119
 
122
120
  expect(() => Frame.fromBytes(bytes)).toThrow(
@@ -127,9 +125,9 @@ describe('Frames', () => {
127
125
  it('parsing fails when error frame has invalid body.', async () => {
128
126
  const errorFrame = new ErrorFrame({ error: 'BadOops' })
129
127
 
130
- const bytes = uint8arrays.concat([
131
- cborx.encode(errorFrame.header),
132
- cborx.encode({ blah: 1 }),
128
+ const bytes = Buffer.concat([
129
+ encode(errorFrame.header),
130
+ encode({ blah: 1 }),
133
131
  ])
134
132
 
135
133
  expect(() => Frame.fromBytes(bytes)).toThrow('Invalid error frame body:')
@@ -1,7 +1,6 @@
1
1
  import * as http from 'node:http'
2
2
  import { AddressInfo } from 'node:net'
3
- import getPort from 'get-port'
4
- import { WebSocket, WebSocketServer, createWebSocketStream } from 'ws'
3
+ import { WebSocket, createWebSocketStream } from 'ws'
5
4
  import { wait } from '@atproto/common'
6
5
  import { LexiconDoc } from '@atproto/lexicon'
7
6
  import { ErrorFrame, Frame, MessageFrame, Subscription, byFrame } from '../src'
@@ -347,59 +346,4 @@ describe('Subscriptions', () => {
347
346
  ])
348
347
  })
349
348
  })
350
-
351
- it('uses a heartbeat to reconnect if a connection is dropped', async () => {
352
- // we run a server that, on first connection, pauses for longer than the heartbeat interval (doesn't return "pong"s)
353
- // on second connection, it returns a message frame and then closes
354
- const port = await getPort()
355
- const server = new WebSocketServer({ port })
356
- let firstConnection = true
357
- let firstWasClosed = false
358
- server.on('connection', async (socket) => {
359
- if (firstConnection === true) {
360
- firstConnection = false
361
- socket.pause()
362
- await wait(600)
363
- // shouldn't send this message because the socket would be closed
364
- const frame = new ErrorFrame({
365
- error: 'AuthenticationRequired',
366
- message: 'Authentication Required',
367
- })
368
- socket.send(frame.toBytes(), { binary: true }, (err) => {
369
- if (err) throw err
370
- socket.close(xrpcServer.CloseCode.Normal)
371
- })
372
- socket.on('close', () => {
373
- firstWasClosed = true
374
- })
375
- } else {
376
- const frame = new MessageFrame({ count: 1 })
377
- socket.send(frame.toBytes(), { binary: true }, (err) => {
378
- if (err) throw err
379
- socket.close(xrpcServer.CloseCode.Normal)
380
- })
381
- }
382
- })
383
-
384
- const subscription = new Subscription({
385
- service: `ws://localhost:${port}`,
386
- method: '',
387
- heartbeatIntervalMs: 500,
388
- validate: (obj) => {
389
- return lex.assertValidXrpcMessage<{ count: number }>(
390
- 'io.example.streamOne',
391
- obj,
392
- )
393
- },
394
- })
395
-
396
- const messages: { count: number }[] = []
397
- for await (const msg of subscription) {
398
- messages.push(msg)
399
- }
400
-
401
- expect(messages).toEqual([{ count: 1 }])
402
- expect(firstWasClosed).toBe(true)
403
- server.close()
404
- })
405
349
  })
@@ -1 +1 @@
1
- {"root":["./src/auth.ts","./src/errors.ts","./src/index.ts","./src/logger.ts","./src/rate-limiter.ts","./src/server.ts","./src/types.ts","./src/util.ts","./src/stream/frames.ts","./src/stream/index.ts","./src/stream/logger.ts","./src/stream/server.ts","./src/stream/stream.ts","./src/stream/subscription.ts","./src/stream/types.ts","./src/stream/websocket-keepalive.ts"],"version":"5.8.2"}
1
+ {"root":["./src/auth.ts","./src/errors.ts","./src/index.ts","./src/logger.ts","./src/rate-limiter.ts","./src/server.ts","./src/types.ts","./src/util.ts","./src/stream/frames.ts","./src/stream/index.ts","./src/stream/logger.ts","./src/stream/server.ts","./src/stream/stream.ts","./src/stream/subscription.ts","./src/stream/types.ts"],"version":"5.8.2"}
@@ -1,24 +0,0 @@
1
- import { ClientOptions, WebSocket } from 'ws';
2
- export declare class WebSocketKeepAlive {
3
- opts: ClientOptions & {
4
- getUrl: () => Promise<string>;
5
- maxReconnectSeconds?: number;
6
- signal?: AbortSignal;
7
- heartbeatIntervalMs?: number;
8
- onReconnectError?: (error: unknown, n: number, initialSetup: boolean) => void;
9
- };
10
- ws: WebSocket | null;
11
- initialSetup: boolean;
12
- reconnects: number | null;
13
- constructor(opts: ClientOptions & {
14
- getUrl: () => Promise<string>;
15
- maxReconnectSeconds?: number;
16
- signal?: AbortSignal;
17
- heartbeatIntervalMs?: number;
18
- onReconnectError?: (error: unknown, n: number, initialSetup: boolean) => void;
19
- });
20
- [Symbol.asyncIterator](): AsyncGenerator<Uint8Array>;
21
- startHeartbeat(ws: WebSocket): void;
22
- }
23
- export default WebSocketKeepAlive;
24
- //# sourceMappingURL=websocket-keepalive.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"websocket-keepalive.d.ts","sourceRoot":"","sources":["../../src/stream/websocket-keepalive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,IAAI,CAAA;AAK7C,qBAAa,kBAAkB;IAMpB,IAAI,EAAE,aAAa,GAAG;QAC3B,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;QAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAA;QAC5B,MAAM,CAAC,EAAE,WAAW,CAAA;QACpB,mBAAmB,CAAC,EAAE,MAAM,CAAA;QAC5B,gBAAgB,CAAC,EAAE,CACjB,KAAK,EAAE,OAAO,EACd,CAAC,EAAE,MAAM,EACT,YAAY,EAAE,OAAO,KAClB,IAAI,CAAA;KACV;IAfI,EAAE,EAAE,SAAS,GAAG,IAAI,CAAO;IAC3B,YAAY,UAAO;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAO;gBAG9B,IAAI,EAAE,aAAa,GAAG;QAC3B,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;QAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAA;QAC5B,MAAM,CAAC,EAAE,WAAW,CAAA;QACpB,mBAAmB,CAAC,EAAE,MAAM,CAAA;QAC5B,gBAAgB,CAAC,EAAE,CACjB,KAAK,EAAE,OAAO,EACd,CAAC,EAAE,MAAM,EACT,YAAY,EAAE,OAAO,KAClB,IAAI,CAAA;KACV;IAGI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,cAAc,CAAC,UAAU,CAAC;IAwD3D,cAAc,CAAC,EAAE,EAAE,SAAS;CA4B7B;AAED,eAAe,kBAAkB,CAAA"}
@@ -1,160 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WebSocketKeepAlive = void 0;
4
- const ws_1 = require("ws");
5
- const common_1 = require("@atproto/common");
6
- const stream_1 = require("./stream");
7
- const types_1 = require("./types");
8
- class WebSocketKeepAlive {
9
- constructor(opts) {
10
- Object.defineProperty(this, "opts", {
11
- enumerable: true,
12
- configurable: true,
13
- writable: true,
14
- value: opts
15
- });
16
- Object.defineProperty(this, "ws", {
17
- enumerable: true,
18
- configurable: true,
19
- writable: true,
20
- value: null
21
- });
22
- Object.defineProperty(this, "initialSetup", {
23
- enumerable: true,
24
- configurable: true,
25
- writable: true,
26
- value: true
27
- });
28
- Object.defineProperty(this, "reconnects", {
29
- enumerable: true,
30
- configurable: true,
31
- writable: true,
32
- value: null
33
- });
34
- }
35
- async *[Symbol.asyncIterator]() {
36
- const maxReconnectMs = 1000 * (this.opts.maxReconnectSeconds ?? 64);
37
- while (true) {
38
- if (this.reconnects !== null) {
39
- const duration = this.initialSetup
40
- ? Math.min(1000, maxReconnectMs)
41
- : backoffMs(this.reconnects++, maxReconnectMs);
42
- await (0, common_1.wait)(duration);
43
- }
44
- const url = await this.opts.getUrl();
45
- this.ws = new ws_1.WebSocket(url, this.opts);
46
- const ac = new AbortController();
47
- if (this.opts.signal) {
48
- forwardSignal(this.opts.signal, ac);
49
- }
50
- this.ws.once('open', () => {
51
- this.initialSetup = false;
52
- this.reconnects = 0;
53
- if (this.ws) {
54
- this.startHeartbeat(this.ws);
55
- }
56
- });
57
- this.ws.once('close', (code, reason) => {
58
- if (code === types_1.CloseCode.Abnormal) {
59
- // Forward into an error to distinguish from a clean close
60
- ac.abort(new AbnormalCloseError(`Abnormal ws close: ${reason.toString()}`));
61
- }
62
- });
63
- try {
64
- const wsStream = (0, stream_1.streamByteChunks)(this.ws, { signal: ac.signal });
65
- for await (const chunk of wsStream) {
66
- yield chunk;
67
- }
68
- }
69
- catch (_err) {
70
- const err = _err?.['code'] === 'ABORT_ERR' ? _err['cause'] : _err;
71
- if (err instanceof types_1.DisconnectError) {
72
- // We cleanly end the connection
73
- this.ws?.close(err.wsCode);
74
- break;
75
- }
76
- this.ws?.close(); // No-ops if already closed or closing
77
- if (isReconnectable(err)) {
78
- this.reconnects ?? (this.reconnects = 0); // Never reconnect with a null
79
- this.opts.onReconnectError?.(err, this.reconnects, this.initialSetup);
80
- continue;
81
- }
82
- else {
83
- throw err;
84
- }
85
- }
86
- break; // Other side cleanly ended stream and disconnected
87
- }
88
- }
89
- startHeartbeat(ws) {
90
- let isAlive = true;
91
- let heartbeatInterval = null;
92
- const checkAlive = () => {
93
- if (!isAlive) {
94
- return ws.terminate();
95
- }
96
- isAlive = false; // expect websocket to no longer be alive unless we receive a "pong" within the interval
97
- ws.ping();
98
- };
99
- checkAlive();
100
- heartbeatInterval = setInterval(checkAlive, this.opts.heartbeatIntervalMs ?? 10 * common_1.SECOND);
101
- ws.on('pong', () => {
102
- isAlive = true;
103
- });
104
- ws.once('close', () => {
105
- if (heartbeatInterval) {
106
- clearInterval(heartbeatInterval);
107
- heartbeatInterval = null;
108
- }
109
- });
110
- }
111
- }
112
- exports.WebSocketKeepAlive = WebSocketKeepAlive;
113
- exports.default = WebSocketKeepAlive;
114
- class AbnormalCloseError extends Error {
115
- constructor() {
116
- super(...arguments);
117
- Object.defineProperty(this, "code", {
118
- enumerable: true,
119
- configurable: true,
120
- writable: true,
121
- value: 'EWSABNORMALCLOSE'
122
- });
123
- }
124
- }
125
- function isReconnectable(err) {
126
- // Network errors are reconnectable.
127
- // AuthenticationRequired and InvalidRequest XRPCErrors are not reconnectable.
128
- // @TODO method-specific XRPCErrors may be reconnectable, need to consider. Receiving
129
- // an invalid message is not current reconnectable, but the user can decide to skip them.
130
- if (!err || typeof err['code'] !== 'string')
131
- return false;
132
- return networkErrorCodes.includes(err['code']);
133
- }
134
- const networkErrorCodes = [
135
- 'EWSABNORMALCLOSE',
136
- 'ECONNRESET',
137
- 'ECONNREFUSED',
138
- 'ECONNABORTED',
139
- 'EPIPE',
140
- 'ETIMEDOUT',
141
- 'ECANCELED',
142
- ];
143
- function backoffMs(n, maxMs) {
144
- const baseSec = Math.pow(2, n); // 1, 2, 4, ...
145
- const randSec = Math.random() - 0.5; // Random jitter between -.5 and .5 seconds
146
- const ms = 1000 * (baseSec + randSec);
147
- return Math.min(ms, maxMs);
148
- }
149
- function forwardSignal(signal, ac) {
150
- if (signal.aborted) {
151
- return ac.abort(signal.reason);
152
- }
153
- else {
154
- signal.addEventListener('abort', () => ac.abort(signal.reason), {
155
- // @ts-ignore https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68625
156
- signal: ac.signal,
157
- });
158
- }
159
- }
160
- //# sourceMappingURL=websocket-keepalive.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"websocket-keepalive.js","sourceRoot":"","sources":["../../src/stream/websocket-keepalive.ts"],"names":[],"mappings":"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"}
@@ -1,152 +0,0 @@
1
- import { ClientOptions, WebSocket } from 'ws'
2
- import { SECOND, wait } from '@atproto/common'
3
- import { streamByteChunks } from './stream'
4
- import { CloseCode, DisconnectError } from './types'
5
-
6
- export class WebSocketKeepAlive {
7
- public ws: WebSocket | null = null
8
- public initialSetup = true
9
- public reconnects: number | null = null
10
-
11
- constructor(
12
- public opts: ClientOptions & {
13
- getUrl: () => Promise<string>
14
- maxReconnectSeconds?: number
15
- signal?: AbortSignal
16
- heartbeatIntervalMs?: number
17
- onReconnectError?: (
18
- error: unknown,
19
- n: number,
20
- initialSetup: boolean,
21
- ) => void
22
- },
23
- ) {}
24
-
25
- async *[Symbol.asyncIterator](): AsyncGenerator<Uint8Array> {
26
- const maxReconnectMs = 1000 * (this.opts.maxReconnectSeconds ?? 64)
27
- while (true) {
28
- if (this.reconnects !== null) {
29
- const duration = this.initialSetup
30
- ? Math.min(1000, maxReconnectMs)
31
- : backoffMs(this.reconnects++, maxReconnectMs)
32
- await wait(duration)
33
- }
34
- const url = await this.opts.getUrl()
35
- this.ws = new WebSocket(url, this.opts)
36
- const ac = new AbortController()
37
- if (this.opts.signal) {
38
- forwardSignal(this.opts.signal, ac)
39
- }
40
- this.ws.once('open', () => {
41
- this.initialSetup = false
42
- this.reconnects = 0
43
- if (this.ws) {
44
- this.startHeartbeat(this.ws)
45
- }
46
- })
47
- this.ws.once('close', (code, reason) => {
48
- if (code === CloseCode.Abnormal) {
49
- // Forward into an error to distinguish from a clean close
50
- ac.abort(
51
- new AbnormalCloseError(`Abnormal ws close: ${reason.toString()}`),
52
- )
53
- }
54
- })
55
-
56
- try {
57
- const wsStream = streamByteChunks(this.ws, { signal: ac.signal })
58
- for await (const chunk of wsStream) {
59
- yield chunk
60
- }
61
- } catch (_err) {
62
- const err = _err?.['code'] === 'ABORT_ERR' ? _err['cause'] : _err
63
- if (err instanceof DisconnectError) {
64
- // We cleanly end the connection
65
- this.ws?.close(err.wsCode)
66
- break
67
- }
68
- this.ws?.close() // No-ops if already closed or closing
69
- if (isReconnectable(err)) {
70
- this.reconnects ??= 0 // Never reconnect with a null
71
- this.opts.onReconnectError?.(err, this.reconnects, this.initialSetup)
72
- continue
73
- } else {
74
- throw err
75
- }
76
- }
77
- break // Other side cleanly ended stream and disconnected
78
- }
79
- }
80
-
81
- startHeartbeat(ws: WebSocket) {
82
- let isAlive = true
83
- let heartbeatInterval: NodeJS.Timeout | null = null
84
-
85
- const checkAlive = () => {
86
- if (!isAlive) {
87
- return ws.terminate()
88
- }
89
- isAlive = false // expect websocket to no longer be alive unless we receive a "pong" within the interval
90
- ws.ping()
91
- }
92
-
93
- checkAlive()
94
- heartbeatInterval = setInterval(
95
- checkAlive,
96
- this.opts.heartbeatIntervalMs ?? 10 * SECOND,
97
- )
98
-
99
- ws.on('pong', () => {
100
- isAlive = true
101
- })
102
- ws.once('close', () => {
103
- if (heartbeatInterval) {
104
- clearInterval(heartbeatInterval)
105
- heartbeatInterval = null
106
- }
107
- })
108
- }
109
- }
110
-
111
- export default WebSocketKeepAlive
112
-
113
- class AbnormalCloseError extends Error {
114
- code = 'EWSABNORMALCLOSE'
115
- }
116
-
117
- function isReconnectable(err: unknown): boolean {
118
- // Network errors are reconnectable.
119
- // AuthenticationRequired and InvalidRequest XRPCErrors are not reconnectable.
120
- // @TODO method-specific XRPCErrors may be reconnectable, need to consider. Receiving
121
- // an invalid message is not current reconnectable, but the user can decide to skip them.
122
- if (!err || typeof err['code'] !== 'string') return false
123
- return networkErrorCodes.includes(err['code'])
124
- }
125
-
126
- const networkErrorCodes = [
127
- 'EWSABNORMALCLOSE',
128
- 'ECONNRESET',
129
- 'ECONNREFUSED',
130
- 'ECONNABORTED',
131
- 'EPIPE',
132
- 'ETIMEDOUT',
133
- 'ECANCELED',
134
- ]
135
-
136
- function backoffMs(n: number, maxMs: number) {
137
- const baseSec = Math.pow(2, n) // 1, 2, 4, ...
138
- const randSec = Math.random() - 0.5 // Random jitter between -.5 and .5 seconds
139
- const ms = 1000 * (baseSec + randSec)
140
- return Math.min(ms, maxMs)
141
- }
142
-
143
- function forwardSignal(signal: AbortSignal, ac: AbortController) {
144
- if (signal.aborted) {
145
- return ac.abort(signal.reason)
146
- } else {
147
- signal.addEventListener('abort', () => ac.abort(signal.reason), {
148
- // @ts-ignore https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68625
149
- signal: ac.signal,
150
- })
151
- }
152
- }