The Javascript sandbox based on Web Workers.
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
src
test
typings
.gitignore
.npmignore
.travis.yml
LICENSE
README.md
karma.conf.js
package.json
tsconfig.base.json
tsconfig.json
tslint.json
webpack.common.js
webpack.dev.js
webpack.prod.js
yarn.lock

README.md

worker-sandbox npm GitHub license Build Status Coverage Status

The Javascript sandbox based on Web Workers.

Usage

Install

npm install --save worker-sandbox

OR

yarn add worker-sandbox

Quickstart

To build a runInContext function as an example:

import Sandbox from 'worker-sandbox' // Import module

async function runInContext(code, context) {
  try {
    const sandbox = new Sandbox() // Create a sandbox instance
    await sandbox.assign(context) // Assign the context of the sandbox
    return await sandbox.eval(code) // Run the code
  } finally {
    sandbox.destroy() // Destroy the Web Worker instance in the sandbox
  }
}

runInContext('sayHelloWorld()', {
  helloWorld: 'hello world'
, sayHelloWorld() {
    return helloWorld
  }
})
.then(console.log) // hello world

API

class Sandbox([worker: Worker])

Use the new operator to create a sandbox instance, and the Sandbox class constructor has no arguments.

const sandbox = new Sandbox()
sandbox instanceof Sandbox // true

You can also use your own Worker instance, see below.

Sandbox#eval(code: string): Promise<any>

Eval code in the sandbox.

const sandbox = new Sandbox()
const result = await sandbox.eval('"hello world"')
result === 'hello world' // true

Sandbox#execute(code: string): Promise<void>

No return value version of Sandbox#eval.

const sandbox = new Sandbox()
const result = await sandbox.execute('"hello world"')
result === undefined // true

Sandbox#context: { [string]: any }

This is an asynchronous Proxy object that can be used as syntactic sugar for Sandbox#set, Sandbox#get, Sandbox#remove, Sandbox#call.

const sandbox = new Sandbox()

// Get the full context
await sandbox.context // {}

// Set the value of a specific path
sandbox.context.helloWorld = 'hello world'

// Get the value of a specific path
await sandbox.context.helloWorld === 'hello world' // true

// Set a specific path as a function
sandbox.context.sayHelloWorld = speaker =>
  `${ speaker }: ${ helloWorld }`

// Call a function of a specific path (the actual function runs in the sandbox)
await sandbox.context.sayHelloWorld('Sandbox') === 'Sandbox: hello world'

// Remove the value of a specific path
delete sandbox.context.helloWorld
delete sandbox.context.sayHelloWorld
await sandbox.context.helloWorld === undefined // true
await sandbox.context.sayHelloWorld === undefined // true

Sandbox#set(path: string | string[], value: any): Promise<void>

Set the value of a specific path in the sandbox context.

const sandbox = new Sandbox()
await sandbox.set('arr', [])
await sandbox.set('arr[0]', 'hello')
await sandbox.set('arr[1]', 'world')
await sandbox.set(['arr[2]'], 'arr[2]')
(await sandbox.context.arr).join(' ') === 'hello world' // true
await sandbox.context['arr[2]'] === 'arr[2]' // true

Equivalent to

const sandbox = new Sandbox()
await sandbox.set('arr', [])
await sandbox.set(['arr', '0'], 'hello')
await sandbox.set(['arr', '1'], 'world')
await sandbox.set(['arr[2]'], 'arr[2]')
(await sandbox.context.arr).join(' ') === 'hello world' // true
await sandbox.context['arr[2]'] === 'arr[2]' // true

Equivalent to

const sandbox = new Sandbox()
sandbox.context.arr = []
sandbox.context.arr[0] = 'hello'
sandbox.context.arr[1] = 'world'
sandbox.context['arr[2]'] = 'arr[2]'
(await sandbox.context.arr).join(' ') === 'hello world' // true
await sandbox.context['arr[2]'] === 'arr[2]' // true

Sandbox#assign(obj: any) : Promise<void>

It is the Object.assign() for Sandbox#context.

const sandbox = new Sandbox()
await sandbox.assign({
  hello: 'hello'
, world: 'world'
, sayHelloWorld() {
    return `${ hello } ${ world}`
  }
, 'functions.sayHelloWorld': function() {
    return `${ hello } ${ world}`
  }
})
await sandbox.context.sayHelloWorld() === 'hello world' // true
await sandbox.context['functions.sayHelloWorld']() === 'hello world' // true

Equivalent to

const sandbox = new Sandbox()
Object.assign(sandbox.context, {
  hello: 'hello'
, world: 'world'
, sayHelloWorld() {
    return `${ hello } ${ world}`
  }
, ['functions.sayHelloWorld']() {
    return `${ hello } ${ world}`
  }
})
await sandbox.context.sayHelloWorld() === 'hello world' // true
await sandbox.context['functions.sayHelloWorld']() === 'hello world' // true

Sandbox#get(path: string | string[]): Promsie<any>

Get the value of a specific path in the sandbox context.

const sandbox = new Sandbox()
await sandbox.set('obj', {
  hello: 'hello'
, world: 'world'
})
await sandbox.get('obj.hello') === 'hello' // true
await sandbox.get('obj["world"]') === 'world' // true

Equivalent to

const sandbox = new Sandbox()
sandbox.set('obj', {
  hello: 'hello'
, world: 'world'
})
await sandbox.get(['obj', 'hello']) === 'hello' // true
await sandbox.get(['obj', 'world']) === 'world' // true

Equivalent to

const sandbox = new Sandbox()
await sandbox.set('obj', {
  hello: 'hello'
, world: 'world'
})
await sandbox.context.obj.hello === 'hello' // true
await sandbox.context.obj['world'] === 'world' // true

Sandbox#remove(path: string | string[]): Promise<void>

Remove the value of a specific path in the sandbox context.

const sandbox = new Sandbox()
await sandbox.set('obj', {
  hello: 'hello'
, world: 'world'
})
await sandbox.remove('obj.hello')
await sandbox.remove('obj["world"]')
await sandbox.context.obj // {}

Equivalent to

const sandbox = new Sandbox()
await sandbox.set('obj', {
  hello: 'hello'
, world: 'world'
})
await sandbox.remove(['obj', 'hello'])
await sandbox.remove(['obj', 'world'])
await sandbox.context.obj // {}

Equivalent to

const sandbox = new Sandbox()
await sandbox.set('obj', {
  hello: 'hello'
, world: 'world'
})
delete sandbox.context.obj.hello
delete sandbox.context.obj.world
await sandbox.context.obj // {}

Sandbox#call(path: string | string[], ...args: any[]): Promise<any>

Calling a function within a sandbox context within a specific path, the actual function runs in the sandbox.

const sandbox = new Sandbox()
sandbox.context.helloWorld = 'hello world'
sandbox.context.functions = {}
sandbox.context.functions.sayHelloWorld = speaker =>
  `${ speaker }: ${ helloWorld }`
await sandbox.call('functions.sayHelloWorld', 'Sandbox') === 'Sandbox: hello world' // true

Equivalent to

const sandbox = new Sandbox()
sandbox.context.helloWorld = 'hello world'
sandbox.context.functions = {}
sandbox.context.functions.sayHelloWorld = speaker =>
  `${ speaker }: ${ helloWorld }`
await sandbox.call(['functions', 'sayHelloWorld'], 'Sandbox') === 'Sandbox: hello world' // true

Equivalent to

const sandbox = new Sandbox()
sandbox.context.helloWorld = 'hello world'
sandbox.context.functions = {}
sandbox.context.functions.sayHelloWorld = speaker => {
  `${ speaker }: ${ helloWorld }`
await sandbox.context.functions.sayHelloWorld('Sandbox') === 'Sandbox: hello world' // true

Sandbox#callable : { [string]: Function }

This is an asynchronous Proxy object that can be used as syntactic sugar for Sandbox#registerCall and Sandbox#cancelCall.

const sandbox = new Sandbox()
const helloWorld = 'hello world'

// Register the Callable function
sandbox.callable.sayHelloWorld = function(speaker) {
  return `${ speaker }: ${ helloWorld }`
}

// Call the Callable function
await sandbox.eval('sayHelloWorld("Sandbox")') === 'Sandbox: hello world' // true

// Cancel registered Callable function
delete sandbox.callable.sayHelloWorld
await sandbox.eval('sayHelloWorld') // ReferenceError!

Sandbox#registerCall(path: string | string[], func: Function): Promise<void>

Register a Callable function in the sandbox, which can be called in the sandbox, but the actual function is done outside the sandbox.

const sandbox = new Sandbox()
const helloWorld = 'hello world'
await sandbox.registerCall('sayHelloWorld', speaker =>
  `${ speaker }: ${ helloWorld }`
)
await sandbox.eval('sayHelloWorld("Sandbox")') === 'Sandbox: hello world' // true

Sandbox#cancelCall(path: string | string[]): Promise<void>

Cancel registered Callable function.

const sandbox = new Sandbox()
const helloWorld = 'hello world'
await sandbox.registerCall('sayHelloWorld', speaker =>
  `${ speaker }: ${ helloWorld }`
)
await sandbox.cancelCall('sayHelloWorld')
await sandbox.eval('sayHelloWorld')  // ReferenceError

Sandbox#destroy(): void

Destroy the Web Worker in the instance of the sandbox, which will call the Worker#terminate () to terminate the Web Worker.

const sandbox = new Sandbox()
sandbox.destroy()

Advanced

Custom worker

The minimal worker code is:

import { MessageSystem, PERMISSION } from 'message-system'
import { WorkerMessenger } from 'message-system-worker-messenger'

(self as any)['window'] = self

new MessageSystem(new WorkerMessenger(), [
  PERMISSION.RECEIVE.EVAL
, PERMISSION.RECEIVE.CALL
, PERMISSION.RECEIVE.ASSIGN
, PERMISSION.RECEIVE.ACCESS
, PERMISSION.RECEIVE.REMOVE
, PERMISSION.RECEIVE.REGISTER
, PERMISSION.SEND.CALL
], {/* add your context here */})

Tips

The await operator can be omitted when you call Sandbox#set, Sandbox#assign, Sandox#remove, Sandbox#registerCall, Sandbox#cancelCall in the async function. Because the Web Worker inside the sandbox is single-threaded, the asynchronous methods are executed in the order they are called, the await operator is just need added when calling a function that requires a return value.

const sandbox = new Sandbox()
for (let i = 1000; i--;) {
  sandbox.set('hello', 'hello')
  sandbox.assign({
    world: 'world'
  , removable: 'removable'
  })
  sandbox.remove('removable')
}
await sandbox.context.removable === undefined // true
await sandbox.context.hello === 'hello' // true
await sandbox.context.world === 'world' // true

Projects using worker-sandbox

gloria-sandbox: Sandbox for Gloria based on worker-sandbox