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 28, 2017
1 parent d69688b commit e2dce3b
Show file tree
Hide file tree
Showing 2 changed files with 287 additions and 0 deletions.
203 changes: 203 additions & 0 deletions src/util/web_worker_transfer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// @flow

const assert = require('assert');
const Color = require('../style-spec/util/color');

import type {Transferable} from '../types/transferable';

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


type Registry = {
[string]: {
klass: Class<any>,
omit: $ReadOnlyArray<string>,
shallow: $ReadOnlyArray<string>
}
};

type RegisterOptions<T> = {
omit?: $ReadOnlyArray<$Keys<T>>,
shallow?: $ReadOnlyArray<$Keys<T>>
}

const registry: Registry = {};

/**
* Register the given class as serializable.
*
* @param options
* @param options.omit List of properties to omit from serialization (e.g., cached/computed properties)
* @param options.shallow List of properties that should be serialized by a simple shallow copy, rather than by a recursive call to serialize().
*
* @private
*/
function register<T: any>(klass: Class<T>, options: RegisterOptions<T> = {}) {
const name: string = klass.name;
assert(name);
assert(!registry[name], `${name} is already registered.`);
registry[name] = {
klass,
omit: options.omit || [],
shallow: options.shallow || []
};
}

register(Object);
register(Color);
/**
* 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)) {
const serialized = [];
for (const item of input) {
serialized.push(serialize(item, transferables));
}
return serialized;
}

if (typeof input === 'object') {
const klass = (input.constructor: any);
const name = klass.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 properties: {[string]: Serialized} = {};

if (klass.serialize) {
// (Temporary workaround) allow a class to provide static
// `serialize()` and `deserialize()` methods to bypass the generic
// approach.
// This temporary workaround lets us use the generic serialization
// approach for objects whose members include instances of dynamic
// StructArray types. Once we refactor StructArray to be static,
// we can remove this complexity.
properties._serialized = (klass.serialize: typeof serialize)(input, transferables);
} else {
for (const key in input) {
// any cast due to https://github.com/facebook/flow/issues/5393
if (!(input: any).hasOwnProperty(key)) continue;
if (registry[name].omit.indexOf(key) >= 0) continue;
const property = (input: any)[key];
properties[key] = registry[name].shallow.indexOf(key) >= 0 ?
property :
serialize(property, 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}`);
}

if (klass.deserialize) {
return (klass.deserialize: typeof deserialize)(properties._serialized);
}

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

for (const key of Object.keys(properties)) {
result[key] = registry[name].shallow.indexOf(key) >= 0 ?
properties[key] : deserialize(properties[key]);
}

return result;
}

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

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

'use strict';

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

/*::
import type {Serialized} from '../../../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();
});

test('custom serialization', (t) => {
class Bar {
/*:: id: string; */
/*:: _deserialized: boolean; */
constructor(id) {
this.id = id;
this._deserialized = false;
}

static serialize(b/*: Bar*/)/*: Serialized*/ {
return `custom serialization,${b.id}`;
}

static deserialize(input/*: Serialized*/)/*: Bar*/ {
const b = new Bar((input/*: any*/).split(',')[1]);
b._deserialized = true;
return b;
}
}

register(Bar);

const bar = new Bar('a');
t.assert(!bar._deserialized);

const deserialized = deserialize(serialize(bar));
t.assert(deserialized instanceof Bar);
const bar2/*: Bar*/ = (deserialized/*: any*/);
t.equal(bar2.id, bar.id);
t.assert(bar2._deserialized);
t.end();
});


0 comments on commit e2dce3b

Please sign in to comment.