webpack/lib/serialization/ObjectMiddleware.js

313 lines
8.4 KiB
JavaScript
Raw Normal View History

2018-10-09 20:30:59 +08:00
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
const MapObjectSerializer = require("./MapObjectSerializer");
const PlainObjectSerializer = require("./PlainObjectSerializer");
const SerializerMiddleware = require("./SerializerMiddleware");
const SetObjectSerializer = require("./SetObjectSerializer");
/** @typedef {new (...params: any[]) => any} Constructor */
/*
Format:
File -> Section*
Section -> ObjectSection | ReferenceSection | EscapeSection | OtherSection
ObjectSection -> ESCAPE (
2018-10-10 15:55:06 +08:00
number:relativeOffset (number > 0) |
2018-10-09 20:30:59 +08:00
string:request (string|null):export
) Section:value* ESCAPE ESCAPE_END_OBJECT
2018-10-10 15:55:06 +08:00
ReferenceSection -> ESCAPE number:relativeOffset (number < 0)
2018-10-09 20:30:59 +08:00
EscapeSection -> ESCAPE ESCAPE_ESCAPE_VALUE (escaped value ESCAPE)
EscapeSection -> ESCAPE ESCAPE_UNDEFINED (escaped value ESCAPE)
OtherSection -> any (except ESCAPE)
Why using null as escape value?
Multiple null values can merged by the BinaryMiddleware, which makes it very efficient
Technically any value can be used.
*/
/**
* @typedef {Object} ObjectSerializerContext
* @property {function(any): void} write
*/
/**
* @typedef {Object} ObjectDeserializerContext
* @property {function(): any} read
*/
/**
* @typedef {Object} ObjectSerializer
* @property {function(any, ObjectSerializerContext): void} serialize
* @property {function(ObjectDeserializerContext): any} deserialize
*/
const ESCAPE = null;
2018-10-10 15:55:06 +08:00
const ESCAPE_ESCAPE_VALUE = null;
const ESCAPE_END_OBJECT = true;
const ESCAPE_UNDEFINED = false;
2018-10-09 20:30:59 +08:00
const CURRENT_VERSION = 1;
const plainObjectSerializer = new PlainObjectSerializer();
const mapObjectSerializer = new MapObjectSerializer();
const setObjectSerializer = new SetObjectSerializer();
const serializers = new Map();
const serializerInversed = new Map();
const loadedRequests = new Set();
serializers.set(Object, {
2018-10-10 15:55:06 +08:00
request: "",
2018-10-09 20:30:59 +08:00
name: null,
serializer: plainObjectSerializer
});
serializers.set(Array, {
2018-10-10 15:55:06 +08:00
request: "",
2018-10-09 20:30:59 +08:00
name: null,
serializer: plainObjectSerializer
});
serializers.set(Map, {
2018-10-10 15:55:06 +08:00
request: "",
2018-10-09 20:30:59 +08:00
name: 1,
serializer: mapObjectSerializer
});
serializers.set(Set, {
2018-10-10 15:55:06 +08:00
request: "",
2018-10-09 20:30:59 +08:00
name: 2,
serializer: setObjectSerializer
});
for (const { request, name, serializer } of serializers.values()) {
serializerInversed.set(`${request}/${name}`, serializer);
}
class ObjectMiddleware extends SerializerMiddleware {
/**
* @param {Constructor} Constructor the constructor
* @param {string} request the request which will be required when deserializing
* @param {string} name the name to make multiple serializer unique when sharing a request
* @param {ObjectSerializer} serializer the serializer
* @returns {void}
*/
static register(Constructor, request, name, serializer) {
const key = request + "/" + name;
if (serializers.has(Constructor)) {
throw new Error(
`ObjectMiddleware.register: serializer for ${
Constructor.name
} is already registered`
);
}
if (serializerInversed.has(key)) {
throw new Error(
`ObjectMiddleware.register: serializer for ${key} is already registered`
);
}
serializers.set(Constructor, {
request,
name,
serializer
});
serializerInversed.set(key, serializer);
}
static getSerializerFor(object) {
const c = object.constructor;
const config = serializers.get(c);
if (!config) throw new Error(`No serializer registered for ${c.name}`);
return config;
}
static getDeserializerFor(request, name) {
const key = request + "/" + name;
const serializer = serializerInversed.get(key);
if (serializer === undefined) {
throw new Error(`No deserializer registered for ${key}`);
}
return serializer;
}
_handleFunctionSerialization(fn, context) {
return () => {
const r = fn();
if (r instanceof Promise)
return r.then(data => this.serialize([data], context));
return this.serialize([r], context);
};
}
_handleFunctionDeserialization(fn, context) {
return () => {
const r = fn();
if (r instanceof Promise)
return r.then(data => this.deserialize(data, context)[0]);
return this.deserialize(r, context)[0];
};
}
/**
* @param {any[]} data data items
* @param {TODO} context TODO
* @returns {any[]|Promise<any[]>} serialized data
*/
serialize(data, context) {
/** @type {any[]} */
const result = [CURRENT_VERSION];
let currentPos = 0;
const referenceable = new Map();
const addReferenceable = item => {
referenceable.set(item, currentPos++);
};
let currentPosTypeLookup = 0;
const objectTypeLookup = new Map();
const process = item => {
2018-10-10 16:24:44 +08:00
// check if we can emit a reference
2018-10-09 20:30:59 +08:00
const ref = referenceable.get(item);
if (ref !== undefined) {
result.push(ESCAPE, ref - currentPos);
return;
}
if (typeof item === "object" && item !== null) {
const { request, name, serializer } = ObjectMiddleware.getSerializerFor(
item
);
const key = `${request}/${name}`;
const lastIndex = objectTypeLookup.get(key);
if (lastIndex === undefined) {
objectTypeLookup.set(key, currentPosTypeLookup++);
result.push(ESCAPE, request, name);
} else {
2018-10-10 15:55:06 +08:00
result.push(ESCAPE, currentPosTypeLookup - lastIndex);
2018-10-09 20:30:59 +08:00
}
serializer.serialize(item, {
write(value) {
process(value);
}
});
result.push(ESCAPE, ESCAPE_END_OBJECT);
addReferenceable(item);
} else if (typeof item === "string") {
2018-10-10 16:24:44 +08:00
if (item !== "") {
// empty strings are shorter when not emitting a reference (this saves 1 byte per empty string)
addReferenceable(item);
}
2018-10-09 20:30:59 +08:00
result.push(item);
} else if (Buffer.isBuffer(item)) {
addReferenceable(item);
result.push(item);
} else if (item === ESCAPE) {
result.push(ESCAPE, ESCAPE_ESCAPE_VALUE);
} else if (typeof item === "function") {
result.push(this._handleFunctionSerialization(item));
} else if (item === undefined) {
result.push(ESCAPE, ESCAPE_UNDEFINED);
} else {
result.push(item);
}
};
for (const item of data) {
process(item);
}
return result;
}
/**
* @param {any[]} data data items
* @param {TODO} context TODO
* @returns {any[]|Promise<any[]>} deserialized data
*/
deserialize(data, context) {
let currentDataPos = 0;
const read = () => {
if (currentDataPos >= data.length)
throw new Error("Unexpected end of stream");
return data[currentDataPos++];
};
if (read() !== CURRENT_VERSION)
throw new Error("Version missmatch, serializer changed");
let currentPos = 0;
const referenceable = new Map();
const addReferenceable = item => {
referenceable.set(currentPos++, item);
};
let currentPosTypeLookup = 0;
const objectTypeLookup = new Map();
const result = [];
const decodeValue = () => {
const item = read();
if (item === ESCAPE) {
const nextItem = read();
if (nextItem === ESCAPE_ESCAPE_VALUE) {
return ESCAPE;
} else if (nextItem === ESCAPE_UNDEFINED) {
return undefined;
} else if (nextItem === ESCAPE_END_OBJECT) {
2018-10-10 15:55:06 +08:00
throw new Error(
`Unexpected end of object at position ${currentDataPos - 1}`
);
} else if (typeof nextItem === "number" && nextItem < 0) {
2018-10-09 20:30:59 +08:00
// relative reference
return referenceable.get(currentPos + nextItem);
} else {
2018-10-10 15:55:06 +08:00
const request = nextItem;
2018-10-09 20:30:59 +08:00
let serializer;
2018-10-10 15:55:06 +08:00
if (typeof request === "number") {
serializer = objectTypeLookup.get(currentPosTypeLookup - request);
2018-10-09 20:30:59 +08:00
} else {
2018-10-10 15:55:06 +08:00
const name = read();
2018-10-09 20:30:59 +08:00
if (request && !loadedRequests.has(request)) {
require(request);
loadedRequests.add(request);
}
serializer = ObjectMiddleware.getDeserializerFor(request, name);
objectTypeLookup.set(currentPosTypeLookup++, serializer);
}
const item = serializer.deserialize({
read() {
const item = decodeValue();
return item;
}
});
const end1 = read();
if (end1 !== ESCAPE) {
throw new Error("Expected end of object");
}
const end2 = read();
if (end2 !== ESCAPE_END_OBJECT) {
throw new Error("Expected end of object");
}
addReferenceable(item);
return item;
}
} else if (typeof item === "string") {
2018-10-10 16:24:44 +08:00
if (item !== "") {
addReferenceable(item);
}
2018-10-09 20:30:59 +08:00
return item;
} else if (Buffer.isBuffer(item)) {
addReferenceable(item);
return item;
} else if (typeof item === "function") {
return this._handleFunctionDeserialization(item, context);
} else {
return item;
}
};
while (currentDataPos < data.length) {
result.push(decodeValue());
}
return result;
}
}
module.exports = ObjectMiddleware;