Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions shared/modules/caip-stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ class MockStream extends Duplex {
}
}

const createMockPort = () => {
// Minimal chrome.runtime.Port‐like stub
const listeners: (() => void)[] = [];
return {
onDisconnect: {
addListener: (fn: () => void) => listeners.push(fn),
// helper used by tests
_trigger: () => listeners.forEach((fn) => fn()),
},
};
};

describe('CAIP Stream', () => {
describe('createCaipStream', () => {
it('pipes and unwraps a caip-348 message from source stream to the substream', async () => {
Expand Down Expand Up @@ -92,5 +104,26 @@ describe('CAIP Stream', () => {

await expect(promise).resolves.toBe(undefined);
});

it('does not emit an error on “Premature close” after graceful shutdown', async () => {
const sourceStream = new MockStream();
const mockPort = createMockPort();
(sourceStream as unknown as { _port: typeof mockPort })._port = mockPort;

const providerStream = createCaipStream(sourceStream);

const errorSpy = jest.fn();
providerStream.on('error', errorSpy);

// Trigger disconnect
(
mockPort as unknown as { onDisconnect: { _trigger: () => void } }
).onDisconnect._trigger();

// Give the event loop a tick to propagate potential errors
await new Promise((r) => setImmediate(r));

expect(errorSpy).not.toHaveBeenCalled();
});
});
});
30 changes: 28 additions & 2 deletions shared/modules/caip-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,35 @@ export class CaipStream extends Duplex {
export const createCaipStream = (portStream: Duplex): Duplex => {
const caipStream = new CaipStream();

pipeline(portStream, caipStream, portStream, (err: Error) => {
/** Cleanly end the CAIP side if the port goes away. */
const handlePortGone = () => {
// End only once
if (
!caipStream.substream.destroyed &&
!caipStream.substream.writableEnded
) {
caipStream.substream.end();
}
};

// 1. Listen for tab/iframe shutdown signals
// a. Node-style streams emit 'close' and/or 'end'
portStream.once?.('close', handlePortGone);
portStream.once?.('end', handlePortGone);

// b. chrome.runtime.Port exposes onDisconnect
// (ExtensionPortStream exposes the raw Port at `._port`)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rawPort: chrome.runtime.Port | undefined = (portStream as any)?._port;
rawPort?.onDisconnect?.addListener(handlePortGone);

// 2. Wire up the full duplex pipeline
pipeline(portStream, caipStream, portStream, (err: Error | null) => {
caipStream.substream.destroy();
console.log('MetaMask CAIP stream', err);

if (err && err.message !== 'Premature close') {
console.error('MetaMask CAIP stream error:', err);
}
});

return caipStream.substream;
Expand Down
Loading