@whitewall/blip-sdk 0.0.180 → 0.0.182

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 (40) hide show
  1. package/dist/cjs/namespaces/billing.js +20 -4
  2. package/dist/cjs/namespaces/billing.js.map +1 -1
  3. package/dist/cjs/sender/enveloperesolver.js +2 -2
  4. package/dist/cjs/sender/enveloperesolver.js.map +1 -1
  5. package/dist/cjs/sender/sender.js +6 -0
  6. package/dist/cjs/sender/sender.js.map +1 -1
  7. package/dist/cjs/sender/sessionnegotiator.js +15 -7
  8. package/dist/cjs/sender/sessionnegotiator.js.map +1 -1
  9. package/dist/cjs/sender/tcp/tcpsender.js +47 -31
  10. package/dist/cjs/sender/tcp/tcpsender.js.map +1 -1
  11. package/dist/cjs/sender/websocket/websocketsender.js +40 -12
  12. package/dist/cjs/sender/websocket/websocketsender.js.map +1 -1
  13. package/dist/esm/namespaces/billing.js +20 -4
  14. package/dist/esm/namespaces/billing.js.map +1 -1
  15. package/dist/esm/sender/enveloperesolver.js +2 -2
  16. package/dist/esm/sender/enveloperesolver.js.map +1 -1
  17. package/dist/esm/sender/sender.js +6 -0
  18. package/dist/esm/sender/sender.js.map +1 -1
  19. package/dist/esm/sender/sessionnegotiator.js +15 -7
  20. package/dist/esm/sender/sessionnegotiator.js.map +1 -1
  21. package/dist/esm/sender/tcp/tcpsender.js +47 -31
  22. package/dist/esm/sender/tcp/tcpsender.js.map +1 -1
  23. package/dist/esm/sender/websocket/websocketsender.js +40 -12
  24. package/dist/esm/sender/websocket/websocketsender.js.map +1 -1
  25. package/dist/types/namespaces/billing.d.ts +8 -1
  26. package/dist/types/namespaces/billing.d.ts.map +1 -1
  27. package/dist/types/sender/enveloperesolver.d.ts +1 -1
  28. package/dist/types/sender/enveloperesolver.d.ts.map +1 -1
  29. package/dist/types/sender/sender.d.ts +1 -0
  30. package/dist/types/sender/sender.d.ts.map +1 -1
  31. package/dist/types/sender/sessionnegotiator.d.ts +2 -0
  32. package/dist/types/sender/sessionnegotiator.d.ts.map +1 -1
  33. package/dist/types/sender/tcp/tcpsender.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/namespaces/billing.ts +21 -5
  36. package/src/sender/enveloperesolver.ts +2 -2
  37. package/src/sender/sender.ts +7 -0
  38. package/src/sender/sessionnegotiator.ts +16 -7
  39. package/src/sender/tcp/tcpsender.ts +51 -33
  40. package/src/sender/websocket/websocketsender.ts +40 -14
@@ -10,7 +10,6 @@ import type {
10
10
  Notification,
11
11
  UnknownCommandResponse,
12
12
  } from '../../types/index.ts'
13
- import { logger } from '../../utils/logger.ts'
14
13
  import { BlipError } from '../bliperror.ts'
15
14
  import { RetryableError } from '../retryableerror.ts'
16
15
  import { ConnectionSender, type ConnectionSenderConstructor, OpenConnectionSender } from '../sender.ts'
@@ -41,21 +40,20 @@ export class TCPSender extends OpenConnectionSender {
41
40
  this.connectionHandle = new TCPHandle<Envelope>(
42
41
  host,
43
42
  443,
44
- () => {
43
+ (socketRef) => {
45
44
  this.sessionNegotiator = new SessionNegotiator(this, (session) => {
46
- this.connectionHandle
47
- .get()
48
- .then((s) => s.write(JSON.stringify(session)))
49
- .catch((err) => logger.warn('TCPSender', 'Failed to write session frame', err))
45
+ socketRef.current.write(JSON.stringify(session))
50
46
  })
51
- const upgradeToTls = tlsOptions ? () => this.connectionHandle.upgradeToTls(tlsOptions) : undefined
47
+ const upgradeToTls = tlsOptions
48
+ ? () => this.connectionHandle.upgradeToTls(tlsOptions, socketRef)
49
+ : undefined
52
50
  return this.sessionNegotiator.negotiate({
53
51
  node: options.node,
54
52
  authentication: auth,
55
53
  upgradeToTls,
56
54
  })
57
55
  },
58
- () => this.envelopeResolver.rejectPendingEnvelopes('Connection was closed'),
56
+ () => this.rejectPending('Connection was closed'),
59
57
  (envelope: Envelope) => {
60
58
  if (this.sessionNegotiator?.negotiating) {
61
59
  return this.sessionNegotiator.handleEnvelope(envelope)
@@ -143,7 +141,7 @@ class TCPHandle<T> {
143
141
  constructor(
144
142
  host: string,
145
143
  port: number,
146
- onConnected: () => Promise<void>,
144
+ onConnected: (socketRef: { current: Socket }) => Promise<void>,
147
145
  onClose: () => void,
148
146
  onMessage: (message: T) => void,
149
147
  ) {
@@ -167,17 +165,19 @@ class TCPHandle<T> {
167
165
  }
168
166
  }
169
167
 
170
- public async upgradeToTls(options: ConnectionOptions): Promise<void> {
171
- if (!this.currentSocketPromise) {
172
- throw new Error('Cannot upgrade: no active socket.')
173
- }
174
- const plain = await this.currentSocketPromise
168
+ public async upgradeToTls(options: ConnectionOptions, socketRef: { current: Socket }): Promise<void> {
169
+ const plain = socketRef.current
175
170
  plain.removeAllListeners('data')
176
171
  this.buffer = Buffer.alloc(0)
177
172
 
178
173
  const { connect: tlsConnect } = await import('node:tls')
179
174
  const secured = tlsConnect({ ...options, socket: plain })
180
175
 
176
+ // Permanent no-op handler attached up front, so errors that arrive in
177
+ // the microtask gap after secureConnect resolves don't become
178
+ // uncaughtException. The underlying TCP socket's 'close' drives reconnect.
179
+ secured.on('error', () => undefined)
180
+
181
181
  await new Promise<void>((resolve, reject) => {
182
182
  const onError = (err: Error) => {
183
183
  secured.off('secureConnect', onConnect)
@@ -192,7 +192,7 @@ class TCPHandle<T> {
192
192
  })
193
193
 
194
194
  this.attachDataListener(secured)
195
- this.currentSocketPromise = Promise.resolve(secured)
195
+ socketRef.current = secured
196
196
  }
197
197
 
198
198
  private attachDataListener(socket: NodeJS.ReadableStream) {
@@ -209,7 +209,7 @@ class TCPHandle<T> {
209
209
  private async connect(
210
210
  host: string,
211
211
  port: number,
212
- onConnected: () => Promise<void>,
212
+ onConnected: (socketRef: { current: Socket }) => Promise<void>,
213
213
  onClose: () => void,
214
214
  ): Promise<Socket> {
215
215
  const { connect } = await import('node:net')
@@ -217,42 +217,60 @@ class TCPHandle<T> {
217
217
  const socket = connect({ host, port }).setKeepAlive(true)
218
218
  this.buffer = Buffer.alloc(0)
219
219
 
220
- await new Promise<void>((resolve) => {
220
+ // Mutable holder so upgradeToTls can swap the socket mid-handshake;
221
+ // the negotiator's sendSession reads .current at write time, so frames
222
+ // always go to the active socket even after the TLS upgrade.
223
+ const socketRef = { current: socket }
224
+
225
+ await new Promise<void>((resolve, reject) => {
226
+ let connected = false
227
+
221
228
  socket.once('connect', () => {
229
+ connected = true
222
230
  resolve()
223
231
  })
224
232
 
225
233
  socket.once('error', (err) => {
226
- if (!this.closing) {
227
- throw err
234
+ if (!connected) {
235
+ reject(err)
228
236
  }
237
+ // Post-connect errors are followed by 'close', which drives reconnect;
238
+ // this listener exists only to prevent uncaughtException.
229
239
  })
230
240
 
231
241
  socket.once('close', () => {
232
- socket.removeAllListeners('data')
242
+ // socketRef.current is the TLS-upgraded socket if upgrade ran; the
243
+ // plain one otherwise. Both cases hold the only live data listener.
244
+ socketRef.current.removeAllListeners('data')
245
+
233
246
  if (!this.closing) {
234
- onClose()
247
+ if (connected) onClose()
235
248
 
236
249
  this.connectionAttempts++
237
- if (this.connectionAttempts < 3) {
238
- this.currentSocketPromise = this.connect(host, port, onConnected, onClose)
239
- } else {
240
- throw new Error('Failed to connect/reconnect to TCP socket')
241
- }
250
+ // Swallow the rejection of the now-orphaned previous promise; any
251
+ // in-flight get() awaiter still sees it via their captured reference.
252
+ const previous = this.currentSocketPromise
253
+ this.currentSocketPromise =
254
+ this.connectionAttempts < 3 ? this.connect(host, port, onConnected, onClose) : null
255
+ previous?.catch(() => undefined)
242
256
  }
257
+
258
+ if (!connected) reject(new Error('Socket closed before connect'))
243
259
  })
244
260
 
245
261
  this.attachDataListener(socket)
246
262
  })
247
263
 
248
- // sendSession callbacks read currentSocketPromise via .get(); resolve it
249
- // before onConnected runs so negotiate() can write the 'new' session frame
250
- // (otherwise we deadlock waiting on the outer connect() promise).
251
- this.currentSocketPromise = Promise.resolve(socket)
252
-
253
- await onConnected()
264
+ try {
265
+ await onConnected(socketRef)
266
+ } catch (err) {
267
+ // Handshake failures (timeout, bad response) leave the socket open
268
+ // otherwise; destroy it so the 'close' handler drives reconnect.
269
+ socketRef.current.destroy()
270
+ throw err
271
+ }
254
272
  this.connectionAttempts = 0
255
273
 
256
- return socket
274
+ return socketRef.current
257
275
  }
258
276
  }
@@ -39,7 +39,7 @@ export class WebSocketSender extends OpenConnectionSender {
39
39
  authentication: options.authentication,
40
40
  })
41
41
  },
42
- () => this.envelopeResolver.rejectPendingEnvelopes('Connection was closed'),
42
+ () => this.rejectPending('Connection was closed'),
43
43
  (envelope: Envelope) => {
44
44
  if (this.sessionNegotiator?.negotiating) {
45
45
  return this.sessionNegotiator.handleEnvelope(envelope)
@@ -139,7 +139,10 @@ class WebSocketHandle<T> {
139
139
  }
140
140
 
141
141
  public get() {
142
- return this.currentWebSocketPromise!
142
+ if (!this.currentWebSocketPromise) {
143
+ throw new Error('WebSocket connection is not available.')
144
+ }
145
+ return this.currentWebSocketPromise
143
146
  }
144
147
 
145
148
  public async close() {
@@ -159,31 +162,54 @@ class WebSocketHandle<T> {
159
162
  const connection = new WebSocket(url, 'lime')
160
163
 
161
164
  await new Promise<void>((resolve, reject) => {
165
+ let connected = false
166
+
162
167
  connection.onopen = () => {
168
+ connected = true
163
169
  resolve()
164
170
  }
171
+ connection.onerror = (err) => {
172
+ if (!connected) {
173
+ reject('message' in err ? new Error(`WebSocket error: ${err.message}`, { cause: err }) : err)
174
+ }
175
+ // Post-open errors are followed by 'close', which drives reconnect.
176
+ }
165
177
  connection.onclose = () => {
166
178
  if (!this.closing) {
167
- this.connectionAttempts++
168
-
169
- if (this.connectionAttempts < 3) {
170
- this.currentWebSocketPromise = this.connect(url, onConnected, onClose, onMessage)
171
- } else {
172
- reject(new Error('Failed to connect to WebSocket'))
173
- }
179
+ if (connected) onClose()
174
180
 
175
- onClose()
181
+ this.connectionAttempts++
182
+ // Swallow the rejection of the now-orphaned previous promise; any
183
+ // in-flight get() awaiter still sees it via their captured reference.
184
+ const previous = this.currentWebSocketPromise
185
+ this.currentWebSocketPromise =
186
+ this.connectionAttempts < 3 ? this.connect(url, onConnected, onClose, onMessage) : null
187
+ previous?.catch(() => undefined)
176
188
  }
189
+
190
+ if (!connected) reject(new Error('WebSocket closed before open'))
177
191
  }
178
192
  connection.onmessage = (event) => {
179
193
  onMessage(JSON.parse(event.data))
180
194
  }
181
- connection.onerror = (err) => {
182
- reject('message' in err ? new Error(`WebSocket error: ${err.message}`, { cause: err }) : err)
183
- }
184
195
  })
185
196
 
186
- await onConnected(connection)
197
+ try {
198
+ await onConnected(connection)
199
+ } catch (err) {
200
+ // Handshake failures leave the socket open otherwise; tear it down so
201
+ // the 'onclose' handler drives reconnect. Prefer ws.terminate() (hard
202
+ // kill, Node only) to avoid waiting on the graceful close handshake
203
+ // with a peer that just failed to negotiate; fall back to close() in
204
+ // browsers where terminate() doesn't exist.
205
+ const terminable = connection as WebSocket & { terminate?: () => void }
206
+ if (terminable.terminate) {
207
+ terminable.terminate()
208
+ } else {
209
+ connection.close()
210
+ }
211
+ throw err
212
+ }
187
213
  this.connectionAttempts = 0
188
214
 
189
215
  return connection