Skip to content

Commit 59cf984

Browse files
authored
Worker Support (#22)
* proof of concept use paralleljs for inline workers * use Paralleljs and reintroduce workers to SiteMap * automatic babel polyfill support in worker libs * document worker deatils in README; SiteMap feature tweak * workerize parsing of time series data * comments and commented code cleanup
1 parent 190024d commit 59cf984

30 files changed

+1194
-882
lines changed

README.md

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,21 +101,57 @@ If you have added or modified third-party dependencies then it is important to v
101101
102102
Run `rm -rf node_modules && npm install` and re-run the app to validate a fresh install. This mimics how other apps importing `portal-core-components` will see your changes.
103103
104-
### Worker Caveats
104+
### Using Workers in Components
105105
106-
This library does support workers in its current build process using `worker-loader`. To create a worker
107-
name any worker file `*.worker.js`.
106+
This library supports parallel processing using web workers for components by using [Parallel.js](https://parallel.js.org/).
108107
109-
Note, however, that a bug in `react-app-rewired` can mean lint errors in workers may silently break
110-
production builds but not development builds. See [here](https://github.com/timarney/react-app-rewired/issues/362) for details.
108+
To see how workers are currently in use in this library, see `src/lib_components/workers`. Example:
111109
112-
If you have added or modified a worker file and are seeing empty production builds then manually look for
113-
and fix any lint errors using this command (from the root directory):
110+
```
111+
import Parallel from 'paralleljs';
112+
113+
export default function myWorker(argument) {
114+
const worker = new Parallel(argument);
115+
return worker.spawn((inData) => {
116+
/* do processing to generate outData */
117+
return outData;
118+
});
119+
}
120+
```
121+
122+
A worker like this could then be imported elsewhere and used with promise-stype syntax. Example:
114123
115124
```
116-
npx eslint ./PATH_TO_YOUR_WORKER_FILE -c ./node_modules/eslint-config-react-app/index.js
125+
import myWorker from 'path/to/workers/myWorker.js';
126+
127+
myWorker.then((result) => {
128+
/* do stuff with result */
129+
});
117130
```
118131
132+
Critical rules for this worker pattern:
133+
134+
* **Only put worker files in `src/lib_components/workers`.**
135+
* **Only define one worker function per worker file.**
136+
* **A worker file should only import Parallel and nothing else.**
137+
Any logic inside of `worker.spawn` will have no access to external definitions, even if defined in the worker file.
138+
139+
And most important: **Always test the lib export!**
140+
141+
How a worker runs when developing core-components locally and how it runs when pulled in through the
142+
lib export in another app are *very* different. The former uses run-time webpack and the latter uses
143+
babel only, along with whatever bundling toolchain is in use by the other app.
144+
145+
If you have developed a worker but find it is not working when pulled in as a lib export, run a clean
146+
lib build and then inspect the transpiled worker file in `lib/workers`. Look for any babel polyfill
147+
definitions that appear outside of `worker.spawn` but are used inside. This is the most common reason
148+
a worker fails to perform when pulled in through core components as an app dependency.
149+
150+
The lib build for core components includes a step to migrate any babel polyfills (other than the one
151+
for `import` used to import Parallel.js) directly into the worker logic. This migration is not perfect.
152+
If it missed something that it should have caught please update `lif-fix-worker-babel.js` to suit.
153+
154+
119155
## Modifying Existing Components
120156
121157
Have nodejs.

config-overrides.js

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,7 @@ const svgLoadRule = {
1616
]
1717
};
1818

19-
const workerLoadRule = {
20-
test: /\.worker\.js$/,
21-
use: {
22-
loader: 'worker-loader',
23-
options: {
24-
filename: '[name].[contenthash].worker.js',
25-
chunkFilename: '[id].[contenthash].worker.js',
26-
},
27-
},
28-
}
29-
3019
module.exports = override(
3120
useEslintRc(path.resolve(__dirname, '.eslintrc')),
3221
addWebpackModuleRule(svgLoadRule),
33-
addWebpackModuleRule(workerLoadRule),
3422
);

lib-fix-worker-babel.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict';
2+
3+
const path = require('path');
4+
const fs = require('fs');
5+
6+
/**
7+
Fix Worker Babel
8+
9+
The lib export runs everything through babel, even the workers. But the transpiled internal logic
10+
of a given worker will fail if it uses polyfills injected by babel at the top of the worker file
11+
(outside of worker.spawn(...)).
12+
13+
This script runs as part of the post-babel cleanup of building the lib export. It edits any
14+
workers that have babel polyfills by moving the polyfill definition lines directly into the
15+
worker's internal body. This allows us to build workers with ES* syntax and trust that they
16+
will work as expected when transpiled into the lib export.
17+
*/
18+
19+
console.log('Adjusting babel polyfills for workers...');
20+
21+
const workers = fs.readdirSync(path.resolve(__dirname, 'lib/workers/'));
22+
workers.forEach((worker) => {
23+
// Only look at .js files
24+
if (!/\.js$/.test(worker)) { return; }
25+
const uri = path.resolve(__dirname, 'lib/workers/', worker);
26+
const inSource = fs.readFileSync(uri, 'utf8');
27+
let workerEntered = false;
28+
const preLines = [];
29+
const polyfillLines = ['', ' // Babel polyfills for worker logic'];
30+
const postLines = [''];
31+
// Go through the file one line at a time
32+
inSource.split('\n').forEach((line) => {
33+
// Store babel polyfills OTHER THAN _interopRequireDefault
34+
if (/^function _[a-zA-Z0-9]+\(.*\) \{.*\}$/.test(line) && !line.includes('interopRequireDefault')) {
35+
polyfillLines.push(` ${line}`);
36+
return;
37+
}
38+
// Store lines before entering the worker and note when we do enter it
39+
if (!workerEntered) {
40+
preLines.push(line);
41+
if (/^\s*return [_a-zA-Z0-9]+\.spawn\(function \(.*\) \{$/.test(line)) {
42+
workerEntered = true;
43+
}
44+
return;
45+
}
46+
// Store lines after entering the worker
47+
postLines.push(line);
48+
});
49+
// Final sanity checks
50+
if (!workerEntered) {
51+
console.log(`* ${worker} - SKIPPED - no worker entrypoint found (not good! go fix it!)`);
52+
return;
53+
}
54+
if (polyfillLines.length === 2) {
55+
console.log(`* ${worker} - SKIPPED - no polyfills in need of moving`);
56+
return;
57+
}
58+
// Write the updated file back out to lib
59+
const outSource = [...preLines, ...polyfillLines, ...postLines].join('\n').replace(/[\n]{2,}/g, '\n\n');
60+
fs.writeFileSync(uri, outSource);
61+
console.log(`* ${worker} - UPDATED - ${polyfillLines.length - 2} polyfill(s) moved`);
62+
});
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export function parseLocationHierarchy(inHierarchy: any, parent?: any): {};
21
export function fetchDomainHierarchy(domain: any): Promise<any>;
3-
export function fetchLocations(locations: any): Promise<any>;
2+
export function fetchSingleLocationREST(location: any): Promise<any>;
3+
export function fetchManyLocationsGraphQL(locations: any): Promise<any>;

lib/components/SiteMap/FetchLocationUtils.js

Lines changed: 41 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Object.defineProperty(exports, "__esModule", {
44
value: true
55
});
6-
exports.fetchLocations = exports.fetchDomainHierarchy = exports.parseLocationHierarchy = void 0;
6+
exports.fetchManyLocationsGraphQL = exports.fetchSingleLocationREST = exports.fetchDomainHierarchy = void 0;
77

88
var _rxjs = require("rxjs");
99

@@ -13,43 +13,11 @@ var _NeonApi = _interopRequireDefault(require("../NeonApi/NeonApi"));
1313

1414
var _NeonGraphQL = _interopRequireDefault(require("../NeonGraphQL/NeonGraphQL"));
1515

16-
var _SiteMapWorkerSafeUtils = require("./SiteMapWorkerSafeUtils");
16+
var _parseDomainHierarchy = _interopRequireDefault(require("../../workers/parseDomainHierarchy"));
1717

18-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
19-
20-
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
21-
22-
/**
23-
Recursive function to parse a deeply nest hierarchy object into a flat key/value object
24-
where keys are location names and values are objects containing only those location attributes
25-
the hierarchy affords us (type, description, and parent)
26-
*/
27-
var parseLocationHierarchy = function parseLocationHierarchy(inHierarchy) {
28-
var parent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
29-
var outHierarchy = {};
30-
var name = inHierarchy.locationParentHierarchy ? null : inHierarchy.locationName;
31-
var description = inHierarchy.locationDescription || null;
32-
var type = inHierarchy.locationType || null;
33-
34-
if (description.includes('Not Used')) {
35-
return outHierarchy;
36-
}
37-
38-
if (name !== null) {
39-
outHierarchy[name] = {
40-
type: type,
41-
description: description,
42-
parent: parent
43-
};
44-
}
45-
46-
inHierarchy.locationChildHierarchy.forEach(function (subLocation) {
47-
outHierarchy = _extends(_extends({}, outHierarchy), parseLocationHierarchy(subLocation, name));
48-
});
49-
return outHierarchy;
50-
};
18+
var _parseLocationsArray = _interopRequireDefault(require("../../workers/parseLocationsArray"));
5119

52-
exports.parseLocationHierarchy = parseLocationHierarchy;
20+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
5321

5422
var domainIsValid = function domainIsValid(domainString) {
5523
if (typeof domainString !== 'string') {
@@ -74,22 +42,13 @@ var domainIsValid = function domainIsValid(domainString) {
7442
var fetchDomainHierarchy = function fetchDomainHierarchy(domain) {
7543
if (!domainIsValid) {
7644
return Promise.reject(new Error('Domain is not valid'));
77-
} // Execute Locations REST API hierarchy fetch and pipe results to processing function
45+
} // Execute Locations REST API hierarchy fetch
7846

7947

8048
return new Promise(function (resolve, reject) {
8149
_NeonApi.default.getSiteLocationHierarchyObservable(domain).pipe((0, _operators.map)(function (response) {
8250
if (response && response.data) {
83-
var data = {};
84-
response.data.locationChildHierarchy.forEach(function (child) {
85-
// At the top level we only care about sites and don't want the HQ test site
86-
if (child.locationType !== 'SITE' || child.locationName === 'HQTW') {
87-
return;
88-
}
89-
90-
data[child.locationName] = parseLocationHierarchy(child);
91-
});
92-
resolve(data);
51+
resolve(response.data);
9352
return (0, _rxjs.of)(true);
9453
}
9554

@@ -99,12 +58,42 @@ var fetchDomainHierarchy = function fetchDomainHierarchy(domain) {
9958
reject(new Error(error.message));
10059
return (0, _rxjs.of)(false);
10160
})).subscribe();
61+
}).then(function (data) {
62+
return (0, _parseDomainHierarchy.default)(data);
10263
});
10364
};
10465

10566
exports.fetchDomainHierarchy = fetchDomainHierarchy;
10667

107-
var fetchLocations = function fetchLocations(locations) {
68+
var fetchSingleLocationREST = function fetchSingleLocationREST(location) {
69+
if (typeof location !== 'string' || !location.length) {
70+
return Promise.reject(new Error('Location is not valid; must be non-empty string'));
71+
} // Execute REST query and pipe results to processing function
72+
73+
74+
return new Promise(function (resolve, reject) {
75+
_NeonApi.default.getLocationObservable(location).pipe((0, _operators.map)(function (response) {
76+
if (response && response.data) {
77+
resolve([response.data]);
78+
return (0, _rxjs.of)(true);
79+
}
80+
81+
reject(new Error('Malformed response'));
82+
return (0, _rxjs.of)(false);
83+
}), (0, _operators.catchError)(function (error) {
84+
reject(new Error(error.message));
85+
return (0, _rxjs.of)(false);
86+
})).subscribe();
87+
}).then(function (data) {
88+
return (0, _parseLocationsArray.default)(data);
89+
}).then(function (locationMap) {
90+
return (locationMap || {})[location];
91+
});
92+
};
93+
94+
exports.fetchSingleLocationREST = fetchSingleLocationREST;
95+
96+
var fetchManyLocationsGraphQL = function fetchManyLocationsGraphQL(locations) {
10897
// Extract locations list and validate
10998
if (!Array.isArray(locations) || !locations.length || !locations.every(function (loc) {
11099
return typeof loc === 'string';
@@ -121,24 +110,16 @@ var fetchLocations = function fetchLocations(locations) {
121110
return (0, _rxjs.of)(false);
122111
}
123112

124-
var data = {};
125-
result.response.data.locations.forEach(function (rawLocationData) {
126-
var locationName = rawLocationData.locationName;
127-
128-
if (!locationName) {
129-
return;
130-
}
131-
132-
data[locationName] = (0, _SiteMapWorkerSafeUtils.parseLocationData)(rawLocationData);
133-
});
134-
resolve(data);
113+
resolve(result.response.data.locations);
135114
return (0, _rxjs.of)(true);
136115
}), // Error
137116
(0, _operators.catchError)(function (error) {
138117
reject(new Error(error.message));
139118
return (0, _rxjs.of)(false);
140119
})).subscribe();
120+
}).then(function (data) {
121+
return (0, _parseLocationsArray.default)(data);
141122
});
142123
};
143124

144-
exports.fetchLocations = fetchLocations;
125+
exports.fetchManyLocationsGraphQL = fetchManyLocationsGraphQL;

0 commit comments

Comments
 (0)