Skip to content

Commit

Permalink
Introduce generic serializer for web worker transfers
Browse files Browse the repository at this point in the history
Closes #5143
  • Loading branch information
Anand Thakker committed Nov 17, 2017
1 parent 485ff9f commit f8f65bb
Show file tree
Hide file tree
Showing 2 changed files with 200 additions and 0 deletions.
155 changes: 155 additions & 0 deletions src/util/web_worker_transfer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// @flow

const assert = require('assert');
const registry: {[string]: { klass: Class<any>, omit: Array<string> }} = {};

/**
* Register the given class as serializable.
*
* To mark certain properties as non-serializable (such as cached/computed
* properties), include an optional { omit: [] } argument.
*
* @private
*/
function register<T: any>(klass: Class<T>, options: { omit: Array<$Keys<T>> } = { omit: [] }) {
assert(klass.name);
assert(!registry[klass.name]);
registry[klass.name] = { klass, omit: options.omit };
}

register(Object);

export type Serialized =
| null
| void
| boolean
| number
| string
| Boolean
| Number
| String
| Date
| RegExp
| ArrayBuffer
| $ArrayBufferView
| Array<Serialized>
| {| name: string, properties: {+[string]: Serialized} |};

/**
* Serialize the given object for transfer to or from a web worker.
*
* For non-builtin types, recursively serialize each property (possibly
* omitting certain properties - see register()), and package the result along
* with the constructor's `name` so that the appropriate constructor can be
* looked up in `deserialize()`.
*
* If a `transferables` array is provided, add any transferable objects (i.e.,
* any ArrayBuffers or ArrayBuffer views) to the list. (If a copy is needed,
* this should happen in the client code, before using serialize().)
*/
function serialize(input: mixed, transferables?: Array<Transferable>): Serialized {
if (input === null ||
input === undefined ||
typeof input === 'boolean' ||
typeof input === 'number' ||
typeof input === 'string' ||
input instanceof Boolean ||
input instanceof Number ||
input instanceof String ||
input instanceof Date ||
input instanceof RegExp) {
return input;
}

if (input instanceof ArrayBuffer) {
if (transferables) {
transferables.push(input);
}
return input;
}

if (ArrayBuffer.isView(input)) {
const view: $ArrayBufferView = (input: any);
if (transferables) {
transferables.push(view.buffer);
}
return view;
}

if (Array.isArray(input)) {
return input.map((i) => serialize(i, transferables));
}

if (typeof input === 'object') {
const name = input.constructor.name;
if (!name) {
throw new Error(`can't serialize object of anonymous class`);
}

if (!registry[name]) {
throw new Error(`can't serialize unregistered class ${name}`);
}

const {omit} = registry[name];

const properties: {[string]: Serialized} = {};

for (const key of Object.keys(input)) {
if (omit.indexOf(key) >= 0) continue;
properties[key] = serialize(input[key], transferables);
}

return {name, properties};
}

throw new Error(`can't serialize object of type ${typeof input}`);
}

function deserialize(input: Serialized): mixed {
if (input === null ||
input === undefined ||
typeof input === 'boolean' ||
typeof input === 'number' ||
typeof input === 'string' ||
input instanceof Boolean ||
input instanceof Number ||
input instanceof String ||
input instanceof Date ||
input instanceof RegExp ||
input instanceof ArrayBuffer ||
ArrayBuffer.isView(input)) {
return input;
}

if (Array.isArray(input)) {
return input.map((i) => deserialize(i));
}

if (typeof input === 'object') {
const {name, properties} = (input: any);
if (!name) {
throw new Error(`can't deserialize object of anonymous class`);
}

const {klass} = registry[name];
if (!klass) {
throw new Error(`can't deserialize unregistered class ${name}`);
}

const result = Object.create(klass.prototype);

for (const key of Object.keys(properties)) {
result[key] = deserialize(properties[key]);
}

return result;
}

throw new Error(`can't deserialize object of type ${typeof input}`);
}

module.exports = {
register,
serialize,
deserialize
};
45 changes: 45 additions & 0 deletions test/unit/util/web_worker_transfer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// @flow

'use strict';

const test = require('mapbox-gl-js-test').test;
const {register, serialize, deserialize} = require('../../../src/util/web_worker_transfer');

test('round trip', (t) => {
class Foo {
n/*: number*/;
buffer/*: ArrayBuffer*/;
_cached/*: ?number*/;

constructor(n) {
this.n = n;
this.buffer = new ArrayBuffer(100);
this.squared();
}

squared() {
if (this._cached) {
return this._cached;
}
this._cached = this.n * this.n;
return this._cached;
}
}

register(Foo, { omit: ['_cached'] });

const foo = new Foo(10);
const transferables = [];
const deserialized = deserialize(serialize(foo, transferables));
t.assert(deserialized instanceof Foo);
const bar/*: Foo*/ = (deserialized/*: any*/);

t.assert(foo !== bar);
t.assert(bar.constructor === Foo);
t.assert(bar.n === 10);
t.assert(bar.buffer === foo.buffer);
t.assert(transferables[0] === foo.buffer);
t.assert(bar._cached === undefined);
t.assert(bar.squared() === 100);
t.end();
});

0 comments on commit f8f65bb

Please sign in to comment.