@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.
- package/CHANGELOG.md +26 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +7 -8
- package/dist/auth.js.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/rate-limiter.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -2
- package/dist/server.js.map +1 -1
- package/dist/stream/frames.d.ts +7 -6
- package/dist/stream/frames.d.ts.map +1 -1
- package/dist/stream/frames.js +7 -54
- package/dist/stream/frames.js.map +1 -1
- package/dist/stream/index.d.ts +0 -1
- package/dist/stream/index.d.ts.map +1 -1
- package/dist/stream/index.js +0 -1
- package/dist/stream/index.js.map +1 -1
- package/dist/stream/logger.js.map +1 -1
- package/dist/stream/server.d.ts.map +1 -1
- package/dist/stream/server.js +4 -4
- package/dist/stream/server.js.map +1 -1
- package/dist/stream/stream.d.ts +3 -3
- package/dist/stream/stream.d.ts.map +1 -1
- package/dist/stream/stream.js.map +1 -1
- package/dist/stream/subscription.d.ts.map +1 -1
- package/dist/stream/subscription.js +13 -8
- package/dist/stream/subscription.js.map +1 -1
- package/dist/stream/types.d.ts +0 -10
- package/dist/stream/types.d.ts.map +1 -1
- package/dist/stream/types.js +1 -26
- package/dist/stream/types.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js.map +1 -1
- package/dist/util.js.map +1 -1
- package/package.json +7 -6
- package/src/auth.ts +7 -8
- package/src/server.ts +3 -3
- package/src/stream/frames.ts +19 -20
- package/src/stream/index.ts +0 -1
- package/src/stream/server.ts +1 -1
- package/src/stream/stream.ts +1 -1
- package/src/stream/subscription.ts +14 -7
- package/src/stream/types.ts +0 -16
- package/tests/auth.test.ts +5 -2
- package/tests/bodies.test.ts +2 -0
- package/tests/frames.test.ts +16 -18
- package/tests/subscriptions.test.ts +1 -57
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/stream/websocket-keepalive.d.ts +0 -24
- package/dist/stream/websocket-keepalive.d.ts.map +0 -1
- package/dist/stream/websocket-keepalive.js +0 -160
- package/dist/stream/websocket-keepalive.js.map +0 -1
- package/src/stream/websocket-keepalive.ts +0 -152
package/src/stream/types.ts
CHANGED
|
@@ -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
|
-
}
|
package/tests/auth.test.ts
CHANGED
|
@@ -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(
|
|
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
|
}
|
package/tests/bodies.test.ts
CHANGED
package/tests/frames.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
85
|
-
expect(() => Frame.fromBytes(bytes)).toThrow(
|
|
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 =
|
|
93
|
-
|
|
94
|
-
|
|
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 =
|
|
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 =
|
|
115
|
+
const bytes = Buffer.concat([
|
|
118
116
|
messageFrame.toBytes(),
|
|
119
|
-
|
|
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 =
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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"
|
|
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
|
-
}
|