UNPKG

@canboat/canboatjs

Version:

Native javascript version of canboat

703 lines 28.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = require("events"); const maretron_ipg_1 = require("./maretron-ipg"); // 260 bytes: i mod 256 for i = 0..259. function buildPayload260Hex() { const out = []; for (let i = 0; i < 260; i++) { out.push((i & 0xff).toString(16).padStart(2, '0')); } return out.join(''); } const VECTORS = [ // PGN 127488 (Engine Parameters, Rapid Update) from SA 0x0F, priority 2, // PDU2 broadcast, single frame. { name: 'rx-pgn127488-pdu2-singleframe', direction: 'rx', wireHex: 'a5a3f2000f0800a8160000 00ffff', decoded: { pgn: 127488, src: 0x0f, dst: 0xff, priority: 2, dp: 1, edp: 0, msg_type: 1, payload_length: 8, payload_hex: '00a8160000 00ffff' } }, // PGN 59904 (ISO Request) PDU1 directed to SA 0x23 from SA 0x05. { name: 'rx-pgn59904-pdu1-singleframe', direction: 'rx', wireHex: 'a5e2ea2305030 0ee01', decoded: { pgn: 59904, src: 5, dst: 0x23, priority: 6, dp: 0, edp: 0, msg_type: 1, payload_length: 3, payload_hex: '00ee01' } }, // PGN 60928 (ISO Address Claim) PDU1 broadcast (dst=0xFF) from SA 0x29. { name: 'rx-pgn60928-pdu1-broadcast', direction: 'rx', wireHex: 'a5e2eeff290801020304 05060708', decoded: { pgn: 60928, src: 41, dst: 0xff, priority: 6, dp: 0, edp: 0, msg_type: 1, payload_length: 8, payload_hex: '0102030405060708' } }, // PGN 126996 (Product Information) reassembled fast-packet envelope. { name: 'rx-pgn126996-pdu2-fastpacket', direction: 'rx', wireHex: 'a5e5f014294001020304050607 08090a0b0c0d0e0f10111213141516171819 1a1b1c1d1e1f202122232425262728292a 2b2c2d2e2f303132333435363738393a3b 3c3d3e3f40', decoded: { pgn: 126996, src: 41, dst: 0xff, priority: 6, dp: 1, edp: 0, msg_type: 2, payload_length: 64, payload_hex: '0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40' } }, // PGN 130820 with msg_type=3 and a 260-byte payload — the only case // that exercises the 16-bit length encoding (LL=0x04 LH=0x01 = 260). { name: 'rx-pgn130820-pdu2-transport', direction: 'rx', wireHex: 'a5e7ff04290401' + buildPayload260Hex(), decoded: { pgn: 130820, src: 41, dst: 0xff, priority: 6, dp: 1, edp: 0, msg_type: 3, payload_length: 260, payload_hex: buildPayload260Hex() } }, // TX: ISO Request with SA=0xFF sentinel. { name: 'tx-pgn59904-iso-request-sa0xff', direction: 'tx', wireHex: 'a5e2ea23ff0300ee01', decoded: { pgn: 59904, src: 0xff, dst: 0x23, priority: 6, dp: 0, edp: 0, msg_type: 1, payload_length: 3, payload_hex: '00ee01' } }, // TX: PGN 126720 — PDU1 (PF=0xEF, just below the PDU2 boundary), so // PS carries the destination SA. msg_type=2 fast packet. { name: 'tx-pgn126720-fastpacket', direction: 'tx', wireHex: 'a5b5ef14ff108924038000006400000000000000001f', decoded: { pgn: 126720, src: 0xff, dst: 0x14, priority: 3, dp: 1, edp: 0, msg_type: 2, payload_length: 16, payload_hex: '8924038000006400000000000000001f' } } ]; function hexStringToBuffer(hex) { return Buffer.from(hex.replace(/\s+/g, ''), 'hex'); } describe('Maretron IPG wire format — vectors', () => { for (const v of VECTORS) { const wire = hexStringToBuffer(v.wireHex); if (v.direction === 'rx') { test(`${v.name} — parseMaretronFrame decodes correctly`, () => { const result = (0, maretron_ipg_1.parseMaretronFrame)(wire, 0); expect(result.invalid).toBeFalsy(); expect(result.consumed).toBe(wire.length); const frame = result.frame; expect(frame.pgn).toBe(v.decoded.pgn); expect(frame.src).toBe(v.decoded.src); expect(frame.dst).toBe(v.decoded.dst); expect(frame.priority).toBe(v.decoded.priority); expect(frame.dp).toBe(v.decoded.dp); expect(frame.edp).toBe(v.decoded.edp); expect(frame.msg_type).toBe(v.decoded.msg_type); expect(frame.payload_length).toBe(v.decoded.payload_length); expect(frame.payload.toString('hex')).toBe(v.decoded.payload_hex.replace(/\s+/g, '')); }); } else { test(`${v.name} — buildMaretronFrame produces exact wire bytes`, () => { const payload = hexStringToBuffer(v.decoded.payload_hex); const built = (0, maretron_ipg_1.buildMaretronFrame)({ pgn: v.decoded.pgn, src: v.decoded.src, dst: v.decoded.dst, priority: v.decoded.priority, msg_type: v.decoded.msg_type, edp: v.decoded.edp, payload }); expect(built.toString('hex')).toBe(v.wireHex.replace(/\s+/g, '').toLowerCase()); }); // Feeding the built bytes back through the parser must yield the // exact same `decoded` fields. Ensures parse/build are inverses. test(`${v.name} — parse(build(decoded)) round-trips`, () => { const payload = hexStringToBuffer(v.decoded.payload_hex); const built = (0, maretron_ipg_1.buildMaretronFrame)({ pgn: v.decoded.pgn, src: v.decoded.src, dst: v.decoded.dst, priority: v.decoded.priority, msg_type: v.decoded.msg_type, edp: v.decoded.edp, payload }); const reparsed = (0, maretron_ipg_1.parseMaretronFrame)(built, 0).frame; expect(reparsed.pgn).toBe(v.decoded.pgn); expect(reparsed.src).toBe(v.decoded.src); expect(reparsed.dst).toBe(v.decoded.dst); expect(reparsed.priority).toBe(v.decoded.priority); expect(reparsed.dp).toBe(v.decoded.dp); expect(reparsed.msg_type).toBe(v.decoded.msg_type); expect(reparsed.payload_length).toBe(v.decoded.payload_length); }); } } }); // --------------------------------------------------------------------------- // Parser edge cases // --------------------------------------------------------------------------- describe('parseMaretronFrame — incremental input', () => { // A PGN 127488 PDU2 frame, used as the seed for partial-input cases. const VECTOR_01 = Buffer.from('a5a3f2000f0800a8160000 00ffff'.replace(/\s+/g, ''), 'hex'); test('returns consumed=0 (not invalid) when fewer than 6 bytes are present', () => { for (let i = 0; i < 6; i++) { const slice = VECTOR_01.subarray(0, i); const r = (0, maretron_ipg_1.parseMaretronFrame)(slice, 0); expect(r.consumed).toBe(0); expect(r.frame).toBeUndefined(); expect(r.invalid).toBeFalsy(); } }); test('returns consumed=0 when payload is incomplete', () => { // 6-byte header advertises 8-byte payload but we only have 6+4 bytes. const slice = VECTOR_01.subarray(0, 10); const r = (0, maretron_ipg_1.parseMaretronFrame)(slice, 0); expect(r.consumed).toBe(0); expect(r.frame).toBeUndefined(); }); test('rejects frames with F1 sync bit unset as invalid', () => { const bad = Buffer.from(VECTOR_01); bad[1] = bad[1] & 0x7f; // clear sync bit const r = (0, maretron_ipg_1.parseMaretronFrame)(bad, 0); expect(r.invalid).toBe(true); }); test('rejects frames not starting with 0xA5 as invalid', () => { const bad = Buffer.from(VECTOR_01); bad[0] = 0x00; const r = (0, maretron_ipg_1.parseMaretronFrame)(bad, 0); expect(r.invalid).toBe(true); }); test('msg_type=3 needs 7 bytes of header before length is known', () => { // Construct a minimal msg_type=3 frame with a 1-byte payload. const built = (0, maretron_ipg_1.buildMaretronFrame)({ pgn: 130820, src: 41, priority: 6, msg_type: 3, payload: Buffer.from([0xaa]) }); expect((0, maretron_ipg_1.parseMaretronFrame)(built.subarray(0, 6), 0).consumed).toBe(0); expect((0, maretron_ipg_1.parseMaretronFrame)(built, 0).frame?.payload[0]).toBe(0xaa); }); test('PDU2 broadcast: PS becomes PGN low byte, dst=0xFF regardless of input dst', () => { const built = (0, maretron_ipg_1.buildMaretronFrame)({ pgn: 127488, src: 0x0f, dst: 0x42, // ignored because PDU2 priority: 2, msg_type: 1, payload: Buffer.alloc(8) }); expect(built[3]).toBe(0x00); // PS = PGN low byte (0x1F200 & 0xFF) expect((0, maretron_ipg_1.parseMaretronFrame)(built, 0).frame?.dst).toBe(0xff); }); test('PDU1 directed: PS carries dst, dst is preserved through round-trip', () => { const built = (0, maretron_ipg_1.buildMaretronFrame)({ pgn: 59904, src: 0xff, dst: 0x23, priority: 6, msg_type: 1, payload: Buffer.from([0x00, 0xee, 0x01]) }); expect(built[3]).toBe(0x23); expect((0, maretron_ipg_1.parseMaretronFrame)(built, 0).frame?.dst).toBe(0x23); }); test('rejects payloads > 255 bytes when msg_type != 3', () => { expect(() => (0, maretron_ipg_1.buildMaretronFrame)({ pgn: 127488, msg_type: 1, payload: Buffer.alloc(256) })).toThrow(/Transport Protocol/); }); test('handles back-to-back frames in a single buffer', () => { const a = (0, maretron_ipg_1.buildMaretronFrame)({ pgn: 127488, src: 0x0f, priority: 2, msg_type: 1, payload: Buffer.from('00a816000000ffff', 'hex') }); const b = (0, maretron_ipg_1.buildMaretronFrame)({ pgn: 60928, src: 0x29, priority: 6, msg_type: 1, payload: Buffer.from('0102030405060708', 'hex') }); const both = Buffer.concat([a, b]); const r1 = (0, maretron_ipg_1.parseMaretronFrame)(both, 0); expect(r1.frame?.pgn).toBe(127488); const r2 = (0, maretron_ipg_1.parseMaretronFrame)(both, r1.consumed); expect(r2.frame?.pgn).toBe(60928); expect(r1.consumed + r2.consumed).toBe(both.length); }); }); // --------------------------------------------------------------------------- // Handshake helpers // --------------------------------------------------------------------------- describe('handshake helpers', () => { test('buildConnectMessage wraps the password in double quotes and NUL-terminates', () => { const buf = (0, maretron_ipg_1.buildConnectMessage)(''); expect(buf.toString('utf8')).toBe('CONNECT\t""\t\tMOBILE\0'); expect(buf[buf.length - 1]).toBe(0); }); test('buildConnectMessage carries the supplied password verbatim inside the quotes', () => { expect((0, maretron_ipg_1.buildConnectMessage)('hunter2').toString('utf8')).toBe('CONNECT\t"hunter2"\t\tMOBILE\0'); }); test('buildConnectMessage rejects passwords with handshake-breaking characters', () => { for (const bad of [ 'has"quote', 'has\ttab', 'has\0nul', 'has\rcr', 'has\nlf' ]) { expect(() => (0, maretron_ipg_1.buildConnectMessage)(bad)).toThrow(/quotes, tabs, or NUL/); } }); test('SET_MODE_BINARY matches the documented wire bytes', () => { expect(maretron_ipg_1.SET_MODE_BINARY.toString('utf8')).toBe('SET_MODE\tBINARY\0'); }); }); // --------------------------------------------------------------------------- // Stream wiring — drive the parser via a fake socket (no real TCP) // --------------------------------------------------------------------------- class FakeSocket extends events_1.EventEmitter { written = []; // eslint-disable-next-line @typescript-eslint/no-unused-vars write(data) { if (typeof data === 'string') { this.written.push(Buffer.from(data, 'utf8')); } else { this.written.push(Buffer.from(data)); } return true; } end() { this.emit('close'); } destroy() { this.emit('close'); } } describe('MaretronIPGStream — handshake & frame routing', () => { test('sends CONNECT on connect, SET_MODE BINARY on CONNECTED, then emits parsed frames', () => { const fake = new FakeSocket(); const stream = (0, maretron_ipg_1.MaretronIPGStream)({ host: 'fakehost', port: 6543, password: '', reconnect: false, _socketFactory: () => fake }); const frames = []; stream.on('n2kFrame', (f) => frames.push(f)); // 1. Fake socket connects. fake.emit('connect'); expect(fake.written.length).toBe(1); expect(fake.written[0].toString('utf8')).toBe('CONNECT\t""\t\tMOBILE\0'); // 2. Daemon streams the four handshake replies in order. fake.emit('data', Buffer.from('SERVER_VERSION\t4.2.0.1\tIPG100\0' + 'INSTANCE_DATA\t41\t1\0' + 'LICENSES_USED\t1\t1\t1\t1\0', 'utf8')); // Still awaiting CONNECTED — no SET_MODE yet. expect(fake.written.length).toBe(1); fake.emit('data', Buffer.from('CONNECTED\t12345\0', 'utf8')); expect(fake.written.length).toBe(2); expect(fake.written[1].toString('utf8')).toBe('SET_MODE\tBINARY\0'); expect(stream.state).toBe('streaming'); expect(stream.ipgBusAddress).toBe(41); expect(stream.deviceSerial).toBe('12345'); // 3. Inject a binary frame (Vector 01's bytes). fake.emit('data', Buffer.from('a5a3f2000f0800a816000000ffff', 'hex')); expect(frames.length).toBe(1); expect(frames[0].pgn).toBe(127488); expect(frames[0].src).toBe(0x0f); expect(frames[0].priority).toBe(2); }); test('sendPGN writes a 0xA5-framed buffer to the socket when streaming', () => { const fake = new FakeSocket(); const stream = (0, maretron_ipg_1.MaretronIPGStream)({ host: 'fakehost', reconnect: false, _socketFactory: () => fake }); fake.emit('connect'); fake.emit('data', Buffer.from('CONNECTED\t1\0', 'utf8')); fake.written.length = 0; // sendPGN with a minimal handcrafted PGN — toPgn will pad/format it. stream.sendPGN({ pgn: 59904, prio: 6, src: 0, dst: 0x23, fields: { PGN: 126464 } }); expect(fake.written.length).toBeGreaterThan(0); const wire = fake.written[fake.written.length - 1]; expect(wire[0]).toBe(0xa5); // PF = byte 2 = 0xEA, PS = byte 3 = 0x23 (PDU1 directed) expect(wire[2]).toBe(0xea); expect(wire[3]).toBe(0x23); // SA = byte 4 = 0xFF (always 0xFF on TX — IPG substitutes its claim) expect(wire[4]).toBe(0xff); }); test('sendString accepts canboat plain CSV and frames it', () => { const fake = new FakeSocket(); const stream = (0, maretron_ipg_1.MaretronIPGStream)({ host: 'fakehost', reconnect: false, _socketFactory: () => fake }); fake.emit('connect'); fake.emit('data', Buffer.from('CONNECTED\t1\0', 'utf8')); fake.written.length = 0; stream.sendString('2026-05-13-10:00:00.000,6,59904,5,35,3,00,ee,01'); expect(fake.written.length).toBe(1); const wire = fake.written[0]; expect(wire.toString('hex')).toBe('a5e2ea23ff0300ee01'); }); test('drops outbound PGNs before handshake completes', () => { const fake = new FakeSocket(); const stream = (0, maretron_ipg_1.MaretronIPGStream)({ host: 'fakehost', reconnect: false, _socketFactory: () => fake }); fake.emit('connect'); // not yet CONNECTED fake.written.length = 0; stream.sendPGN({ pgn: 59904, prio: 6, dst: 0x23, src: 0, fields: {} }); expect(fake.written.length).toBe(0); }); test('reassembles frames split across multiple data chunks', () => { const fake = new FakeSocket(); const stream = (0, maretron_ipg_1.MaretronIPGStream)({ host: 'fakehost', reconnect: false, _socketFactory: () => fake }); const frames = []; stream.on('n2kFrame', (f) => frames.push(f)); fake.emit('connect'); fake.emit('data', Buffer.from('CONNECTED\t1\0', 'utf8')); const whole = Buffer.from('a5a3f2000f0800a816000000ffff', 'hex'); // Split arbitrarily — first chunk doesn't even contain a full header. fake.emit('data', whole.subarray(0, 3)); expect(frames.length).toBe(0); fake.emit('data', whole.subarray(3, 8)); expect(frames.length).toBe(0); fake.emit('data', whole.subarray(8)); expect(frames.length).toBe(1); expect(frames[0].pgn).toBe(127488); }); test('resyncs past a junk high-bit byte preceding a valid 0xA5 frame', () => { const fake = new FakeSocket(); const stream = (0, maretron_ipg_1.MaretronIPGStream)({ host: 'fakehost', reconnect: false, _socketFactory: () => fake }); const frames = []; stream.on('n2kFrame', (f) => frames.push(f)); fake.emit('connect'); fake.emit('data', Buffer.from('CONNECTED\t1\0', 'utf8')); // 0x90 has the high bit set so it can't be parsed as an ASCII text // frame, and isn't 0xA5 so it can't start a binary frame either. // Real-world cause: brief desync after the SET_MODE BINARY toggle. // The driver must skip exactly one byte and find the real frame. const good = Buffer.from('a5a3f2000f0800a816000000ffff', 'hex'); const junk = Buffer.from([0x90]); fake.emit('data', Buffer.concat([junk, good])); expect(frames.length).toBe(1); expect(frames[0].pgn).toBe(127488); }); test('NO authentication reply emits authfail and ends the socket', () => { const fake = new FakeSocket(); const stream = (0, maretron_ipg_1.MaretronIPGStream)({ host: 'fakehost', reconnect: false, _socketFactory: () => fake }); let authfailed = false; stream.on('authfail', () => (authfailed = true)); fake.emit('connect'); fake.emit('data', Buffer.from('NO\0', 'utf8')); expect(authfailed).toBe(true); }); test('NO authentication failure does not schedule a reconnect or emit a fail-fast error', () => { jest.useFakeTimers(); let factoryCalls = 0; const sockets = []; const factory = () => { factoryCalls += 1; const s = new FakeSocket(); sockets.push(s); return s; }; const stream = (0, maretron_ipg_1.MaretronIPGStream)({ host: 'fakehost', reconnect: true, // would otherwise schedule a retry on close _socketFactory: factory }); const errors = []; stream.on('error', (e) => errors.push(e)); let authfailed = false; stream.on('authfail', () => (authfailed = true)); sockets[0].emit('connect'); sockets[0].emit('data', Buffer.from('NO\0', 'utf8')); // FakeSocket.end() synchronously emits 'close', which would normally // run the reconnect / fail-fast paths. expect(authfailed).toBe(true); expect(stream.reconnectTimer).toBeNull(); expect(errors.length).toBe(0); jest.advanceTimersByTime(60_000); expect(factoryCalls).toBe(1); jest.useRealTimers(); }); }); // --------------------------------------------------------------------------- // Reconnect semantics — fail fast on initial connect (standalone), // retry after success, with exponential backoff // --------------------------------------------------------------------------- describe('MaretronIPGStream — reconnect policy', () => { test('initial connection failure emits stream error and does not schedule a retry', () => { jest.useFakeTimers(); const fake = new FakeSocket(); const stream = (0, maretron_ipg_1.MaretronIPGStream)({ host: 'fakehost', reconnect: true, _socketFactory: () => fake }); const errors = []; stream.on('error', (e) => errors.push(e)); // Simulate ECONNREFUSED with no prior 'connect' event. fake.emit('error', Object.assign(new Error('ECONNREFUSED'), { code: 'ECONNREFUSED' })); fake.emit('close'); expect(errors.length).toBe(1); expect(errors[0].message).toBe('ECONNREFUSED'); expect(stream.reconnectTimer).toBeNull(); // Advancing time should not trigger a new socket factory call. jest.advanceTimersByTime(60_000); expect(stream.reconnectTimer).toBeNull(); jest.useRealTimers(); }); test('post-handshake socket close schedules a reconnect with the timer ref-d', () => { jest.useFakeTimers(); let factoryCalls = 0; const sockets = []; const factory = () => { factoryCalls += 1; const s = new FakeSocket(); sockets.push(s); return s; }; const stream = (0, maretron_ipg_1.MaretronIPGStream)({ host: 'fakehost', reconnect: true, reconnectInitialMs: 5000, _socketFactory: factory }); expect(factoryCalls).toBe(1); // Complete the handshake on socket 0. sockets[0].emit('connect'); sockets[0].emit('data', Buffer.from('CONNECTED\t1\0', 'utf8')); expect(stream.hasEverConnected).toBe(true); // Socket drops mid-session. sockets[0].emit('close'); expect(stream.reconnectTimer).not.toBeNull(); // Critically: the timer must NOT be unref'd. We verify by checking // hasRef() — if the implementation regresses to unref(), this fails. expect(stream.reconnectTimer.hasRef()).toBe(true); // Advance time to fire the reconnect. jest.advanceTimersByTime(5000); expect(factoryCalls).toBe(2); jest.useRealTimers(); }); test('SignalK-mode initial failure (app provided) retries instead of emitting error', () => { jest.useFakeTimers(); let factoryCalls = 0; const sockets = []; const factory = () => { factoryCalls += 1; const s = new FakeSocket(); sockets.push(s); return s; }; const app = new events_1.EventEmitter(); app.setProviderError = jest.fn(); app.setProviderStatus = jest.fn(); const stream = (0, maretron_ipg_1.MaretronIPGStream)({ app, providerId: 'maretron', host: 'fakehost', reconnect: true, reconnectInitialMs: 5000, _socketFactory: factory }); const errors = []; stream.on('error', (e) => errors.push(e)); // Initial connect fails — no 'connect' event ever fired. sockets[0].emit('error', Object.assign(new Error('ECONNREFUSED'), { code: 'ECONNREFUSED' })); sockets[0].emit('close'); // No stream-level error — SignalK keeps trying. expect(errors.length).toBe(0); expect(stream.reconnectTimer).not.toBeNull(); expect(app.setProviderError).toHaveBeenCalled(); // Advance time; retry should fire and call the factory again. jest.advanceTimersByTime(5000); expect(factoryCalls).toBe(2); jest.useRealTimers(); }); test('failFastOnInitialConnect:true override forces fail-fast even with an app', () => { jest.useFakeTimers(); const fake = new FakeSocket(); const app = new events_1.EventEmitter(); app.setProviderError = jest.fn(); const stream = (0, maretron_ipg_1.MaretronIPGStream)({ app, providerId: 'maretron', failFastOnInitialConnect: true, reconnect: true, _socketFactory: () => fake }); const errors = []; stream.on('error', (e) => errors.push(e)); fake.emit('error', Object.assign(new Error('ECONNREFUSED'), { code: 'ECONNREFUSED' })); fake.emit('close'); expect(errors.length).toBe(1); expect(stream.reconnectTimer).toBeNull(); jest.useRealTimers(); }); test('initial failure without an error listener logs to stderr instead of throwing', () => { const fake = new FakeSocket(); const consoleErr = jest.spyOn(console, 'error').mockImplementation(() => { }); // No stream.on('error') listener — emit('error') would otherwise crash. (0, maretron_ipg_1.MaretronIPGStream)({ host: 'fakehost', reconnect: true, _socketFactory: () => fake }); expect(() => { fake.emit('error', Object.assign(new Error('ENOTFOUND'), { code: 'ENOTFOUND' })); fake.emit('close'); }).not.toThrow(); expect(consoleErr).toHaveBeenCalled(); consoleErr.mockRestore(); }); test('reconnect delay doubles on each failure, caps at reconnectMaxMs, resets on CONNECTED', () => { jest.useFakeTimers(); const sockets = []; const factory = () => { const s = new FakeSocket(); sockets.push(s); return s; }; const app = new events_1.EventEmitter(); app.setProviderError = () => { }; app.setProviderStatus = () => { }; const stream = (0, maretron_ipg_1.MaretronIPGStream)({ app, providerId: 'maretron', reconnect: true, reconnectInitialMs: 100, reconnectMaxMs: 800, _socketFactory: factory }); // First socket constructed in the constructor. expect(stream.reconnectDelayMs).toBe(100); // Walk through six failed attempts: 100, 200, 400, 800, 800, 800. const expected = [100, 200, 400, 800, 800, 800]; for (let i = 0; i < expected.length; i++) { const delay = expected[i]; sockets[i].emit('error', Object.assign(new Error('flap'), { code: 'ECONNREFUSED' })); sockets[i].emit('close'); // Next scheduled delay is doubled, capped. expect(stream.reconnectDelayMs).toBe(Math.min(delay * 2, 800)); jest.advanceTimersByTime(delay); expect(sockets.length).toBe(i + 2); } // Successful handshake on the next socket resets the delay. sockets[sockets.length - 1].emit('connect'); sockets[sockets.length - 1].emit('data', Buffer.from('CONNECTED\t1\0', 'utf8')); expect(stream.reconnectDelayMs).toBe(100); // A subsequent close starts the backoff cycle over from initial. const lastIndex = sockets.length - 1; sockets[lastIndex].emit('close'); expect(stream.reconnectDelayMs).toBe(200); jest.advanceTimersByTime(100); expect(sockets.length).toBe(lastIndex + 2); jest.useRealTimers(); }); }); //# sourceMappingURL=maretron-ipg.test.js.map