Skip to content
This repository was archived by the owner on Nov 9, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 4 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
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 69.23,
functions: 88.88,
lines: 93.75,
statements: 93.75,
branches: 78.94,
functions: 100,
lines: 96.27,
statements: 96.27,
},
},

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"eslint-plugin-jsdoc": "^36.1.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.3.1",
"extension-port-stream": "^2.0.1",
"jest": "^27.5.1",
"jest-it-up": "^2.0.2",
"json-rpc-engine": "^6.1.0",
Expand All @@ -54,7 +55,8 @@
"rimraf": "^3.0.2",
"ts-jest": "^27.1.4",
"ts-node": "^10.7.0",
"typescript": "^4.2.4"
"typescript": "^4.2.4",
"webextension-polyfill-ts": "^0.26.0"
},
"engines": {
"node": ">=14.0.0"
Expand Down
34 changes: 31 additions & 3 deletions src/createStreamMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,21 @@ interface IdMap {
[requestId: string]: IdMapValue;
}

interface Options {
retryOnMessage?: string;
}

/**
* Creates a JsonRpcEngine middleware with an associated Duplex stream and
* EventEmitter. The middleware, and by extension stream, assume that middleware
* parameters are properly formatted. No runtime type checking or validation is
* performed.
*
* @param options - Configuration options for middleware.
* @returns The event emitter, middleware, and stream.
*/
export default function createStreamMiddleware() {
const idMap: IdMap = {};
export default function createStreamMiddleware(options: Options = {}) {
const idMap: IdMap = {}; // TODO: replace with actual Map
const stream = new Duplex({
objectMode: true,
read: () => undefined,
Expand All @@ -45,13 +50,23 @@ export default function createStreamMiddleware() {
end,
) => {
// write req to stream
stream.push(req);
sendToStream(req);
// register request on id map
idMap[req.id as unknown as string] = { req, res, next, end };
};

return { events, middleware, stream };

/**
* Forwards JSON-RPC request to the stream.
*
* @param req - The JSON-RPC request object.
*/
function sendToStream(req: JsonRpcRequest<unknown>) {
// TODO: limiting retries could be implemented here
stream.push(req);
}

/**
* Writes a JSON-RPC object to the stream.
*
Expand Down Expand Up @@ -104,6 +119,19 @@ export default function createStreamMiddleware() {
* @param notif - The notification to process.
*/
function processNotification(notif: JsonRpcNotification<unknown>) {
if (options?.retryOnMessage && notif.method === options.retryOnMessage) {
retryStuckRequests();
}
events.emit('notification', notif);
}

/**
* Retry pending requests.
*/
function retryStuckRequests() {
Object.values(idMap).forEach(({ req }) => {
// TODO: limiting retries could be implemented here
sendToStream(req);
});
}
}
82 changes: 82 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { Duplex } from 'stream';
import { JsonRpcEngine } from 'json-rpc-engine';
import PortStream from 'extension-port-stream';
import { Runtime } from 'webextension-polyfill-ts';
import { createStreamMiddleware, createEngineStream } from '.';

const artificialDelay = (t = 0) =>
new Promise((resolve) => setTimeout(resolve, t));
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = function (_a: any) {};

const jsonrpc = '2.0' as const;

describe('createStreamMiddleware', () => {
Expand Down Expand Up @@ -98,3 +106,77 @@ describe('middleware and engine to stream', () => {
expect(response).toStrictEqual(res);
});
});

const RECONNECTED = 'CONNECTED';
describe('retry logic in middleware connected to a port', () => {
it('retries requests on reconnect message', async () => {
// create guest
const engineA = new JsonRpcEngine();
const jsonRpcConnection = createStreamMiddleware({
retryOnMessage: RECONNECTED,
});
engineA.push(jsonRpcConnection.middleware);

// create port
let messageConsumer = noop;
const messages: any[] = [];
const extensionPort = {
onMessage: {
addListener: (cb: any) => {
messageConsumer = cb;
},
},
onDisconnect: {
addListener: noop,
},
postMessage(m: any) {
messages.push(m);
},
};

const connectionStream = new PortStream(
extensionPort as unknown as Runtime.Port,
);

// connect both
const clientSideStream = jsonRpcConnection.stream;
clientSideStream
.pipe(connectionStream as unknown as Duplex)
.pipe(clientSideStream);

// request and expected result
const req1 = { id: 1, jsonrpc, method: 'test' };
const req2 = { id: 2, jsonrpc, method: 'test' };
const res = { id: 1, jsonrpc, result: 'test' };

// Initially sent once
const responsePromise1 = engineA.handle(req1);
engineA.handle(req2);
await artificialDelay();

expect(messages).toHaveLength(2);

// Reconnected, gets sent again
messageConsumer({
method: RECONNECTED,
});
await artificialDelay();

expect(messages).toHaveLength(4);
expect(messages[0]).toBe(messages[2]);
expect(messages[1]).toBe(messages[3]);

messageConsumer(res);

expect(await responsePromise1).toStrictEqual(res);

// Handled messages don't get retried but unhandled still do

messageConsumer({
method: RECONNECTED,
});
await artificialDelay();

expect(messages).toHaveLength(5);
});
});
31 changes: 31 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2113,6 +2113,13 @@ extend@~3.0.2:
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==

extension-port-stream@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extension-port-stream/-/extension-port-stream-2.0.1.tgz#d374820c581418c2275d3c4439ade0b82c4cfac6"
integrity sha512-ltrv4Dh/979I04+D4Te6TFygfRSOc5EBzzlHRldWMS8v73V80qWluxH88hqF0qyUsBXTb8NmzlmSipcre6a+rg==
dependencies:
webextension-polyfill-ts "^0.22.0"

extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
Expand Down Expand Up @@ -4692,6 +4699,30 @@ walker@^1.0.7:
dependencies:
makeerror "1.0.12"

webextension-polyfill-ts@^0.22.0:
version "0.22.0"
resolved "https://registry.yarnpkg.com/webextension-polyfill-ts/-/webextension-polyfill-ts-0.22.0.tgz#86cfd7bab4d9d779d98c8340983f4b691b2343f3"
integrity sha512-3P33ClMwZ/qiAT7UH1ROrkRC1KM78umlnPpRhdC/292UyoTTW9NcjJEqDsv83HbibcTB6qCtpVeuB2q2/oniHQ==
dependencies:
webextension-polyfill "^0.7.0"

webextension-polyfill-ts@^0.26.0:
version "0.26.0"
resolved "https://registry.yarnpkg.com/webextension-polyfill-ts/-/webextension-polyfill-ts-0.26.0.tgz#80b7063ddaf99abaa1ca73aad0cec09f306612d3"
integrity sha512-XEFL+aYVEsm/d4RajVwP75g56c/w2aSHnPwgtUv8/nCzbLNSzRQIix6aj1xqFkA5yr7OIDkk3OD/QTnPp8ThYA==
dependencies:
webextension-polyfill "^0.8.0"

webextension-polyfill@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.7.0.tgz#0df1120ff0266056319ce1a622b09ad8d4a56505"
integrity sha512-su48BkMLxqzTTvPSE1eWxKToPS2Tv5DLGxKexLEVpwFd6Po6N8hhSLIvG6acPAg7qERoEaDL+Y5HQJeJeml5Aw==

webextension-polyfill@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.8.0.tgz#f80e9f4b7f81820c420abd6ffbebfa838c60e041"
integrity sha512-a19+DzlT6Kp9/UI+mF9XQopeZ+n2ussjhxHJ4/pmIGge9ijCDz7Gn93mNnjpZAk95T4Tae8iHZ6sSf869txqiQ==

webidl-conversions@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
Expand Down