Skip to content
This repository was archived by the owner on Nov 9, 2023. It is now read-only.

Commit cff34a1

Browse files
authored
retry support (#27)
1 parent 8c64407 commit cff34a1

File tree

5 files changed

+151
-8
lines changed

5 files changed

+151
-8
lines changed

jest.config.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ module.exports = {
2121
// An object that configures minimum threshold enforcement for coverage results
2222
coverageThreshold: {
2323
global: {
24-
branches: 69.23,
25-
functions: 88.88,
26-
lines: 93.75,
27-
statements: 93.75,
24+
branches: 78.94,
25+
functions: 100,
26+
lines: 96.27,
27+
statements: 96.27,
2828
},
2929
},
3030

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"eslint-plugin-jsdoc": "^36.1.0",
4747
"eslint-plugin-node": "^11.1.0",
4848
"eslint-plugin-prettier": "^3.3.1",
49+
"extension-port-stream": "^2.0.1",
4950
"jest": "^27.5.1",
5051
"jest-it-up": "^2.0.2",
5152
"json-rpc-engine": "^6.1.0",
@@ -54,7 +55,8 @@
5455
"rimraf": "^3.0.2",
5556
"ts-jest": "^27.1.4",
5657
"ts-node": "^10.7.0",
57-
"typescript": "^4.2.4"
58+
"typescript": "^4.2.4",
59+
"webextension-polyfill-ts": "^0.26.0"
5860
},
5961
"engines": {
6062
"node": ">=14.0.0"

src/createStreamMiddleware.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,21 @@ interface IdMap {
2020
[requestId: string]: IdMapValue;
2121
}
2222

23+
interface Options {
24+
retryOnMessage?: string;
25+
}
26+
2327
/**
2428
* Creates a JsonRpcEngine middleware with an associated Duplex stream and
2529
* EventEmitter. The middleware, and by extension stream, assume that middleware
2630
* parameters are properly formatted. No runtime type checking or validation is
2731
* performed.
2832
*
33+
* @param options - Configuration options for middleware.
2934
* @returns The event emitter, middleware, and stream.
3035
*/
31-
export default function createStreamMiddleware() {
32-
const idMap: IdMap = {};
36+
export default function createStreamMiddleware(options: Options = {}) {
37+
const idMap: IdMap = {}; // TODO: replace with actual Map
3338
const stream = new Duplex({
3439
objectMode: true,
3540
read: () => undefined,
@@ -45,13 +50,23 @@ export default function createStreamMiddleware() {
4550
end,
4651
) => {
4752
// write req to stream
48-
stream.push(req);
53+
sendToStream(req);
4954
// register request on id map
5055
idMap[req.id as unknown as string] = { req, res, next, end };
5156
};
5257

5358
return { events, middleware, stream };
5459

60+
/**
61+
* Forwards JSON-RPC request to the stream.
62+
*
63+
* @param req - The JSON-RPC request object.
64+
*/
65+
function sendToStream(req: JsonRpcRequest<unknown>) {
66+
// TODO: limiting retries could be implemented here
67+
stream.push(req);
68+
}
69+
5570
/**
5671
* Writes a JSON-RPC object to the stream.
5772
*
@@ -104,6 +119,19 @@ export default function createStreamMiddleware() {
104119
* @param notif - The notification to process.
105120
*/
106121
function processNotification(notif: JsonRpcNotification<unknown>) {
122+
if (options?.retryOnMessage && notif.method === options.retryOnMessage) {
123+
retryStuckRequests();
124+
}
107125
events.emit('notification', notif);
108126
}
127+
128+
/**
129+
* Retry pending requests.
130+
*/
131+
function retryStuckRequests() {
132+
Object.values(idMap).forEach(({ req }) => {
133+
// TODO: limiting retries could be implemented here
134+
sendToStream(req);
135+
});
136+
}
109137
}

src/index.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
import { Duplex } from 'stream';
12
import { JsonRpcEngine } from 'json-rpc-engine';
3+
import PortStream from 'extension-port-stream';
4+
import type { Runtime } from 'webextension-polyfill-ts';
25
import { createStreamMiddleware, createEngineStream } from '.';
36

7+
const artificialDelay = (t = 0) =>
8+
new Promise((resolve) => setTimeout(resolve, t));
9+
// eslint-disable-next-line @typescript-eslint/no-empty-function
10+
const noop = function (_a: any) {};
11+
412
const jsonrpc = '2.0' as const;
513

614
describe('createStreamMiddleware', () => {
@@ -98,3 +106,77 @@ describe('middleware and engine to stream', () => {
98106
expect(response).toStrictEqual(res);
99107
});
100108
});
109+
110+
const RECONNECTED = 'CONNECTED';
111+
describe('retry logic in middleware connected to a port', () => {
112+
it('retries requests on reconnect message', async () => {
113+
// create guest
114+
const engineA = new JsonRpcEngine();
115+
const jsonRpcConnection = createStreamMiddleware({
116+
retryOnMessage: RECONNECTED,
117+
});
118+
engineA.push(jsonRpcConnection.middleware);
119+
120+
// create port
121+
let messageConsumer = noop;
122+
const messages: any[] = [];
123+
const extensionPort = {
124+
onMessage: {
125+
addListener: (cb: any) => {
126+
messageConsumer = cb;
127+
},
128+
},
129+
onDisconnect: {
130+
addListener: noop,
131+
},
132+
postMessage(m: any) {
133+
messages.push(m);
134+
},
135+
};
136+
137+
const connectionStream = new PortStream(
138+
extensionPort as unknown as Runtime.Port,
139+
);
140+
141+
// connect both
142+
const clientSideStream = jsonRpcConnection.stream;
143+
clientSideStream
144+
.pipe(connectionStream as unknown as Duplex)
145+
.pipe(clientSideStream);
146+
147+
// request and expected result
148+
const req1 = { id: 1, jsonrpc, method: 'test' };
149+
const req2 = { id: 2, jsonrpc, method: 'test' };
150+
const res = { id: 1, jsonrpc, result: 'test' };
151+
152+
// Initially sent once
153+
const responsePromise1 = engineA.handle(req1);
154+
engineA.handle(req2);
155+
await artificialDelay();
156+
157+
expect(messages).toHaveLength(2);
158+
159+
// Reconnected, gets sent again
160+
messageConsumer({
161+
method: RECONNECTED,
162+
});
163+
await artificialDelay();
164+
165+
expect(messages).toHaveLength(4);
166+
expect(messages[0]).toBe(messages[2]);
167+
expect(messages[1]).toBe(messages[3]);
168+
169+
messageConsumer(res);
170+
171+
expect(await responsePromise1).toStrictEqual(res);
172+
173+
// Handled messages don't get retried but unhandled still do
174+
175+
messageConsumer({
176+
method: RECONNECTED,
177+
});
178+
await artificialDelay();
179+
180+
expect(messages).toHaveLength(5);
181+
});
182+
});

yarn.lock

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2113,6 +2113,13 @@ extend@~3.0.2:
21132113
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
21142114
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
21152115

2116+
extension-port-stream@^2.0.1:
2117+
version "2.0.1"
2118+
resolved "https://registry.yarnpkg.com/extension-port-stream/-/extension-port-stream-2.0.1.tgz#d374820c581418c2275d3c4439ade0b82c4cfac6"
2119+
integrity sha512-ltrv4Dh/979I04+D4Te6TFygfRSOc5EBzzlHRldWMS8v73V80qWluxH88hqF0qyUsBXTb8NmzlmSipcre6a+rg==
2120+
dependencies:
2121+
webextension-polyfill-ts "^0.22.0"
2122+
21162123
21172124
version "1.3.0"
21182125
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@@ -4692,6 +4699,30 @@ walker@^1.0.7:
46924699
dependencies:
46934700
makeerror "1.0.12"
46944701

4702+
webextension-polyfill-ts@^0.22.0:
4703+
version "0.22.0"
4704+
resolved "https://registry.yarnpkg.com/webextension-polyfill-ts/-/webextension-polyfill-ts-0.22.0.tgz#86cfd7bab4d9d779d98c8340983f4b691b2343f3"
4705+
integrity sha512-3P33ClMwZ/qiAT7UH1ROrkRC1KM78umlnPpRhdC/292UyoTTW9NcjJEqDsv83HbibcTB6qCtpVeuB2q2/oniHQ==
4706+
dependencies:
4707+
webextension-polyfill "^0.7.0"
4708+
4709+
webextension-polyfill-ts@^0.26.0:
4710+
version "0.26.0"
4711+
resolved "https://registry.yarnpkg.com/webextension-polyfill-ts/-/webextension-polyfill-ts-0.26.0.tgz#80b7063ddaf99abaa1ca73aad0cec09f306612d3"
4712+
integrity sha512-XEFL+aYVEsm/d4RajVwP75g56c/w2aSHnPwgtUv8/nCzbLNSzRQIix6aj1xqFkA5yr7OIDkk3OD/QTnPp8ThYA==
4713+
dependencies:
4714+
webextension-polyfill "^0.8.0"
4715+
4716+
webextension-polyfill@^0.7.0:
4717+
version "0.7.0"
4718+
resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.7.0.tgz#0df1120ff0266056319ce1a622b09ad8d4a56505"
4719+
integrity sha512-su48BkMLxqzTTvPSE1eWxKToPS2Tv5DLGxKexLEVpwFd6Po6N8hhSLIvG6acPAg7qERoEaDL+Y5HQJeJeml5Aw==
4720+
4721+
webextension-polyfill@^0.8.0:
4722+
version "0.8.0"
4723+
resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.8.0.tgz#f80e9f4b7f81820c420abd6ffbebfa838c60e041"
4724+
integrity sha512-a19+DzlT6Kp9/UI+mF9XQopeZ+n2ussjhxHJ4/pmIGge9ijCDz7Gn93mNnjpZAk95T4Tae8iHZ6sSf869txqiQ==
4725+
46954726
webidl-conversions@^5.0.0:
46964727
version "5.0.0"
46974728
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"

0 commit comments

Comments
 (0)