Skip to content

Commit

Permalink
feat(troika-worker-modules): improve rehydration of functions in worker
Browse files Browse the repository at this point in the history
Functions are now rehydrated using `importScripts(blobURL)` rather than
`new Function()`. This makes it slightly easier to whitelist in CSP
rules (see issue #31), and improves debugging since they now show up
as sources in devtools. Added a `name` parameter to defineWorkerModule
which gets inserted as a comment in the rehydrated source so it's easier
to find where each function comes from.
  • Loading branch information
lojjic committed Apr 3, 2020
1 parent f7526f4 commit 8f63090
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 9 deletions.
1 change: 1 addition & 0 deletions packages/troika-3d-text/src/FontParser_OpenType.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ function parserFactory(opentype) {


const workerModule = defineWorkerModule({
name: 'OpenType Font Parser',
dependencies: [opentypeFactory, parserFactory],
init(opentypeFactory, parserFactory) {
const opentype = opentypeFactory()
Expand Down
1 change: 1 addition & 0 deletions packages/troika-3d-text/src/FontParser_Typr.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ function parserFactory(Typr, woff2otf) {


const workerModule = defineWorkerModule({
name: 'Typr Font Parser',
dependencies: [typrFactory, woff2otfFactory, parserFactory],
init(typrFactory, woff2otfFactory, parserFactory) {
const Typr = typrFactory()
Expand Down
2 changes: 2 additions & 0 deletions packages/troika-3d-text/src/TextBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ function assign(toObj, fromObj) {


export const fontProcessorWorkerModule = defineWorkerModule({
name: 'FontProcessor',
dependencies: [
CONFIG,
SDF_DISTANCE_PERCENT,
Expand All @@ -199,6 +200,7 @@ export const fontProcessorWorkerModule = defineWorkerModule({
})

export const processInWorker = defineWorkerModule({
name: 'TextBuilder',
dependencies: [fontProcessorWorkerModule, ThenableWorkerModule],
init(fontProcessor, Thenable) {
return function(args) {
Expand Down
5 changes: 3 additions & 2 deletions packages/troika-3d-ui/src/flex-layout/FlexLayoutProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function requestFlexLayout(styleTree, callback) {


function createFlexLayoutProcessor(Yoga, loadFontFn, measureFn) {

const YOGA_VALUE_MAPPINGS = {
align: {
'auto': 'ALIGN_AUTO',
Expand Down Expand Up @@ -246,6 +246,7 @@ function createFlexLayoutProcessor(Yoga, loadFontFn, measureFn) {


const flexLayoutProcessorWorkerModule = defineWorkerModule({
name: 'FlexLayoutProcessor',
dependencies: [
yogaFactory,
fontProcessorWorkerModule,
Expand Down Expand Up @@ -310,7 +311,7 @@ const styleTreeExample = {
fontSize,
lineHeight,
letterSpacing,
children: [{
//...
}]
Expand Down
1 change: 1 addition & 0 deletions packages/troika-worker-utils/src/ThenableWorkerModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {defineWorkerModule} from './WorkerModules.js'
* the raw function in its `dependencies` array so it only gets registered once.
*/
export default defineWorkerModule({
name: 'Thenable',
dependencies: [Thenable],
init: function(Thenable) {
return Thenable
Expand Down
17 changes: 14 additions & 3 deletions packages/troika-worker-utils/src/WorkerModules.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { workerBootstrap } from './workerBootstrap.js'

let _workerModuleId = 0
let _messageId = 0
let _allowInitAsString = false
const workers = Object.create(null)
const openRequests = Object.create(null)
openRequests._count = 0
Expand Down Expand Up @@ -31,6 +32,8 @@ openRequests._count = 0
* It will be passed that response value, and if it returns an array then that will be
* used as the "transferables" parameter to `postMessage`. Use this if there are values
* in the response that can/should be transfered rather than cloned.
* @param {string} [options.name] - A descriptive name for this module; this can be useful for
* debugging but is not currently used for anything else.
* @param {string} [options.workerId] - By default all modules will run in the same dedicated worker,
* but if you want to use multiple workers you can pass a `workerId` to indicate a specific
* worker to spawn. Note that each worker is completely standalone and no data or state will
Expand All @@ -39,23 +42,27 @@ openRequests._count = 0
* @return {function(...[*]): {then}}
*/
export function defineWorkerModule(options) {
if (!options || typeof options.init !== 'function') {
if ((!options || typeof options.init !== 'function') && !_allowInitAsString) {
throw new Error('requires `options.init` function')
}
let {dependencies, init, getTransferables, workerId} = options
if (workerId == null) {
workerId = '#default'
}
const id = `workerModule${++_workerModuleId}`
const name = options.name || id
let registrationThenable = null

dependencies = dependencies && dependencies.map(dep => {
// Wrap raw functions as worker modules with no dependencies
if (typeof dep === 'function' && !dep.workerModuleData) {
_allowInitAsString = true
dep = defineWorkerModule({
workerId,
init: new Function(`return function(){return (${stringifyFunction(dep)})}`)()
name: `<${name}> function dependency: ${dep.name}`,
init: `function(){return (\n${stringifyFunction(dep)}\n)}`
})
_allowInitAsString = false
}
// Grab postable data for worker modules
if (dep && dep.workerModuleData) {
Expand All @@ -82,6 +89,7 @@ export function defineWorkerModule(options) {
moduleFunc.workerModuleData = {
isWorkerModule: true,
id,
name,
dependencies,
init: stringifyFunction(init),
getTransferables: getTransferables && stringifyFunction(getTransferables)
Expand Down Expand Up @@ -112,7 +120,10 @@ function getWorker(workerId) {
// Create the worker from the bootstrap function content
worker = workers[workerId] = new Worker(
URL.createObjectURL(
new Blob([`;(${bootstrap})()`], {type: 'application/javascript'})
new Blob(
[`/** Worker Module Bootstrap: ${workerId.replace(/\*/g, '')} **/\n\n;(${bootstrap})()`],
{type: 'application/javascript'}
)
)
)

Expand Down
32 changes: 28 additions & 4 deletions packages/troika-worker-utils/src/workerBootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function workerBootstrap() {
const modules = Object.create(null)

// Handle messages for registering a module
function registerModule({id, dependencies=[], init=function(){}, getTransferables=null}, callback) {
function registerModule({id, name, dependencies=[], init=function(){}, getTransferables=null}, callback) {
// Only register once
if (modules[id]) return

Expand All @@ -23,13 +23,18 @@ export function workerBootstrap() {
})

// Rehydrate functions
init = new Function(`return (${init})`)()
init = rehydrate(`<${name}>.init`, init)
if (getTransferables) {
getTransferables = new Function(`return (${getTransferables})`)()
getTransferables = rehydrate(`<${name}>.getTransferables`, getTransferables)
}

// Initialize the module and store its value
const value = init(...dependencies)
let value = null
if (typeof init === 'function') {
value = init(...dependencies)
} else {
console.error('worker module init function failed to rehydrate')
}
modules[id] = {
id,
value,
Expand Down Expand Up @@ -73,6 +78,25 @@ export function workerBootstrap() {
}
}

function rehydrate(name, str) {
let result = void 0
self.troikaDefine = r => result = r
let url = URL.createObjectURL(
new Blob(
[`/** ${name.replace(/\*/g, '')} **/\n\ntroikaDefine(\n${str}\n)`],
{type: 'application/javascript'}
)
)
try {
importScripts(url)
} catch(err) {
console.error(err)
}
URL.revokeObjectURL(url)
delete self.troikaDefine
return result
}

// Handler for all messages within the worker
self.addEventListener('message', e => {
const {messageId, action, data} = e.data
Expand Down

0 comments on commit 8f63090

Please sign in to comment.