Skip to content

Commit 3721531

Browse files
committed
added ability to resurrect connections after service worker deactivation
1 parent 38e54d6 commit 3721531

8 files changed

+154
-15
lines changed

build/bgscript.js

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@ var _CustomEventTarget = _interopRequireDefault(require("./CustomEventTarget.js"
1919
var _Connection = require("./Connection.js");
2020
var _Errors = require("./Errors.js");
2121
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
22+
const STORED_CONNECTIONS_KEY = "bgscript.connections";
23+
2224
/**
2325
* Class that will handle all the content scripts that will connect to the background script.
24-
*
25-
* @property {Map<string, Connection>} scriptConnections A Map that will relate every script ID to its Connection object.
26-
* @property {object} exposedData The properties and methods exposed to the connecting scripts.
27-
* @property {function} errorCallback A callback that gets fired whenever there is an error in the script. It will get passed some details about the error.
2826
*/
2927
class BackgroundHandler extends _CustomEventTarget.default {
3028
/**
@@ -33,24 +31,53 @@ class BackgroundHandler extends _CustomEventTarget.default {
3331
* @param {object} exposedData An object containing all properties and methods to be exposed to the content scripts
3432
* @param {object} options An object that will customize how this class works.
3533
*/
36-
constructor(exposedData = {}, options = {
37-
runtime: chrome.runtime
38-
}) {
39-
var _options$errorCallbac, _options$runtime;
34+
constructor(exposedData = {}, options = {}) {
35+
var _options$errorCallbac, _options$runtime, _options$storage, _options$chromeTabs;
4036
super();
37+
38+
/** @type {Map<string, Connection>} scriptConnections A Map that will relate every script ID to its Connection object. */
4139
this.scriptConnections = new Map(); // script-id --> connection
40+
/** @property {object} exposedData The properties and methods exposed to the connecting scripts. */
4241
this.exposedData = exposedData;
42+
/** @property {function} errorCallback A callback that gets fired whenever there is an error in the script. It will get passed some details about the error. */
4343
this.errorCallback = (_options$errorCallbac = options.errorCallback) !== null && _options$errorCallbac !== void 0 ? _options$errorCallbac : null;
44+
45+
/** @property {chrome.runtime} runtime The runtime that will be used to create the connections. */
4446
this.runtime = (_options$runtime = options.runtime) !== null && _options$runtime !== void 0 ? _options$runtime : chrome.runtime;
47+
this.storage = (_options$storage = options.storage) !== null && _options$storage !== void 0 ? _options$storage : chrome.storage;
48+
this.chromeTabs = (_options$chromeTabs = options.chromeTabs) !== null && _options$chromeTabs !== void 0 ? _options$chromeTabs : chrome.tabs;
4549
this.runtime.onConnect.addListener(port => this.handleNewConnection(port));
50+
this.restoreConnections();
51+
}
52+
async restoreConnections() {
53+
let data = await this.storage.local.get(STORED_CONNECTIONS_KEY);
54+
if (data[STORED_CONNECTIONS_KEY]) {
55+
let connections = data[STORED_CONNECTIONS_KEY];
56+
for (let [scriptId, tab] of connections) {
57+
this.chromeTabs.sendMessage(tab, {
58+
type: _Connection.CONNECTION_PREFIX + "ping",
59+
scriptId
60+
});
61+
}
62+
}
63+
}
64+
async saveConnections() {
65+
let conns = [];
66+
for (let [scriptId, _] of this.scriptConnections) {
67+
let [name, tabId] = scriptId.split("-");
68+
conns.push([name, parseInt(tabId)]);
69+
}
70+
await this.storage.local.set({
71+
[STORED_CONNECTIONS_KEY]: conns
72+
});
4673
}
4774

4875
/**
4976
* Handle a new incoming connection
5077
*
5178
* @param {chrome.runtime.Port} port The newly created connection to a content script
5279
*/
53-
handleNewConnection(port) {
80+
async handleNewConnection(port) {
5481
var _port$sender$tab$id, _port$sender;
5582
if (!this.isInternalConnection(port)) return;
5683
let [name, scriptId] = this.parsePortName(port);
@@ -73,6 +100,7 @@ class BackgroundHandler extends _CustomEventTarget.default {
73100
// see BackgroundScript.js:68
74101
connection.addListener("disconnect", () => this.disconnectScript(name, tabId));
75102
this.scriptConnections.set(scriptId, connection);
103+
await this.saveConnections();
76104

77105
// Fire the connection event
78106
this.fireEvent("connectionreceived", {
@@ -176,6 +204,25 @@ class BackgroundHandler extends _CustomEventTarget.default {
176204
return proxy;
177205
}
178206

207+
/**
208+
* Get all tab ids where a specific scriptId is present.
209+
*
210+
* @param {string} scriptId
211+
* @returns {number[]} The tab IDs
212+
*/
213+
getScriptTabs(scriptId) {
214+
let tabs = [];
215+
for (let [id, connection] of this.scriptConnections) {
216+
if (id.startsWith(scriptId)) {
217+
var _connection$port$send;
218+
if ((_connection$port$send = connection.port.sender) !== null && _connection$port$send !== void 0 && (_connection$port$send = _connection$port$send.tab) !== null && _connection$port$send !== void 0 && _connection$port$send.id) {
219+
tabs.push(connection.port.sender.tab.id);
220+
}
221+
}
222+
}
223+
return tabs;
224+
}
225+
179226
/**
180227
* Check if a script with a specific id associated to a specific tab has made a connection to the background page.
181228
*
@@ -251,6 +298,7 @@ class BackgroundScript extends _CustomEventTarget.default {
251298
this.context = (_options$context = options.context) !== null && _options$context !== void 0 ? _options$context : "content";
252299
this.runtime = (_options$runtime = options.runtime) !== null && _options$runtime !== void 0 ? _options$runtime : chrome.runtime;
253300
this.connectBackgroundScript();
301+
this.checkForReconnection();
254302
}
255303

256304
/**
@@ -311,6 +359,20 @@ class BackgroundScript extends _CustomEventTarget.default {
311359
return proxy;
312360
}
313361

362+
/**
363+
* Check if the background script is pinging us
364+
*/
365+
checkForReconnection() {
366+
this.runtime.onMessage.addListener(async (req, sender, sendResponse) => {
367+
if (this.connection != null) return;
368+
if (req.type == _Connection.CONNECTION_PREFIX + "ping") {
369+
if (req.scriptId == this.scriptId) {
370+
this.connectBackgroundScript();
371+
}
372+
}
373+
});
374+
}
375+
314376
/**
315377
* Function that returns a uuid version 4 formatted string.
316378
*
@@ -474,7 +536,7 @@ class Connection extends _CustomEventTarget.default {
474536
}
475537

476538
/**
477-
* Decides how to answer based on the incomin message type.
539+
* Decides how to answer based on the incoming message type.
478540
*
479541
* @param {Object} message The incoming message
480542
*/

src/BackgroundHandler.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import CustomEventTarget from './CustomEventTarget.js';
22
import { Connection, CONNECTION_PREFIX, CONNECTION_PREFIX_NOTAB } from './Connection.js';
33
import { BgHandlerErrors as ERRORS, Error } from './Errors.js';
44

5+
const STORED_CONNECTIONS_KEY = "bgscript.connections";
6+
57
/**
68
* Class that will handle all the content scripts that will connect to the background script.
79
*/
@@ -13,7 +15,7 @@ class BackgroundHandler extends CustomEventTarget {
1315
* @param {object} exposedData An object containing all properties and methods to be exposed to the content scripts
1416
* @param {object} options An object that will customize how this class works.
1517
*/
16-
constructor(exposedData = {}, options = { runtime: chrome.runtime }) {
18+
constructor(exposedData = {}, options = {}) {
1719
super();
1820

1921
/** @type {Map<string, Connection>} scriptConnections A Map that will relate every script ID to its Connection object. */
@@ -26,15 +28,45 @@ class BackgroundHandler extends CustomEventTarget {
2628
/** @property {chrome.runtime} runtime The runtime that will be used to create the connections. */
2729
this.runtime = options.runtime ?? chrome.runtime;
2830

31+
this.storage = options.storage ?? chrome.storage;
32+
this.chromeTabs = options.chromeTabs ?? chrome.tabs;
33+
2934
this.runtime.onConnect.addListener( (port) => this.handleNewConnection(port) );
35+
36+
this.restoreConnections();
37+
}
38+
39+
async restoreConnections() {
40+
let data = await this.storage.local.get(STORED_CONNECTIONS_KEY);
41+
42+
if (data[STORED_CONNECTIONS_KEY]) {
43+
let connections = data[STORED_CONNECTIONS_KEY];
44+
45+
for (let [scriptId, tab] of connections) {
46+
this.chromeTabs.sendMessage(tab, {
47+
type: CONNECTION_PREFIX + "ping",
48+
scriptId
49+
});
50+
}
51+
}
52+
}
53+
54+
async saveConnections() {
55+
let conns = [];
56+
for (let [scriptId, _] of this.scriptConnections) {
57+
let [name, tabId] = scriptId.split("-");
58+
conns.push([name, parseInt(tabId)]);
59+
}
60+
61+
await this.storage.local.set({ [STORED_CONNECTIONS_KEY]: conns });
3062
}
3163

3264
/**
3365
* Handle a new incoming connection
3466
*
3567
* @param {chrome.runtime.Port} port The newly created connection to a content script
3668
*/
37-
handleNewConnection(port) {
69+
async handleNewConnection(port) {
3870

3971
if (!this.isInternalConnection(port)) return;
4072

@@ -58,6 +90,7 @@ class BackgroundHandler extends CustomEventTarget {
5890
connection.addListener("disconnect", () => this.disconnectScript(name, tabId) );
5991

6092
this.scriptConnections.set(scriptId, connection);
93+
await this.saveConnections();
6194

6295
// Fire the connection event
6396
this.fireEvent("connectionreceived", {

src/BackgroundScript.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class BackgroundScript extends CustomEventTarget {
3232
this.runtime = options.runtime ?? chrome.runtime;
3333

3434
this.connectBackgroundScript();
35+
36+
this.checkForReconnection();
3537
}
3638

3739
/**
@@ -103,6 +105,21 @@ class BackgroundScript extends CustomEventTarget {
103105
let proxy = await this.connection.getProxy();
104106
return proxy;
105107
}
108+
109+
/**
110+
* Check if the background script is pinging us
111+
*/
112+
checkForReconnection() {
113+
this.runtime.onMessage.addListener(async (req, sender, sendResponse) => {
114+
if (this.connection != null) return;
115+
116+
if (req.type == CONNECTION_PREFIX + "ping") {
117+
if (req.scriptId == this.scriptId) {
118+
this.connectBackgroundScript();
119+
}
120+
}
121+
});
122+
}
106123

107124
/**
108125
* Function that returns a uuid version 4 formatted string.

src/Connection.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export class Connection extends CustomEventTarget {
146146
}
147147

148148
/**
149-
* Decides how to answer based on the incomin message type.
149+
* Decides how to answer based on the incoming message type.
150150
*
151151
* @param {Object} message The incoming message
152152
*/

tests/Integrations.test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,22 @@ import { MockedWindow } from "./mocks/mockedWindow.js";
55
import BackgroundHandler from "../src/BackgroundHandler.js";
66
import BackgroundScript from "../src/BackgroundScript.js";
77
import { waitFor, setupScripts } from "./utilities.js";
8+
import { MockedChromeStorage } from "./mocks/mockedChromeStorage.js";
89

910
global.window = new MockedWindow();
1011

1112
describe("The Background Handler", () => {
1213
test("should be constructed correctly", () => {
1314
let runtime = new MockedChromeRuntime();
15+
let storage = new MockedChromeStorage();
16+
let chromeTabs = {};
1417

1518
const foo = () => {};
1619
const exposed = {
1720
foo,
1821
prop: 1
1922
};
20-
let bgHandler = new BackgroundHandler(exposed, { runtime });
23+
let bgHandler = new BackgroundHandler(exposed, { runtime, storage, chromeTabs });
2124

2225
assert.deepEqual(bgHandler.exposedData, { foo, prop: 1});
2326
assert.equal(bgHandler.scriptConnections.size, 0);

tests/mocks/mockedChromeRuntime.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { MockedEvent } from "./mockedEvent.js";
44
export class MockedChromeRuntime {
55
constructor(fixedTabId = null) {
66
this.onConnect = new MockedEvent();
7+
this.onMessage = new MockedEvent();
78
this.fixedTabId = fixedTabId;
89
}
910

tests/mocks/mockedChromeStorage.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
class Storage {
2+
data = new Map();
3+
4+
async get(keys) {
5+
let result = new Map();
6+
for (let key of keys) {
7+
result.set(key, this.data.get(key));
8+
}
9+
return result;
10+
}
11+
12+
async set(entries) {
13+
for (let [key, value] of Object.entries(entries)) {
14+
this.data.set(key, value);
15+
}
16+
}
17+
}
18+
19+
export class MockedChromeStorage {
20+
local = new Storage();
21+
}

tests/utilities.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import BackgroundHandler from "../src/BackgroundHandler.js";
22
import BackgroundScript from "../src/BackgroundScript.js";
33
import { MockedChromeRuntime } from "./mocks/mockedChromeRuntime.js";
4+
import { MockedChromeStorage } from "./mocks/mockedChromeStorage.js";
45

56
export function waitFor(ms) {
67
return new Promise((resolve, _) => {
@@ -10,7 +11,8 @@ export function waitFor(ms) {
1011

1112
export function setupScripts(handlerData, scriptsData, fixedTabId = null) {
1213
let runtime = new MockedChromeRuntime(fixedTabId);
13-
let bgHandler = new BackgroundHandler(handlerData, { runtime });
14+
let storage = new MockedChromeStorage();
15+
let bgHandler = new BackgroundHandler(handlerData, { runtime, storage, chromeTabs: {} });
1416

1517
let scripts = [];
1618
for (let data of scriptsData) {

0 commit comments

Comments
 (0)