-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce generic serializer for web worker transfers
Closes #5143
- Loading branch information
Anand Thakker
committed
Nov 28, 2017
1 parent
d69688b
commit e2dce3b
Showing
2 changed files
with
287 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
|
||
|