@atproto/ws-client 0.0.4 → 0.1.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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @atproto/ws-client
2
2
 
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#4929](https://github.com/bluesky-social/atproto/pull/4929) [`f01c59f`](https://github.com/bluesky-social/atproto/commit/f01c59f5bd3f75fb8b47a9eecd4858b84033fb7c) Thanks [@devinivy](https://github.com/devinivy)! - **BREAKING:** Drop support for Node.js 18 and 20. Node.js 22 is now the minimum supported version. Docker images now use Node.js 24.
8
+
9
+ - [#4943](https://github.com/bluesky-social/atproto/pull/4943) [`c459153`](https://github.com/bluesky-social/atproto/commit/c459153395a30ce89e050892c8fab7dc98e019b9) Thanks [@devinivy](https://github.com/devinivy)! - **BREAKING:** Convert to pure ESM. All packages now ship `"type": "module"` with ES module output and Node16 module resolution.
10
+
11
+ Node.js 22's `require()` compatibility layer can still load these packages in CommonJS code.
12
+
13
+ - [#4930](https://github.com/bluesky-social/atproto/pull/4930) [`908bece`](https://github.com/bluesky-social/atproto/commit/908bece169258bff5ad121e5eec157d6ded6f705) Thanks [@devinivy](https://github.com/devinivy)! - Build with TypeScript 6.0.
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [[`affb50c`](https://github.com/bluesky-social/atproto/commit/affb50c040b497a12631df99a6310f8e78cab557), [`f01c59f`](https://github.com/bluesky-social/atproto/commit/f01c59f5bd3f75fb8b47a9eecd4858b84033fb7c), [`c459153`](https://github.com/bluesky-social/atproto/commit/c459153395a30ce89e050892c8fab7dc98e019b9), [`908bece`](https://github.com/bluesky-social/atproto/commit/908bece169258bff5ad121e5eec157d6ded6f705)]:
18
+ - @atproto/common@0.6.0
19
+
3
20
  ## 0.0.4
4
21
 
5
22
  ### Patch Changes
package/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  Dual MIT/Apache-2.0 License
2
2
 
3
- Copyright (c) 2022-2025 Bluesky Social PBC, and Contributors
3
+ Copyright (c) 2022-2026 Bluesky Social PBC, and Contributors
4
4
 
5
5
  Except as otherwise noted in individual files, this software is licensed under the MIT license (<http://opensource.org/licenses/MIT>), or the Apache License, Version 2.0 (<http://www.apache.org/licenses/LICENSE-2.0>).
6
6
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { ClientOptions, WebSocket } from 'ws';
1
+ import type { ClientOptions } from 'ws';
2
+ import { WebSocket } from 'ws';
2
3
  export declare class WebSocketKeepAlive {
3
4
  opts: ClientOptions & {
4
5
  getUrl: () => Promise<string>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAyB,MAAM,IAAI,CAAA;AAGpE,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;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,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;IAGI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,cAAc,CAAC,UAAU,CAAC;IAiE3D,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB1C,WAAW,IAAI,OAAO;IAItB,cAAc,CAAC,EAAE,EAAE,SAAS;CA4B7B;AAED,eAAe,kBAAkB,CAAA;AAMjC,qBAAa,eAAgB,SAAQ,KAAK;IAE/B,MAAM,EAAE,SAAS;IACjB,QAAQ,CAAC,EAAE,MAAM;gBADjB,MAAM,GAAE,SAA4B,EACpC,QAAQ,CAAC,EAAE,MAAM,YAAA;CAI3B;AAGD,oBAAY,SAAS;IACnB,MAAM,OAAO;IACb,QAAQ,OAAO;IACf,MAAM,OAAO;CACd"}
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;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,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;IAGI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,cAAc,CAAC,UAAU,CAAC;IAiE3D,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB1C,WAAW,IAAI,OAAO;IAItB,cAAc,CAAC,EAAE,EAAE,SAAS;CA4B7B;AAED,eAAe,kBAAkB,CAAA;AAMjC,qBAAa,eAAgB,SAAQ,KAAK;IAE/B,MAAM,EAAE,SAAS;IACjB,QAAQ,CAAC,EAAE,MAAM;gBADjB,MAAM,GAAE,SAA4B,EACpC,QAAQ,CAAC,EAAE,MAAM,YAAA;CAI3B;AAGD,oBAAY,SAAS;IACnB,MAAM,OAAO;IACb,QAAQ,OAAO;IACf,MAAM,OAAO;CACd"}
package/dist/index.js CHANGED
@@ -1,34 +1,11 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CloseCode = exports.DisconnectError = exports.WebSocketKeepAlive = void 0;
4
- const ws_1 = require("ws");
5
- const common_1 = require("@atproto/common");
6
- class WebSocketKeepAlive {
1
+ import { WebSocket, createWebSocketStream } from 'ws';
2
+ import { SECOND, isErrnoException, wait } from '@atproto/common';
3
+ export class WebSocketKeepAlive {
7
4
  constructor(opts) {
8
- Object.defineProperty(this, "opts", {
9
- enumerable: true,
10
- configurable: true,
11
- writable: true,
12
- value: opts
13
- });
14
- Object.defineProperty(this, "ws", {
15
- enumerable: true,
16
- configurable: true,
17
- writable: true,
18
- value: null
19
- });
20
- Object.defineProperty(this, "initialSetup", {
21
- enumerable: true,
22
- configurable: true,
23
- writable: true,
24
- value: true
25
- });
26
- Object.defineProperty(this, "reconnects", {
27
- enumerable: true,
28
- configurable: true,
29
- writable: true,
30
- value: null
31
- });
5
+ this.opts = opts;
6
+ this.ws = null;
7
+ this.initialSetup = true;
8
+ this.reconnects = null;
32
9
  }
33
10
  async *[Symbol.asyncIterator]() {
34
11
  const maxReconnectMs = 1000 * (this.opts.maxReconnectSeconds ?? 64);
@@ -37,10 +14,10 @@ class WebSocketKeepAlive {
37
14
  const duration = this.initialSetup
38
15
  ? Math.min(1000, maxReconnectMs)
39
16
  : backoffMs(this.reconnects++, maxReconnectMs);
40
- await (0, common_1.wait)(duration);
17
+ await wait(duration);
41
18
  }
42
19
  const url = await this.opts.getUrl();
43
- this.ws = new ws_1.WebSocket(url, this.opts);
20
+ this.ws = new WebSocket(url, this.opts);
44
21
  const ac = new AbortController();
45
22
  if (this.opts.signal) {
46
23
  forwardSignal(this.opts.signal, ac);
@@ -62,7 +39,7 @@ class WebSocketKeepAlive {
62
39
  }
63
40
  });
64
41
  try {
65
- const wsStream = (0, ws_1.createWebSocketStream)(this.ws, {
42
+ const wsStream = createWebSocketStream(this.ws, {
66
43
  signal: ac.signal,
67
44
  readableObjectMode: true, // Ensures frame bytes don't get buffered/combined together
68
45
  });
@@ -71,7 +48,7 @@ class WebSocketKeepAlive {
71
48
  }
72
49
  }
73
50
  catch (_err) {
74
- const err = (0, common_1.isErrnoException)(_err) && _err.code === 'ABORT_ERR'
51
+ const err = isErrnoException(_err) && _err.code === 'ABORT_ERR'
75
52
  ? _err.cause
76
53
  : _err;
77
54
  if (err instanceof DisconnectError) {
@@ -81,7 +58,7 @@ class WebSocketKeepAlive {
81
58
  }
82
59
  this.ws?.close(); // No-ops if already closed or closing
83
60
  if (isReconnectable(err)) {
84
- this.reconnects ?? (this.reconnects = 0); // Never reconnect with a null
61
+ this.reconnects ??= 0; // Never reconnect with a null
85
62
  this.opts.onReconnectError?.(err, this.reconnects, this.initialSetup);
86
63
  continue;
87
64
  }
@@ -122,7 +99,7 @@ class WebSocketKeepAlive {
122
99
  ws.ping();
123
100
  };
124
101
  checkAlive();
125
- heartbeatInterval = setInterval(checkAlive, this.opts.heartbeatIntervalMs ?? 10 * common_1.SECOND);
102
+ heartbeatInterval = setInterval(checkAlive, this.opts.heartbeatIntervalMs ?? 10 * SECOND);
126
103
  ws.on('pong', () => {
127
104
  isAlive = true;
128
105
  });
@@ -134,50 +111,33 @@ class WebSocketKeepAlive {
134
111
  });
135
112
  }
136
113
  }
137
- exports.WebSocketKeepAlive = WebSocketKeepAlive;
138
- exports.default = WebSocketKeepAlive;
114
+ export default WebSocketKeepAlive;
139
115
  class AbnormalCloseError extends Error {
140
116
  constructor() {
141
117
  super(...arguments);
142
- Object.defineProperty(this, "code", {
143
- enumerable: true,
144
- configurable: true,
145
- writable: true,
146
- value: 'EWSABNORMALCLOSE'
147
- });
118
+ this.code = 'EWSABNORMALCLOSE';
148
119
  }
149
120
  }
150
- class DisconnectError extends Error {
121
+ export class DisconnectError extends Error {
151
122
  constructor(wsCode = CloseCode.Policy, xrpcCode) {
152
123
  super();
153
- Object.defineProperty(this, "wsCode", {
154
- enumerable: true,
155
- configurable: true,
156
- writable: true,
157
- value: wsCode
158
- });
159
- Object.defineProperty(this, "xrpcCode", {
160
- enumerable: true,
161
- configurable: true,
162
- writable: true,
163
- value: xrpcCode
164
- });
124
+ this.wsCode = wsCode;
125
+ this.xrpcCode = xrpcCode;
165
126
  }
166
127
  }
167
- exports.DisconnectError = DisconnectError;
168
128
  // https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1
169
- var CloseCode;
129
+ export var CloseCode;
170
130
  (function (CloseCode) {
171
131
  CloseCode[CloseCode["Normal"] = 1000] = "Normal";
172
132
  CloseCode[CloseCode["Abnormal"] = 1006] = "Abnormal";
173
133
  CloseCode[CloseCode["Policy"] = 1008] = "Policy";
174
- })(CloseCode || (exports.CloseCode = CloseCode = {}));
134
+ })(CloseCode || (CloseCode = {}));
175
135
  function isReconnectable(err) {
176
136
  // Network errors are reconnectable.
177
137
  // AuthenticationRequired and InvalidRequest XRPCErrors are not reconnectable.
178
138
  // @TODO method-specific XRPCErrors may be reconnectable, need to consider. Receiving
179
139
  // an invalid message is not current reconnectable, but the user can decide to skip them.
180
- if ((0, common_1.isErrnoException)(err) && typeof err.code === 'string') {
140
+ if (isErrnoException(err) && typeof err.code === 'string') {
181
141
  return networkErrorCodes.includes(err.code);
182
142
  }
183
143
  return false;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,2BAAoE;AACpE,4CAAgE;AAEhE,MAAa,kBAAkB;IAK7B,YACS,IAWN;QAXD;;;;mBAAO,IAAI;WAWV;QAhBI;;;;mBAAuB,IAAI;WAAA;QAC3B;;;;mBAAe,IAAI;WAAA;QACnB;;;;mBAA4B,IAAI;WAAA;IAepC,CAAC;IAEJ,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC;QAC3B,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,IAAI,EAAE,CAAC,CAAA;QACnE,OAAO,IAAI,EAAE,CAAC;YACZ,IAAI,IAAI,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;gBAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY;oBAChC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,cAAc,CAAC;oBAChC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,cAAc,CAAC,CAAA;gBAChD,MAAM,IAAA,aAAI,EAAC,QAAQ,CAAC,CAAA;YACtB,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAA;YACpC,IAAI,CAAC,EAAE,GAAG,IAAI,cAAS,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;YACvC,MAAM,EAAE,GAAG,IAAI,eAAe,EAAE,CAAA;YAChC,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACrB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;YACrC,CAAC;YACD,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE;gBACxB,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;oBAChD,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAA;gBACzB,CAAC;gBACD,IAAI,CAAC,YAAY,GAAG,KAAK,CAAA;gBACzB,IAAI,CAAC,UAAU,GAAG,CAAC,CAAA;gBACnB,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;oBACZ,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;gBAC9B,CAAC;YACH,CAAC,CAAC,CAAA;YACF,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;gBACrC,IAAI,IAAI,KAAK,SAAS,CAAC,QAAQ,EAAE,CAAC;oBAChC,0DAA0D;oBAC1D,EAAE,CAAC,KAAK,CACN,IAAI,kBAAkB,CAAC,sBAAsB,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC,CAClE,CAAA;gBACH,CAAC;YACH,CAAC,CAAC,CAAA;YAEF,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,IAAA,0BAAqB,EAAC,IAAI,CAAC,EAAE,EAAE;oBAC9C,MAAM,EAAE,EAAE,CAAC,MAAM;oBACjB,kBAAkB,EAAE,IAAI,EAAE,2DAA2D;iBACtF,CAAC,CAAA;gBACF,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;oBACnC,MAAM,KAAK,CAAA;gBACb,CAAC;YACH,CAAC;YAAC,OAAO,IAAI,EAAE,CAAC;gBACd,MAAM,GAAG,GACP,IAAA,yBAAgB,EAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW;oBACjD,CAAC,CAAC,IAAI,CAAC,KAAK;oBACZ,CAAC,CAAC,IAAI,CAAA;gBACV,IAAI,GAAG,YAAY,eAAe,EAAE,CAAC;oBACnC,gCAAgC;oBAChC,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;oBAC1B,MAAK;gBACP,CAAC;gBACD,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,CAAA,CAAC,sCAAsC;gBACvD,IAAI,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC;oBACzB,IAAI,CAAC,UAAU,KAAf,IAAI,CAAC,UAAU,GAAK,CAAC,EAAA,CAAC,8BAA8B;oBACpD,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC,CAAA;oBACrE,SAAQ;gBACV,CAAC;qBAAM,CAAC;oBACN,MAAM,GAAG,CAAA;gBACX,CAAC;YACH,CAAC;YACD,MAAK,CAAC,mDAAmD;QAC3D,CAAC;IACH,CAAC;IAED,IAAI,CAAC,IAAqB;QACxB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC,UAAU,KAAK,CAAC,CAAC,UAAU,EAAE,CAAC;gBACpD,MAAM,CAAC,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC,CAAA;gBAC/C,OAAM;YACR,CAAC;YACD,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE;gBACzB,IAAI,GAAG,EAAE,CAAC;oBACR,MAAM,CAAC,GAAG,CAAC,CAAA;gBACb,CAAC;qBAAM,CAAC;oBACN,OAAO,EAAE,CAAA;gBACX,CAAC;YACH,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,EAAE,KAAK,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC,UAAU,KAAK,CAAC,CAAA;IACrD,CAAC;IAED,cAAc,CAAC,EAAa;QAC1B,IAAI,OAAO,GAAG,IAAI,CAAA;QAClB,IAAI,iBAAiB,GAA0B,IAAI,CAAA;QAEnD,MAAM,UAAU,GAAG,GAAG,EAAE;YACtB,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,OAAO,EAAE,CAAC,SAAS,EAAE,CAAA;YACvB,CAAC;YACD,OAAO,GAAG,KAAK,CAAA,CAAC,wFAAwF;YACxG,EAAE,CAAC,IAAI,EAAE,CAAA;QACX,CAAC,CAAA;QAED,UAAU,EAAE,CAAA;QACZ,iBAAiB,GAAG,WAAW,CAC7B,UAAU,EACV,IAAI,CAAC,IAAI,CAAC,mBAAmB,IAAI,EAAE,GAAG,eAAM,CAC7C,CAAA;QAED,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;YACjB,OAAO,GAAG,IAAI,CAAA;QAChB,CAAC,CAAC,CAAA;QACF,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE;YACpB,IAAI,iBAAiB,EAAE,CAAC;gBACtB,aAAa,CAAC,iBAAiB,CAAC,CAAA;gBAChC,iBAAiB,GAAG,IAAI,CAAA;YAC1B,CAAC;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;CACF;AArID,gDAqIC;AAED,kBAAe,kBAAkB,CAAA;AAEjC,MAAM,kBAAmB,SAAQ,KAAK;IAAtC;;QACE;;;;mBAAO,kBAAkB;WAAA;IAC3B,CAAC;CAAA;AAED,MAAa,eAAgB,SAAQ,KAAK;IACxC,YACS,SAAoB,SAAS,CAAC,MAAM,EACpC,QAAiB;QAExB,KAAK,EAAE,CAAA;QAHP;;;;mBAAO,MAAM;WAA8B;QAC3C;;;;mBAAO,QAAQ;WAAS;IAG1B,CAAC;CACF;AAPD,0CAOC;AAED,uDAAuD;AACvD,IAAY,SAIX;AAJD,WAAY,SAAS;IACnB,gDAAa,CAAA;IACb,oDAAe,CAAA;IACf,gDAAa,CAAA;AACf,CAAC,EAJW,SAAS,yBAAT,SAAS,QAIpB;AAED,SAAS,eAAe,CAAC,GAAY;IACnC,oCAAoC;IACpC,8EAA8E;IAC9E,qFAAqF;IACrF,yFAAyF;IACzF,IAAI,IAAA,yBAAgB,EAAC,GAAG,CAAC,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC1D,OAAO,iBAAiB,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAC7C,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,iBAAiB,GAAG;IACxB,kBAAkB;IAClB,YAAY;IACZ,cAAc;IACd,cAAc;IACd,OAAO;IACP,WAAW;IACX,WAAW;CACZ,CAAA;AAED,SAAS,SAAS,CAAC,CAAS,EAAE,KAAa;IACzC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA,CAAC,eAAe;IAC9C,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAA,CAAC,2CAA2C;IAC/E,MAAM,EAAE,GAAG,IAAI,GAAG,CAAC,OAAO,GAAG,OAAO,CAAC,CAAA;IACrC,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAA;AAC5B,CAAC;AAED,SAAS,aAAa,CAAC,MAAmB,EAAE,EAAmB;IAC7D,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,OAAO,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAChC,CAAC;SAAM,CAAC;QACN,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;YAC9D,2EAA2E;YAC3E,MAAM,EAAE,EAAE,CAAC,MAAM;SAClB,CAAC,CAAA;IACJ,CAAC;AACH,CAAC","sourcesContent":["import { ClientOptions, 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"]}
@@ -0,0 +1,21 @@
1
+ /** @type {import('jest').Config} */
2
+ module.exports = {
3
+ displayName: 'WebSocket Client',
4
+ transform: {
5
+ '^.+\\.(t|j)s$': [
6
+ '@swc/jest',
7
+ {
8
+ jsc: {
9
+ parser: { syntax: 'typescript', importAttributes: true },
10
+ experimental: { keepImportAttributes: true },
11
+ transform: {},
12
+ },
13
+ module: { type: 'es6' },
14
+ },
15
+ ],
16
+ },
17
+ extensionsToTreatAsEsm: ['.ts'],
18
+ transformIgnorePatterns: [],
19
+ setupFiles: ['<rootDir>/../../test.setup.ts'],
20
+ moduleNameMapper: { '^(\\.\\.?\\/.+)\\.js$': ['$1.ts', '$1.js'] },
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/ws-client",
3
- "version": "0.0.4",
3
+ "version": "0.1.0",
4
4
  "license": "MIT",
5
5
  "description": "Websocket client library",
6
6
  "keywords": [
@@ -14,22 +14,27 @@
14
14
  "directory": "packages/ws-client"
15
15
  },
16
16
  "engines": {
17
- "node": ">=18.7.0"
17
+ "node": ">=22"
18
18
  },
19
- "main": "dist/index.js",
20
- "types": "dist/index.d.ts",
21
19
  "dependencies": {
22
20
  "ws": "^8.12.0",
23
- "@atproto/common": "^0.5.3"
21
+ "@atproto/common": "^0.6.0"
24
22
  },
25
23
  "devDependencies": {
26
24
  "@types/ws": "^8.5.4",
27
25
  "get-port": "^6.1.2",
28
- "jest": "^28.1.2",
29
- "typescript": "^5.6.3"
26
+ "jest": "^30.0.0",
27
+ "typescript": "^6.0.3"
28
+ },
29
+ "type": "module",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ }
30
35
  },
31
36
  "scripts": {
32
- "test": "jest",
37
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
33
38
  "build": "tsc --build tsconfig.build.json"
34
39
  }
35
40
  }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { ClientOptions, WebSocket, createWebSocketStream } from 'ws'
1
+ import type { ClientOptions } from 'ws'
2
+ import { WebSocket, createWebSocketStream } from 'ws'
2
3
  import { SECOND, isErrnoException, wait } from '@atproto/common'
3
4
 
4
5
  export class WebSocketKeepAlive {
@@ -1,7 +1,7 @@
1
1
  import getPort from 'get-port'
2
2
  import { WebSocketServer } from 'ws'
3
3
  import { wait } from '@atproto/common'
4
- import { CloseCode, WebSocketKeepAlive } from '../src'
4
+ import { CloseCode, WebSocketKeepAlive } from '../src/index.js'
5
5
 
6
6
  describe('WebSocketKeepAlive', () => {
7
7
  it('uses a heartbeat to reconnect if a connection is dropped', async () => {
@@ -1 +1 @@
1
- {"root":["./src/index.ts"],"version":"5.8.3"}
1
+ {"root":["./src/index.ts"],"version":"6.0.3"}
package/jest.config.js DELETED
@@ -1,8 +0,0 @@
1
- /** @type {import('jest').Config} */
2
- module.exports = {
3
- displayName: 'WebSocket Client',
4
- transform: { '^.+\\.(j|t)s$': '@swc/jest' },
5
- transformIgnorePatterns: ['/node_modules/.pnpm/(?!(get-port)@)'],
6
- setupFiles: ['<rootDir>/../../jest.setup.ts'],
7
- moduleNameMapper: { '^(\\.\\.?\\/.+)\\.js$': ['$1.ts', '$1.js'] },
8
- }
@@ -1 +0,0 @@
1
- {"root":["./tests/keepalive.test.ts"],"version":"5.8.3"}