@atproto/ws-client 0.1.0 → 0.1.2
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 +15 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/tests/keepalive.test.ts +17 -5
- package/tsconfig.build.json +2 -2
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.json +2 -2
- package/tsconfig.tests.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @atproto/ws-client
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#5117](https://github.com/bluesky-social/atproto/pull/5117) [`8a4c88b`](https://github.com/bluesky-social/atproto/commit/8a4c88b0f63fb2d82b1391d64c54e0c760fac48b) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Make tests less flaky in CI
|
|
8
|
+
|
|
9
|
+
## 0.1.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#4967](https://github.com/bluesky-social/atproto/pull/4967) [`9fc720c`](https://github.com/bluesky-social/atproto/commit/9fc720ce75f3ee88a5e48a9be919b07c7647f6f5) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use TypeScript 7 to build package
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [[`9fc720c`](https://github.com/bluesky-social/atproto/commit/9fc720ce75f3ee88a5e48a9be919b07c7647f6f5)]:
|
|
16
|
+
- @atproto/common@0.6.3
|
|
17
|
+
|
|
3
18
|
## 0.1.0
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,IAAI,CAAA;AACvC,OAAO,EAAE,SAAS,EAAyB,MAAM,IAAI,CAAA;AAGrD,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,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;QACxB,gBAAgB,CAAC,EAAE,CACjB,KAAK,EAAE,OAAO,EACd,CAAC,EAAE,MAAM,EACT,YAAY,EAAE,OAAO,KAClB,IAAI,CAAA;KACV;IAhBI,EAAE,EAAE,SAAS,GAAG,IAAI,CAAO;IAC3B,YAAY,UAAO;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAO;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,IAAI,CAAA;AACvC,OAAO,EAAE,SAAS,EAAyB,MAAM,IAAI,CAAA;AAGrD,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,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;QACxB,gBAAgB,CAAC,EAAE,CACjB,KAAK,EAAE,OAAO,EACd,CAAC,EAAE,MAAM,EACT,YAAY,EAAE,OAAO,KAClB,IAAI,CAAA;KACV;IAhBI,EAAE,EAAE,SAAS,GAAG,IAAI,CAAO;IAC3B,YAAY,UAAO;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAO;IAEvC,YACS,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,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;QACxB,gBAAgB,CAAC,EAAE,CACjB,KAAK,EAAE,OAAO,EACd,CAAC,EAAE,MAAM,EACT,YAAY,EAAE,OAAO,KAClB,IAAI,CAAA;KACV,EACC;IAEG,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,cAAc,CAAC,UAAU,CAAC,CA+D1D;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAczC;IAED,WAAW,IAAI,OAAO,CAErB;IAED,cAAc,CAAC,EAAE,EAAE,SAAS,QA2B3B;CACF;eAEc,kBAAkB;AAMjC,qBAAa,eAAgB,SAAQ,KAAK;IAE/B,MAAM,EAAE,SAAS;IACjB,QAAQ,CAAC,EAAE,MAAM;IAF1B,YACS,MAAM,GAAE,SAA4B,EACpC,QAAQ,CAAC,EAAE,MAAM,YAAA,EAGzB;CACF;AAGD,oBAAY,SAAS;IACnB,MAAM,OAAO;IACb,QAAQ,OAAO;IACf,MAAM,OAAO;CACd"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ClientOptions } from 'ws'\nimport { WebSocket, createWebSocketStream } from 'ws'\nimport { SECOND, isErrnoException, wait } from '@atproto/common'\n\nexport class WebSocketKeepAlive {\n public ws: WebSocket | null = null\n public initialSetup = true\n public reconnects: number | null = null\n\n constructor(\n public opts: ClientOptions & {\n getUrl: () => Promise<string>\n maxReconnectSeconds?: number\n signal?: AbortSignal\n heartbeatIntervalMs?: number\n onReconnect?: () => void\n onReconnectError?: (\n error: unknown,\n n: number,\n initialSetup: boolean,\n ) => void\n },\n ) {}\n\n async *[Symbol.asyncIterator](): AsyncGenerator<Uint8Array> {\n const maxReconnectMs = 1000 * (this.opts.maxReconnectSeconds ?? 64)\n while (true) {\n if (this.reconnects !== null) {\n const duration = this.initialSetup\n ? Math.min(1000, maxReconnectMs)\n : backoffMs(this.reconnects++, maxReconnectMs)\n await wait(duration)\n }\n const url = await this.opts.getUrl()\n this.ws = new WebSocket(url, this.opts)\n const ac = new AbortController()\n if (this.opts.signal) {\n forwardSignal(this.opts.signal, ac)\n }\n this.ws.once('open', () => {\n if (!this.initialSetup && this.opts.onReconnect) {\n this.opts.onReconnect()\n }\n this.initialSetup = false\n this.reconnects = 0\n if (this.ws) {\n this.startHeartbeat(this.ws)\n }\n })\n this.ws.once('close', (code, reason) => {\n if (code === CloseCode.Abnormal) {\n // Forward into an error to distinguish from a clean close\n ac.abort(\n new AbnormalCloseError(`Abnormal ws close: ${reason.toString()}`),\n )\n }\n })\n\n try {\n const wsStream = createWebSocketStream(this.ws, {\n signal: ac.signal,\n readableObjectMode: true, // Ensures frame bytes don't get buffered/combined together\n })\n for await (const chunk of wsStream) {\n yield chunk\n }\n } catch (_err) {\n const err =\n isErrnoException(_err) && _err.code === 'ABORT_ERR'\n ? _err.cause\n : _err\n if (err instanceof DisconnectError) {\n // We cleanly end the connection\n this.ws?.close(err.wsCode)\n break\n }\n this.ws?.close() // No-ops if already closed or closing\n if (isReconnectable(err)) {\n this.reconnects ??= 0 // Never reconnect with a null\n this.opts.onReconnectError?.(err, this.reconnects, this.initialSetup)\n continue\n } else {\n throw err\n }\n }\n break // Other side cleanly ended stream and disconnected\n }\n }\n\n send(data: string | Buffer): Promise<void> {\n return new Promise((resolve, reject) => {\n if (!this.ws || this.ws.readyState !== 1 /* OPEN */) {\n reject(new Error('WebSocket is not connected'))\n return\n }\n this.ws.send(data, (err) => {\n if (err) {\n reject(err)\n } else {\n resolve()\n }\n })\n })\n }\n\n isConnected(): boolean {\n return this.ws !== null && this.ws.readyState === 1\n }\n\n startHeartbeat(ws: WebSocket) {\n let isAlive = true\n let heartbeatInterval: NodeJS.Timeout | null = null\n\n const checkAlive = () => {\n if (!isAlive) {\n return ws.terminate()\n }\n isAlive = false // expect websocket to no longer be alive unless we receive a \"pong\" within the interval\n ws.ping()\n }\n\n checkAlive()\n heartbeatInterval = setInterval(\n checkAlive,\n this.opts.heartbeatIntervalMs ?? 10 * SECOND,\n )\n\n ws.on('pong', () => {\n isAlive = true\n })\n ws.once('close', () => {\n if (heartbeatInterval) {\n clearInterval(heartbeatInterval)\n heartbeatInterval = null\n }\n })\n }\n}\n\nexport default WebSocketKeepAlive\n\nclass AbnormalCloseError extends Error {\n code = 'EWSABNORMALCLOSE'\n}\n\nexport class DisconnectError extends Error {\n constructor(\n public wsCode: CloseCode = CloseCode.Policy,\n public xrpcCode?: string,\n ) {\n super()\n }\n}\n\n// https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1\nexport enum CloseCode {\n Normal = 1000,\n Abnormal = 1006,\n Policy = 1008,\n}\n\nfunction isReconnectable(err: unknown): boolean {\n // Network errors are reconnectable.\n // AuthenticationRequired and InvalidRequest XRPCErrors are not reconnectable.\n // @TODO method-specific XRPCErrors may be reconnectable, need to consider. Receiving\n // an invalid message is not current reconnectable, but the user can decide to skip them.\n if (isErrnoException(err) && typeof err.code === 'string') {\n return networkErrorCodes.includes(err.code)\n }\n return false\n}\n\nconst networkErrorCodes = [\n 'EWSABNORMALCLOSE',\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'ECONNABORTED',\n 'EPIPE',\n 'ETIMEDOUT',\n 'ECANCELED',\n]\n\nfunction backoffMs(n: number, maxMs: number) {\n const baseSec = Math.pow(2, n) // 1, 2, 4, ...\n const randSec = Math.random() - 0.5 // Random jitter between -.5 and .5 seconds\n const ms = 1000 * (baseSec + randSec)\n return Math.min(ms, maxMs)\n}\n\nfunction forwardSignal(signal: AbortSignal, ac: AbortController) {\n if (signal.aborted) {\n return ac.abort(signal.reason)\n } else {\n signal.addEventListener('abort', () => ac.abort(signal.reason), {\n // @ts-ignore https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68625\n signal: ac.signal,\n })\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ClientOptions } from 'ws'\nimport { WebSocket, createWebSocketStream } from 'ws'\nimport { SECOND, isErrnoException, wait } from '@atproto/common'\n\nexport class WebSocketKeepAlive {\n public ws: WebSocket | null = null\n public initialSetup = true\n public reconnects: number | null = null\n\n constructor(\n public opts: ClientOptions & {\n getUrl: () => Promise<string>\n maxReconnectSeconds?: number\n signal?: AbortSignal\n heartbeatIntervalMs?: number\n onReconnect?: () => void\n onReconnectError?: (\n error: unknown,\n n: number,\n initialSetup: boolean,\n ) => void\n },\n ) {}\n\n async *[Symbol.asyncIterator](): AsyncGenerator<Uint8Array> {\n const maxReconnectMs = 1000 * (this.opts.maxReconnectSeconds ?? 64)\n while (true) {\n if (this.reconnects !== null) {\n const duration = this.initialSetup\n ? Math.min(1000, maxReconnectMs)\n : backoffMs(this.reconnects++, maxReconnectMs)\n await wait(duration)\n }\n const url = await this.opts.getUrl()\n this.ws = new WebSocket(url, this.opts)\n const ac = new AbortController()\n if (this.opts.signal) {\n forwardSignal(this.opts.signal, ac)\n }\n this.ws.once('open', () => {\n if (!this.initialSetup && this.opts.onReconnect) {\n this.opts.onReconnect()\n }\n this.initialSetup = false\n this.reconnects = 0\n if (this.ws) {\n this.startHeartbeat(this.ws)\n }\n })\n this.ws.once('close', (code, reason) => {\n if (code === CloseCode.Abnormal) {\n // Forward into an error to distinguish from a clean close\n ac.abort(\n new AbnormalCloseError(`Abnormal ws close: ${reason.toString()}`),\n )\n }\n })\n\n try {\n const wsStream = createWebSocketStream(this.ws, {\n signal: ac.signal,\n readableObjectMode: true, // Ensures frame bytes don't get buffered/combined together\n })\n for await (const chunk of wsStream) {\n yield chunk\n }\n } catch (_err) {\n const err =\n isErrnoException(_err) && _err.code === 'ABORT_ERR'\n ? _err.cause\n : _err\n if (err instanceof DisconnectError) {\n // We cleanly end the connection\n this.ws?.close(err.wsCode)\n break\n }\n this.ws?.close() // No-ops if already closed or closing\n if (isReconnectable(err)) {\n this.reconnects ??= 0 // Never reconnect with a null\n this.opts.onReconnectError?.(err, this.reconnects, this.initialSetup)\n continue\n } else {\n throw err\n }\n }\n break // Other side cleanly ended stream and disconnected\n }\n }\n\n send(data: string | Buffer): Promise<void> {\n return new Promise((resolve, reject) => {\n if (!this.ws || this.ws.readyState !== 1 /* OPEN */) {\n reject(new Error('WebSocket is not connected'))\n return\n }\n this.ws.send(data, (err) => {\n if (err) {\n reject(err)\n } else {\n resolve()\n }\n })\n })\n }\n\n isConnected(): boolean {\n return this.ws !== null && this.ws.readyState === 1\n }\n\n startHeartbeat(ws: WebSocket) {\n let isAlive = true\n let heartbeatInterval: NodeJS.Timeout | null = null\n\n const checkAlive = () => {\n if (!isAlive) {\n return ws.terminate()\n }\n isAlive = false // expect websocket to no longer be alive unless we receive a \"pong\" within the interval\n ws.ping()\n }\n\n checkAlive()\n heartbeatInterval = setInterval(\n checkAlive,\n this.opts.heartbeatIntervalMs ?? 10 * SECOND,\n )\n\n ws.on('pong', () => {\n isAlive = true\n })\n ws.once('close', () => {\n if (heartbeatInterval) {\n clearInterval(heartbeatInterval)\n heartbeatInterval = null\n }\n })\n }\n}\n\nexport default WebSocketKeepAlive\n\nclass AbnormalCloseError extends Error {\n code = 'EWSABNORMALCLOSE'\n}\n\nexport class DisconnectError extends Error {\n constructor(\n public wsCode: CloseCode = CloseCode.Policy,\n public xrpcCode?: string,\n ) {\n super()\n }\n}\n\n// https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1\nexport enum CloseCode {\n Normal = 1000,\n Abnormal = 1006,\n Policy = 1008,\n}\n\nfunction isReconnectable(err: unknown): boolean {\n // Network errors are reconnectable.\n // AuthenticationRequired and InvalidRequest XRPCErrors are not reconnectable.\n // @TODO method-specific XRPCErrors may be reconnectable, need to consider. Receiving\n // an invalid message is not current reconnectable, but the user can decide to skip them.\n if (isErrnoException(err) && typeof err.code === 'string') {\n return networkErrorCodes.includes(err.code)\n }\n return false\n}\n\nconst networkErrorCodes = [\n 'EWSABNORMALCLOSE',\n 'ECONNRESET',\n 'ECONNREFUSED',\n 'ECONNABORTED',\n 'EPIPE',\n 'ETIMEDOUT',\n 'ECANCELED',\n]\n\nfunction backoffMs(n: number, maxMs: number) {\n const baseSec = Math.pow(2, n) // 1, 2, 4, ...\n const randSec = Math.random() - 0.5 // Random jitter between -.5 and .5 seconds\n const ms = 1000 * (baseSec + randSec)\n return Math.min(ms, maxMs)\n}\n\nfunction forwardSignal(signal: AbortSignal, ac: AbortController) {\n if (signal.aborted) {\n return ac.abort(signal.reason)\n } else {\n signal.addEventListener('abort', () => ac.abort(signal.reason), {\n // @ts-ignore https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68625\n signal: ac.signal,\n })\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/ws-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Websocket client library",
|
|
6
6
|
"keywords": [
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"ws": "^8.12.0",
|
|
21
|
-
"@atproto/common": "^0.6.
|
|
21
|
+
"@atproto/common": "^0.6.3"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/ws": "^8.5.4",
|
|
25
25
|
"get-port": "^6.1.2",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
26
|
+
"http-terminator": "^3.2.0",
|
|
27
|
+
"jest": "^30.0.0"
|
|
28
28
|
},
|
|
29
29
|
"type": "module",
|
|
30
30
|
"exports": {
|
|
@@ -35,6 +35,6 @@
|
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
38
|
-
"build": "
|
|
38
|
+
"build": "tsgo --build tsconfig.build.json"
|
|
39
39
|
}
|
|
40
40
|
}
|
package/tests/keepalive.test.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { once } from 'node:events'
|
|
2
|
+
import { createServer } from 'node:http'
|
|
3
|
+
import { AddressInfo } from 'node:net'
|
|
4
|
+
// eslint-disable-next-line import/default
|
|
5
|
+
import httpTerminator from 'http-terminator'
|
|
2
6
|
import { WebSocketServer } from 'ws'
|
|
3
7
|
import { wait } from '@atproto/common'
|
|
4
8
|
import { CloseCode, WebSocketKeepAlive } from '../src/index.js'
|
|
@@ -7,11 +11,17 @@ describe('WebSocketKeepAlive', () => {
|
|
|
7
11
|
it('uses a heartbeat to reconnect if a connection is dropped', async () => {
|
|
8
12
|
// we run a server that, on first connection, pauses for longer than the heartbeat interval (doesn't return "pong"s)
|
|
9
13
|
// on second connection, it sends a message and then closes
|
|
10
|
-
|
|
11
|
-
const server =
|
|
14
|
+
|
|
15
|
+
const server = createServer()
|
|
16
|
+
|
|
17
|
+
// make sure to always close the server (even in case of test failure)
|
|
18
|
+
const { terminate } = httpTerminator.createHttpTerminator({ server })
|
|
19
|
+
await using _ = { [Symbol.asyncDispose]: async () => terminate() }
|
|
20
|
+
|
|
21
|
+
const wsServer = new WebSocketServer({ server })
|
|
12
22
|
let firstConnection = true
|
|
13
23
|
let firstWasClosed = false
|
|
14
|
-
|
|
24
|
+
wsServer.on('connection', async (socket) => {
|
|
15
25
|
if (firstConnection === true) {
|
|
16
26
|
firstConnection = false
|
|
17
27
|
socket.pause()
|
|
@@ -32,6 +42,9 @@ describe('WebSocketKeepAlive', () => {
|
|
|
32
42
|
}
|
|
33
43
|
})
|
|
34
44
|
|
|
45
|
+
await once(server.listen(0), 'listening')
|
|
46
|
+
const port = (server.address() as AddressInfo).port
|
|
47
|
+
|
|
35
48
|
const wsKeepAlive = new WebSocketKeepAlive({
|
|
36
49
|
getUrl: async () => `ws://localhost:${port}`,
|
|
37
50
|
heartbeatIntervalMs: 500,
|
|
@@ -45,6 +58,5 @@ describe('WebSocketKeepAlive', () => {
|
|
|
45
58
|
expect(messages).toHaveLength(1)
|
|
46
59
|
expect(Buffer.from(messages[0]).toString()).toBe('test message')
|
|
47
60
|
expect(firstWasClosed).toBe(true)
|
|
48
|
-
server.close()
|
|
49
61
|
})
|
|
50
62
|
})
|
package/tsconfig.build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/index.ts"]
|
|
1
|
+
{"version":"7.0.0-dev.20260614.1","root":["./src/index.ts"]}
|
package/tsconfig.json
CHANGED