From f8f65bb784acbd7e76e39d485b8525dee48266a5 Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Fri, 17 Nov 2017 15:20:51 -0500 Subject: [PATCH] Introduce generic serializer for web worker transfers Closes #5143 --- src/util/web_worker_transfer.js | 155 +++++++++++++++++++++ test/unit/util/web_worker_transfer.test.js | 45 ++++++ 2 files changed, 200 insertions(+) create mode 100644 src/util/web_worker_transfer.js create mode 100644 test/unit/util/web_worker_transfer.test.js diff --git a/src/util/web_worker_transfer.js b/src/util/web_worker_transfer.js new file mode 100644 index 00000000000..9ef6c8720ba --- /dev/null +++ b/src/util/web_worker_transfer.js @@ -0,0 +1,155 @@ +// @flow + +const assert = require('assert'); +const registry: {[string]: { klass: Class, omit: Array }} = {}; + +/** + * 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(klass: Class, options: { omit: Array<$Keys> } = { 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 + | {| 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): 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 +}; diff --git a/test/unit/util/web_worker_transfer.test.js b/test/unit/util/web_worker_transfer.test.js new file mode 100644 index 00000000000..9c466a2f28e --- /dev/null +++ b/test/unit/util/web_worker_transfer.test.js @@ -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(); +});