Skip to content

Commit

Permalink
Refactor to support other workers in the future
Browse files Browse the repository at this point in the history
  • Loading branch information
bryfox committed Jul 26, 2018
1 parent a1d6c2f commit 0a11d1a
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 52 deletions.
8 changes: 4 additions & 4 deletions src/containers/Node.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { makeGetNodeColor } from '../selectors/protocol';
import { Node as UINode } from '../ui/components';

import WorkerAgent from '../utils/WorkerAgent';
import { Node as UINode } from '../ui/components';
import { getNetwork, getNodeLabelFunction } from '../selectors/interface';
import { getNodeLabelWorkerUrl, makeGetNodeColor } from '../selectors/protocol';

const getNodeColor = makeGetNodeColor();

Expand Down Expand Up @@ -73,8 +73,8 @@ function mapStateToProps(state, props) {
return {
color: getNodeColor(state, props),
getLabel: getNodeLabelFunction(state),
workerUrl: state.protocol.workerUrl,
workerNetwork: (state.protocol.workerUrl && getNetwork(state)) || null,
workerUrl: getNodeLabelWorkerUrl(state),
workerNetwork: (getNodeLabelWorkerUrl(state) && getNetwork(state)) || null,
};
}

Expand Down
13 changes: 1 addition & 12 deletions src/ducks/modules/__tests__/protocol.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,13 @@
import { ActionsObservable } from 'redux-observable';
import { omit } from 'lodash';

import reducer, { actionCreators, epics } from '../protocol';
import reducer, { actionCreators, epics, initialState } from '../protocol';
import { actionTypes as SessionActionTypes } from '../session';
import environments from '../../../utils/environments';
import { getEnvironment } from '../../../utils/Environment';

jest.mock('../../../utils/protocol/index');

const initialState = {
isLoaded: false,
isLoading: false,
error: null,
name: '',
version: '',
required: '',
type: 'factory',
stages: [],
};

describe('protocol module', () => {
describe('reducer', () => {
it('should return the initial state', () => {
Expand Down
39 changes: 25 additions & 14 deletions src/ducks/modules/protocol.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { combineEpics } from 'redux-observable';
import { Observable } from 'rxjs';
import { loadWorker, loadProtocol, importProtocol, downloadProtocol, loadFactoryProtocol } from '../../utils/protocol';
import { actionTypes as SessionActionTypes } from './session';
import {
loadProtocol,
importProtocol,
downloadProtocol,
loadFactoryProtocol,
preloadWorkers,
} from '../../utils/protocol';

import { supportedWorkers } from '../../utils/WorkerAgent';

/**
* `protocol` maintains information about the currently-loaded protocol for session, and
Expand Down Expand Up @@ -32,7 +40,7 @@ const LOAD_PROTOCOL_FAILED = Symbol('LOAD_PROTOCOL_FAILED');
const SET_PROTOCOL = 'SET_PROTOCOL';
const SET_WORKER = 'SET_WORKER';

const initialState = {
export const initialState = {
isLoaded: false,
isLoading: false,
error: null,
Expand All @@ -41,7 +49,7 @@ const initialState = {
required: '',
stages: [],
type: 'factory',
workerUrl: undefined,
workerUrlMap: null,
};

export default function reducer(state = initialState, action = {}) {
Expand All @@ -57,7 +65,7 @@ export default function reducer(state = initialState, action = {}) {
case SET_WORKER:
return {
...state,
workerUrl: action.workerUrl,
workerUrlMap: action.workerUrlMap,
};
case END_SESSION:
return initialState;
Expand Down Expand Up @@ -148,11 +156,11 @@ function setProtocol(path, protocol, isFactoryProtocol) {
};
}

// If there's no custom worker, the set false so we won't expect one later
function setWorkerContent(workerUrl = false) {
// If there's no custom worker, set to empty so we won't expect one later
function setWorkerContent(workerUrlMap = {}) {
return {
type: SET_WORKER,
workerUrl,
workerUrlMap,
};
}

Expand Down Expand Up @@ -199,13 +207,16 @@ const loadProtocolWorkerEpic = action$ =>
.ofType(LOAD_PROTOCOL)
.switchMap(action => // Favour subsequent load actions over earlier ones
Observable
.fromPromise(
loadWorker(
action.path,
'nodeLabelWorker',
action.protocolType === 'factory',
))
.map(workerUrl => setWorkerContent(workerUrl)),
.fromPromise(preloadWorkers(action.path, action.protocolType === 'factory'))
.mergeMap(urls => urls)
.reduce((urlMap, workerUrl, i) => {
if (workerUrl) {
// eslint-disable-next-line no-param-reassign
urlMap[supportedWorkers[i]] = workerUrl;
}
return urlMap;
}, {})
.map(workerUrlMap => setWorkerContent(workerUrlMap)),
);

const actionCreators = {
Expand Down
9 changes: 9 additions & 0 deletions src/selectors/protocol.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import crypto from 'crypto';
import { createSelector } from 'reselect';

import { createDeepEqualSelector } from './utils';
import { NodeLabelWorkerName } from '../utils/WorkerAgent';

/**
* The remote protocol ID on any instance of Server is the hex-encoded sha256 of its [unique] name.
Expand Down Expand Up @@ -45,3 +47,10 @@ export const makeGetEdgeColor = () => createDeepEqualSelector(
return edgeInfo && edgeInfo[edgeType] && edgeInfo[edgeType].color;
},
);

export const getNodeLabelWorkerUrl = createSelector(
// null if URLs haven't yet loaded; false if worker does not exist
state => state.protocol.workerUrlMap &&
(state.protocol.workerUrlMap[NodeLabelWorkerName] || false),
url => url,
);
12 changes: 10 additions & 2 deletions src/utils/WorkerAgent.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import uuidv4 from './uuid';

export const NodeLabelWorkerName = 'nodeLabelWorker';
export const supportedWorkers = [NodeLabelWorkerName];

// Create an object URL from worker contents.
// We own the URL and can release it when no longer needed.
export const urlForWorkerSource = blob => URL.createObjectURL(blob);

const workers = {};

// Maintain one worker per source URL
const getSharedWorker = (url) => {
if (!workers[url]) {
const worker = new Worker(url);
Expand All @@ -22,6 +30,7 @@ const getSharedWorker = (url) => {
delete worker.workMap[evt.data.messageId];
};
workers[url] = worker;
URL.revokeObjectURL(url);
}
return workers[url];
};
Expand All @@ -32,7 +41,6 @@ const getSharedWorker = (url) => {
*/
class WorkerAgent {
constructor(url) {
this.workerUrl = url;
try {
this.worker = getSharedWorker(url);
} catch (e) {
Expand Down Expand Up @@ -71,7 +79,7 @@ class WorkerAgent {
*/
sendMessageAsync(msg) {
if (!this.worker) {
return Promise.reject(new Error(`Worker unavailable at ${this.workerUrl}`));
return Promise.reject(new Error('Worker unavailable'));
}
const messageId = uuidv4();
const taggedMsg = { ...msg, messageId };
Expand Down
4 changes: 2 additions & 2 deletions src/utils/protocol/__mocks__/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
const loadProtocol = () => Promise.resolve({ fake: { protocol: { json: true } } });
const loadWorker = () => Promise.resolve('blob:http://localhost/abc');
const preloadWorkers = () => Promise.resolve('blob:http://localhost/abc');
const importProtocol = () => Promise.resolve('/app/data/protocol/path');
const downloadProtocol = () => Promise.resolve('/downloaded/protocol/to/temp/path');
const loadFactoryProtocol = () =>
Promise.resolve({ fake: { factory: { protocol: { json: true } } } });

export {
loadProtocol,
loadWorker,
preloadWorkers,
importProtocol,
downloadProtocol,
loadFactoryProtocol,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/protocol/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export { default as protocolPath } from './protocolPath';
export { default as factoryProtocolPath } from './factoryProtocolPath';
export { default as loadProtocol } from './loadProtocol';
export { default as loadFactoryProtocol } from './loadFactoryProtocol';
export { default as loadWorker } from './loadWorker';
export { default as importProtocol } from './importProtocol';
export { default as downloadProtocol } from './downloadProtocol';
export { default as preloadWorkers } from './preloadWorkers';
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import inEnvironment from '../Environment';
import factoryProtocolPath from './factoryProtocolPath';
import protocolPath from './protocolPath';

const supportedWorkers = ['nodeLabelWorker'];
import { urlForWorkerSource, supportedWorkers } from '../WorkerAgent';

/**
* Builds source code for a Web Worker based on the protocol's
Expand Down Expand Up @@ -61,25 +61,28 @@ const compileWorker = (src, funcName) => {
/* eslint-enable */
};

const loadWorker = (environment) => {
/**
* preloadWorkers
* @description Read custom worker scripts from the protocol package, if any.
* By preloading any existing, we can bootstrap before protocol.json is parsed.
*/
const preloadWorkers = (environment) => {
if (environment !== environments.WEB) {
return (protocolName, workerName, isFactory) =>
readFile((isFactory ? factoryProtocolPath : protocolPath)(protocolName, `${workerName}.js`))
/**
* Load from blob so that script inherits CSP
*/
.then(buf => new TextDecoder().decode(buf))
.then(str => compileWorker(str, workerName))
.then(source => new Blob([source], { type: 'text/plain' }))
// FIXME: need to release (revokeObjectURL) (make each consumer responsible
// for releasing after worker created, instead of caching?)
.then(blob => URL.createObjectURL(blob))
// TODO: could try to use a shared worker... (but see above about URL)
// .then(url => new Worker(url))
.catch(err => console.warn(err)); // eslint-disable-line no-console
return (protocolName, isFactory) =>
Promise.all(supportedWorkers.map(workerName =>
readFile((isFactory ? factoryProtocolPath : protocolPath)(protocolName, `${workerName}.js`))
/**
* Load from blob so that script inherits CSP
*/
.then(buf => new TextDecoder().decode(buf))
.then(str => compileWorker(str, workerName))
.then(source => new Blob([source], { type: 'text/plain' }))
.then(blob => urlForWorkerSource(blob))
.catch(() => {})),
);
}

return () => Promise.reject(new Error('loadProtocol() not supported on this platform'));
};

export default inEnvironment(loadWorker);
export default inEnvironment(preloadWorkers);

0 comments on commit 0a11d1a

Please sign in to comment.