| 1 | /*
|
|---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | */
|
|---|
| 4 |
|
|---|
| 5 | "use strict";
|
|---|
| 6 |
|
|---|
| 7 | const { DEFAULTS } = require("../config/defaults");
|
|---|
| 8 | const createHash = require("../util/createHash");
|
|---|
| 9 | const AggregateErrorSerializer = require("./AggregateErrorSerializer");
|
|---|
| 10 | const ArraySerializer = require("./ArraySerializer");
|
|---|
| 11 | const DateObjectSerializer = require("./DateObjectSerializer");
|
|---|
| 12 | const ErrorObjectSerializer = require("./ErrorObjectSerializer");
|
|---|
| 13 | const MapObjectSerializer = require("./MapObjectSerializer");
|
|---|
| 14 | const NullPrototypeObjectSerializer = require("./NullPrototypeObjectSerializer");
|
|---|
| 15 | const PlainObjectSerializer = require("./PlainObjectSerializer");
|
|---|
| 16 | const RegExpObjectSerializer = require("./RegExpObjectSerializer");
|
|---|
| 17 | const SerializerMiddleware = require("./SerializerMiddleware");
|
|---|
| 18 | const SetObjectSerializer = require("./SetObjectSerializer");
|
|---|
| 19 |
|
|---|
| 20 | /** @typedef {import("../logging/Logger").Logger} Logger */
|
|---|
| 21 | /** @typedef {import("../util/Hash").HashFunction} HashFunction */
|
|---|
| 22 | /** @typedef {import("./SerializerMiddleware").LazyOptions} LazyOptions */
|
|---|
| 23 | /** @typedef {import("./types").ComplexSerializableType} ComplexSerializableType */
|
|---|
| 24 | /** @typedef {import("./types").PrimitiveSerializableType} PrimitiveSerializableType */
|
|---|
| 25 |
|
|---|
| 26 | /** @typedef {new (...params: EXPECTED_ANY[]) => EXPECTED_ANY} Constructor */
|
|---|
| 27 |
|
|---|
| 28 | /*
|
|---|
| 29 |
|
|---|
| 30 | Format:
|
|---|
| 31 |
|
|---|
| 32 | File -> Section*
|
|---|
| 33 | Section -> ObjectSection | ReferenceSection | EscapeSection | OtherSection
|
|---|
| 34 |
|
|---|
| 35 | ObjectSection -> ESCAPE (
|
|---|
| 36 | number:relativeOffset (number > 0) |
|
|---|
| 37 | string:request (string|null):export
|
|---|
| 38 | ) Section:value* ESCAPE ESCAPE_END_OBJECT
|
|---|
| 39 | ReferenceSection -> ESCAPE number:relativeOffset (number < 0)
|
|---|
| 40 | EscapeSection -> ESCAPE ESCAPE_ESCAPE_VALUE (escaped value ESCAPE)
|
|---|
| 41 | EscapeSection -> ESCAPE ESCAPE_UNDEFINED (escaped value ESCAPE)
|
|---|
| 42 | OtherSection -> any (except ESCAPE)
|
|---|
| 43 |
|
|---|
| 44 | Why using null as escape value?
|
|---|
| 45 | Multiple null values can merged by the BinaryMiddleware, which makes it very efficient
|
|---|
| 46 | Technically any value can be used.
|
|---|
| 47 |
|
|---|
| 48 | */
|
|---|
| 49 |
|
|---|
| 50 | /**
|
|---|
| 51 | * Defines the object serializer snapshot type used by this module.
|
|---|
| 52 | * @typedef {object} ObjectSerializerSnapshot
|
|---|
| 53 | * @property {number} length
|
|---|
| 54 | * @property {number} cycleStackSize
|
|---|
| 55 | * @property {number} referenceableSize
|
|---|
| 56 | * @property {number} currentPos
|
|---|
| 57 | * @property {number} objectTypeLookupSize
|
|---|
| 58 | * @property {number} currentPosTypeLookup
|
|---|
| 59 | */
|
|---|
| 60 |
|
|---|
| 61 | /** @typedef {EXPECTED_OBJECT | string} ReferenceableItem */
|
|---|
| 62 |
|
|---|
| 63 | /**
|
|---|
| 64 | * Defines the object serializer context type used by this module.
|
|---|
| 65 | * @typedef {object} ObjectSerializerContext
|
|---|
| 66 | * @property {(value: EXPECTED_ANY) => void} write
|
|---|
| 67 | * @property {(value: ReferenceableItem) => void} setCircularReference
|
|---|
| 68 | * @property {() => ObjectSerializerSnapshot} snapshot
|
|---|
| 69 | * @property {(snapshot: ObjectSerializerSnapshot) => void} rollback
|
|---|
| 70 | * @property {((item: EXPECTED_ANY | (() => EXPECTED_ANY)) => void)=} writeLazy
|
|---|
| 71 | * @property {((item: (EXPECTED_ANY | (() => EXPECTED_ANY)), obj: LazyOptions | undefined) => import("./SerializerMiddleware").LazyFunction<EXPECTED_ANY, EXPECTED_ANY, EXPECTED_ANY, LazyOptions>)=} writeSeparate
|
|---|
| 72 | */
|
|---|
| 73 |
|
|---|
| 74 | /**
|
|---|
| 75 | * Defines the object deserializer context type used by this module.
|
|---|
| 76 | * @typedef {object} ObjectDeserializerContext
|
|---|
| 77 | * @property {() => EXPECTED_ANY} read
|
|---|
| 78 | * @property {(value: ReferenceableItem) => void} setCircularReference
|
|---|
| 79 | */
|
|---|
| 80 |
|
|---|
| 81 | /**
|
|---|
| 82 | * Defines the object serializer type used by this module.
|
|---|
| 83 | * @typedef {object} ObjectSerializer
|
|---|
| 84 | * @property {(value: EXPECTED_ANY, context: ObjectSerializerContext) => void} serialize
|
|---|
| 85 | * @property {(context: ObjectDeserializerContext) => EXPECTED_ANY} deserialize
|
|---|
| 86 | */
|
|---|
| 87 |
|
|---|
| 88 | /**
|
|---|
| 89 | * Updates set size using the provided set.
|
|---|
| 90 | * @template T
|
|---|
| 91 | * @param {Set<T>} set set
|
|---|
| 92 | * @param {number} size count of items to keep
|
|---|
| 93 | */
|
|---|
| 94 | const setSetSize = (set, size) => {
|
|---|
| 95 | let i = 0;
|
|---|
| 96 | for (const item of set) {
|
|---|
| 97 | if (i++ >= size) {
|
|---|
| 98 | set.delete(item);
|
|---|
| 99 | }
|
|---|
| 100 | }
|
|---|
| 101 | };
|
|---|
| 102 |
|
|---|
| 103 | /**
|
|---|
| 104 | * Updates map size using the provided map.
|
|---|
| 105 | * @template K, X
|
|---|
| 106 | * @param {Map<K, X>} map map
|
|---|
| 107 | * @param {number} size count of items to keep
|
|---|
| 108 | */
|
|---|
| 109 | const setMapSize = (map, size) => {
|
|---|
| 110 | let i = 0;
|
|---|
| 111 | for (const item of map.keys()) {
|
|---|
| 112 | if (i++ >= size) {
|
|---|
| 113 | map.delete(item);
|
|---|
| 114 | }
|
|---|
| 115 | }
|
|---|
| 116 | };
|
|---|
| 117 |
|
|---|
| 118 | /**
|
|---|
| 119 | * Returns hash.
|
|---|
| 120 | * @param {Buffer} buffer buffer
|
|---|
| 121 | * @param {HashFunction} hashFunction hash function to use
|
|---|
| 122 | * @returns {string} hash
|
|---|
| 123 | */
|
|---|
| 124 | const toHash = (buffer, hashFunction) => {
|
|---|
| 125 | const hash = createHash(hashFunction);
|
|---|
| 126 | hash.update(buffer);
|
|---|
| 127 | return hash.digest("latin1");
|
|---|
| 128 | };
|
|---|
| 129 |
|
|---|
| 130 | const ESCAPE = null;
|
|---|
| 131 | const ESCAPE_ESCAPE_VALUE = null;
|
|---|
| 132 | const ESCAPE_END_OBJECT = true;
|
|---|
| 133 | const ESCAPE_UNDEFINED = false;
|
|---|
| 134 |
|
|---|
| 135 | const CURRENT_VERSION = 2;
|
|---|
| 136 |
|
|---|
| 137 | /** @typedef {{ request?: string, name?: string | number | null, serializer?: ObjectSerializer }} SerializerConfig */
|
|---|
| 138 | /** @typedef {{ request?: string, name?: string | number | null, serializer: ObjectSerializer }} SerializerConfigWithSerializer */
|
|---|
| 139 |
|
|---|
| 140 | /** @type {Map<Constructor | null, SerializerConfig>} */
|
|---|
| 141 | const serializers = new Map();
|
|---|
| 142 | /** @type {Map<string | number, ObjectSerializer>} */
|
|---|
| 143 | const serializerInversed = new Map();
|
|---|
| 144 |
|
|---|
| 145 | /** @type {Set<string>} */
|
|---|
| 146 | const loadedRequests = new Set();
|
|---|
| 147 |
|
|---|
| 148 | const NOT_SERIALIZABLE = {};
|
|---|
| 149 |
|
|---|
| 150 | /** @type {Map<Constructor | null, ObjectSerializer>} */
|
|---|
| 151 | const jsTypes = new Map();
|
|---|
| 152 |
|
|---|
| 153 | jsTypes.set(Object, new PlainObjectSerializer());
|
|---|
| 154 | jsTypes.set(Array, new ArraySerializer());
|
|---|
| 155 | jsTypes.set(null, new NullPrototypeObjectSerializer());
|
|---|
| 156 | jsTypes.set(Map, new MapObjectSerializer());
|
|---|
| 157 | jsTypes.set(Set, new SetObjectSerializer());
|
|---|
| 158 | jsTypes.set(Date, new DateObjectSerializer());
|
|---|
| 159 | jsTypes.set(RegExp, new RegExpObjectSerializer());
|
|---|
| 160 | jsTypes.set(Error, new ErrorObjectSerializer(Error));
|
|---|
| 161 | jsTypes.set(EvalError, new ErrorObjectSerializer(EvalError));
|
|---|
| 162 | jsTypes.set(RangeError, new ErrorObjectSerializer(RangeError));
|
|---|
| 163 | jsTypes.set(ReferenceError, new ErrorObjectSerializer(ReferenceError));
|
|---|
| 164 | jsTypes.set(SyntaxError, new ErrorObjectSerializer(SyntaxError));
|
|---|
| 165 | jsTypes.set(TypeError, new ErrorObjectSerializer(TypeError));
|
|---|
| 166 |
|
|---|
| 167 | // eslint-disable-next-line n/no-unsupported-features/es-builtins, n/no-unsupported-features/es-syntax
|
|---|
| 168 | if (typeof AggregateError !== "undefined") {
|
|---|
| 169 | jsTypes.set(
|
|---|
| 170 | // eslint-disable-next-line n/no-unsupported-features/es-builtins, n/no-unsupported-features/es-syntax
|
|---|
| 171 | AggregateError,
|
|---|
| 172 | new AggregateErrorSerializer()
|
|---|
| 173 | );
|
|---|
| 174 | }
|
|---|
| 175 |
|
|---|
| 176 | // If in a sandboxed environment (e.g. jest), this escapes the sandbox and registers
|
|---|
| 177 | // real Object and Array types to. These types may occur in the wild too, e.g. when
|
|---|
| 178 | // using Structured Clone in postMessage.
|
|---|
| 179 | // eslint-disable-next-line n/exports-style
|
|---|
| 180 | if (exports.constructor !== Object) {
|
|---|
| 181 | // eslint-disable-next-line n/exports-style
|
|---|
| 182 | const Obj = /** @type {ObjectConstructor} */ (exports.constructor);
|
|---|
| 183 | const Fn = /** @type {FunctionConstructor} */ (Obj.constructor);
|
|---|
| 184 | for (const [type, config] of jsTypes) {
|
|---|
| 185 | if (type) {
|
|---|
| 186 | const Type = new Fn(`return ${type.name};`)();
|
|---|
| 187 | jsTypes.set(Type, config);
|
|---|
| 188 | }
|
|---|
| 189 | }
|
|---|
| 190 | }
|
|---|
| 191 |
|
|---|
| 192 | {
|
|---|
| 193 | let i = 1;
|
|---|
| 194 | for (const [type, serializer] of jsTypes) {
|
|---|
| 195 | serializers.set(type, {
|
|---|
| 196 | request: "",
|
|---|
| 197 | name: i++,
|
|---|
| 198 | serializer
|
|---|
| 199 | });
|
|---|
| 200 | }
|
|---|
| 201 | }
|
|---|
| 202 |
|
|---|
| 203 | for (const { request, name, serializer } of serializers.values()) {
|
|---|
| 204 | serializerInversed.set(
|
|---|
| 205 | `${request}/${name}`,
|
|---|
| 206 | /** @type {ObjectSerializer} */ (serializer)
|
|---|
| 207 | );
|
|---|
| 208 | }
|
|---|
| 209 |
|
|---|
| 210 | /** @type {Map<RegExp, (request: string) => boolean>} */
|
|---|
| 211 | const loaders = new Map();
|
|---|
| 212 |
|
|---|
| 213 | /** @typedef {ComplexSerializableType[]} DeserializedType */
|
|---|
| 214 | /** @typedef {PrimitiveSerializableType[]} SerializedType */
|
|---|
| 215 | /** @typedef {{ logger: Logger }} Context */
|
|---|
| 216 |
|
|---|
| 217 | /** @typedef {(context: ObjectSerializerContext | ObjectDeserializerContext) => void} ExtendContext */
|
|---|
| 218 |
|
|---|
| 219 | /**
|
|---|
| 220 | * Represents ObjectMiddleware.
|
|---|
| 221 | * @extends {SerializerMiddleware<DeserializedType, SerializedType, Context>}
|
|---|
| 222 | */
|
|---|
| 223 | class ObjectMiddleware extends SerializerMiddleware {
|
|---|
| 224 | /**
|
|---|
| 225 | * Creates an instance of ObjectMiddleware.
|
|---|
| 226 | * @param {ExtendContext} extendContext context extensions
|
|---|
| 227 | * @param {HashFunction} hashFunction hash function to use
|
|---|
| 228 | */
|
|---|
| 229 | constructor(extendContext, hashFunction = DEFAULTS.HASH_FUNCTION) {
|
|---|
| 230 | super();
|
|---|
| 231 | /** @type {ExtendContext} */
|
|---|
| 232 | this.extendContext = extendContext;
|
|---|
| 233 | /** @type {HashFunction} */
|
|---|
| 234 | this._hashFunction = hashFunction;
|
|---|
| 235 | }
|
|---|
| 236 |
|
|---|
| 237 | /**
|
|---|
| 238 | * Processes the provided reg exp.
|
|---|
| 239 | * @param {RegExp} regExp RegExp for which the request is tested
|
|---|
| 240 | * @param {(request: string) => boolean} loader loader to load the request, returns true when successful
|
|---|
| 241 | * @returns {void}
|
|---|
| 242 | */
|
|---|
| 243 | static registerLoader(regExp, loader) {
|
|---|
| 244 | loaders.set(regExp, loader);
|
|---|
| 245 | }
|
|---|
| 246 |
|
|---|
| 247 | /**
|
|---|
| 248 | * Processes the provided constructor.
|
|---|
| 249 | * @param {Constructor} Constructor the constructor
|
|---|
| 250 | * @param {string} request the request which will be required when deserializing
|
|---|
| 251 | * @param {string | null} name the name to make multiple serializer unique when sharing a request
|
|---|
| 252 | * @param {ObjectSerializer} serializer the serializer
|
|---|
| 253 | * @returns {void}
|
|---|
| 254 | */
|
|---|
| 255 | static register(Constructor, request, name, serializer) {
|
|---|
| 256 | const key = `${request}/${name}`;
|
|---|
| 257 |
|
|---|
| 258 | if (serializers.has(Constructor)) {
|
|---|
| 259 | throw new Error(
|
|---|
| 260 | `ObjectMiddleware.register: serializer for ${Constructor.name} is already registered`
|
|---|
| 261 | );
|
|---|
| 262 | }
|
|---|
| 263 |
|
|---|
| 264 | if (serializerInversed.has(key)) {
|
|---|
| 265 | throw new Error(
|
|---|
| 266 | `ObjectMiddleware.register: serializer for ${key} is already registered`
|
|---|
| 267 | );
|
|---|
| 268 | }
|
|---|
| 269 |
|
|---|
| 270 | serializers.set(Constructor, {
|
|---|
| 271 | request,
|
|---|
| 272 | name,
|
|---|
| 273 | serializer
|
|---|
| 274 | });
|
|---|
| 275 |
|
|---|
| 276 | serializerInversed.set(key, serializer);
|
|---|
| 277 | }
|
|---|
| 278 |
|
|---|
| 279 | /**
|
|---|
| 280 | * Register not serializable.
|
|---|
| 281 | * @param {Constructor} Constructor the constructor
|
|---|
| 282 | * @returns {void}
|
|---|
| 283 | */
|
|---|
| 284 | static registerNotSerializable(Constructor) {
|
|---|
| 285 | if (serializers.has(Constructor)) {
|
|---|
| 286 | throw new Error(
|
|---|
| 287 | `ObjectMiddleware.registerNotSerializable: serializer for ${Constructor.name} is already registered`
|
|---|
| 288 | );
|
|---|
| 289 | }
|
|---|
| 290 |
|
|---|
| 291 | serializers.set(Constructor, NOT_SERIALIZABLE);
|
|---|
| 292 | }
|
|---|
| 293 |
|
|---|
| 294 | /**
|
|---|
| 295 | * Gets serializer for.
|
|---|
| 296 | * @param {EXPECTED_ANY} object for serialization
|
|---|
| 297 | * @returns {SerializerConfigWithSerializer} Serializer config
|
|---|
| 298 | */
|
|---|
| 299 | static getSerializerFor(object) {
|
|---|
| 300 | const proto = Object.getPrototypeOf(object);
|
|---|
| 301 | /** @type {null | Constructor} */
|
|---|
| 302 | let c;
|
|---|
| 303 | if (proto === null) {
|
|---|
| 304 | // Object created with Object.create(null)
|
|---|
| 305 | c = null;
|
|---|
| 306 | } else {
|
|---|
| 307 | c = proto.constructor;
|
|---|
| 308 | if (!c) {
|
|---|
| 309 | throw new Error(
|
|---|
| 310 | "Serialization of objects with prototype without valid constructor property not possible"
|
|---|
| 311 | );
|
|---|
| 312 | }
|
|---|
| 313 | }
|
|---|
| 314 | const config = serializers.get(c);
|
|---|
| 315 |
|
|---|
| 316 | if (!config) {
|
|---|
| 317 | throw new Error(
|
|---|
| 318 | `No serializer registered for ${/** @type {Constructor} */ (c).name}`
|
|---|
| 319 | );
|
|---|
| 320 | }
|
|---|
| 321 | if (config === NOT_SERIALIZABLE) throw NOT_SERIALIZABLE;
|
|---|
| 322 |
|
|---|
| 323 | return /** @type {SerializerConfigWithSerializer} */ (config);
|
|---|
| 324 | }
|
|---|
| 325 |
|
|---|
| 326 | /**
|
|---|
| 327 | * Gets deserializer for.
|
|---|
| 328 | * @param {string} request request
|
|---|
| 329 | * @param {string} name name
|
|---|
| 330 | * @returns {ObjectSerializer} serializer
|
|---|
| 331 | */
|
|---|
| 332 | static getDeserializerFor(request, name) {
|
|---|
| 333 | const key = `${request}/${name}`;
|
|---|
| 334 | const serializer = serializerInversed.get(key);
|
|---|
| 335 |
|
|---|
| 336 | if (serializer === undefined) {
|
|---|
| 337 | throw new Error(`No deserializer registered for ${key}`);
|
|---|
| 338 | }
|
|---|
| 339 |
|
|---|
| 340 | return serializer;
|
|---|
| 341 | }
|
|---|
| 342 |
|
|---|
| 343 | /**
|
|---|
| 344 | * Get deserializer for without error.
|
|---|
| 345 | * @param {string} request request
|
|---|
| 346 | * @param {string} name name
|
|---|
| 347 | * @returns {ObjectSerializer | undefined} serializer
|
|---|
| 348 | */
|
|---|
| 349 | static _getDeserializerForWithoutError(request, name) {
|
|---|
| 350 | const key = `${request}/${name}`;
|
|---|
| 351 | const serializer = serializerInversed.get(key);
|
|---|
| 352 | return serializer;
|
|---|
| 353 | }
|
|---|
| 354 |
|
|---|
| 355 | /**
|
|---|
| 356 | * Serializes this instance into the provided serializer context.
|
|---|
| 357 | * @param {DeserializedType} data data
|
|---|
| 358 | * @param {Context} context context object
|
|---|
| 359 | * @returns {SerializedType | Promise<SerializedType> | null} serialized data
|
|---|
| 360 | */
|
|---|
| 361 | serialize(data, context) {
|
|---|
| 362 | /** @type {PrimitiveSerializableType[]} */
|
|---|
| 363 | let result = [CURRENT_VERSION];
|
|---|
| 364 | let currentPos = 0;
|
|---|
| 365 | /** @type {Map<ReferenceableItem, number>} */
|
|---|
| 366 | let referenceable = new Map();
|
|---|
| 367 | /**
|
|---|
| 368 | * Adds referenceable.
|
|---|
| 369 | * @param {ReferenceableItem} item referenceable item
|
|---|
| 370 | */
|
|---|
| 371 | const addReferenceable = (item) => {
|
|---|
| 372 | referenceable.set(item, currentPos++);
|
|---|
| 373 | };
|
|---|
| 374 | /** @type {Map<number, Buffer | [Buffer, Buffer] | Map<string, Buffer>>} */
|
|---|
| 375 | let bufferDedupeMap = new Map();
|
|---|
| 376 | /**
|
|---|
| 377 | * Returns deduped buffer.
|
|---|
| 378 | * @param {Buffer} buf buffer
|
|---|
| 379 | * @returns {Buffer} deduped buffer
|
|---|
| 380 | */
|
|---|
| 381 | const dedupeBuffer = (buf) => {
|
|---|
| 382 | const len = buf.length;
|
|---|
| 383 | const entry = bufferDedupeMap.get(len);
|
|---|
| 384 | if (entry === undefined) {
|
|---|
| 385 | bufferDedupeMap.set(len, buf);
|
|---|
| 386 | return buf;
|
|---|
| 387 | }
|
|---|
| 388 | if (Buffer.isBuffer(entry)) {
|
|---|
| 389 | if (len < 32) {
|
|---|
| 390 | if (buf.equals(entry)) {
|
|---|
| 391 | return entry;
|
|---|
| 392 | }
|
|---|
| 393 | bufferDedupeMap.set(len, [entry, buf]);
|
|---|
| 394 | return buf;
|
|---|
| 395 | }
|
|---|
| 396 | const hash = toHash(entry, this._hashFunction);
|
|---|
| 397 | /** @type {Map<string, Buffer>} */
|
|---|
| 398 | const newMap = new Map();
|
|---|
| 399 | newMap.set(hash, entry);
|
|---|
| 400 | bufferDedupeMap.set(len, newMap);
|
|---|
| 401 | const hashBuf = toHash(buf, this._hashFunction);
|
|---|
| 402 | if (hash === hashBuf) {
|
|---|
| 403 | return entry;
|
|---|
| 404 | }
|
|---|
| 405 | return buf;
|
|---|
| 406 | } else if (Array.isArray(entry)) {
|
|---|
| 407 | if (entry.length < 16) {
|
|---|
| 408 | for (const item of entry) {
|
|---|
| 409 | if (buf.equals(item)) {
|
|---|
| 410 | return item;
|
|---|
| 411 | }
|
|---|
| 412 | }
|
|---|
| 413 | entry.push(buf);
|
|---|
| 414 | return buf;
|
|---|
| 415 | }
|
|---|
| 416 | /** @type {Map<string, Buffer>} */
|
|---|
| 417 | const newMap = new Map();
|
|---|
| 418 | const hash = toHash(buf, this._hashFunction);
|
|---|
| 419 | /** @type {undefined | Buffer} */
|
|---|
| 420 | let found;
|
|---|
| 421 | for (const item of entry) {
|
|---|
| 422 | const itemHash = toHash(item, this._hashFunction);
|
|---|
| 423 | newMap.set(itemHash, item);
|
|---|
| 424 | if (found === undefined && itemHash === hash) found = item;
|
|---|
| 425 | }
|
|---|
| 426 | bufferDedupeMap.set(len, newMap);
|
|---|
| 427 | if (found === undefined) {
|
|---|
| 428 | newMap.set(hash, buf);
|
|---|
| 429 | return buf;
|
|---|
| 430 | }
|
|---|
| 431 | return found;
|
|---|
| 432 | }
|
|---|
| 433 | const hash = toHash(buf, this._hashFunction);
|
|---|
| 434 | const item = entry.get(hash);
|
|---|
| 435 | if (item !== undefined) {
|
|---|
| 436 | return item;
|
|---|
| 437 | }
|
|---|
| 438 | entry.set(hash, buf);
|
|---|
| 439 | return buf;
|
|---|
| 440 | };
|
|---|
| 441 | let currentPosTypeLookup = 0;
|
|---|
| 442 | /** @type {Map<ComplexSerializableType, number>} */
|
|---|
| 443 | let objectTypeLookup = new Map();
|
|---|
| 444 | /** @type {Set<ComplexSerializableType>} */
|
|---|
| 445 | const cycleStack = new Set();
|
|---|
| 446 | /**
|
|---|
| 447 | * Returns stack.
|
|---|
| 448 | * @param {ComplexSerializableType} item item to stack
|
|---|
| 449 | * @returns {string} stack
|
|---|
| 450 | */
|
|---|
| 451 | const stackToString = (item) => {
|
|---|
| 452 | const arr = [...cycleStack];
|
|---|
| 453 | arr.push(item);
|
|---|
| 454 | return arr
|
|---|
| 455 | .map((item) => {
|
|---|
| 456 | if (typeof item === "string") {
|
|---|
| 457 | if (item.length > 100) {
|
|---|
| 458 | return `String ${JSON.stringify(item.slice(0, 100)).slice(
|
|---|
| 459 | 0,
|
|---|
| 460 | -1
|
|---|
| 461 | )}..."`;
|
|---|
| 462 | }
|
|---|
| 463 | return `String ${JSON.stringify(item)}`;
|
|---|
| 464 | }
|
|---|
| 465 | try {
|
|---|
| 466 | const { request, name } = ObjectMiddleware.getSerializerFor(item);
|
|---|
| 467 | if (request) {
|
|---|
| 468 | return `${request}${name ? `.${name}` : ""}`;
|
|---|
| 469 | }
|
|---|
| 470 | } catch (_err) {
|
|---|
| 471 | // ignore -> fallback
|
|---|
| 472 | }
|
|---|
| 473 | if (typeof item === "object" && item !== null) {
|
|---|
| 474 | if (item.constructor) {
|
|---|
| 475 | if (item.constructor === Object) {
|
|---|
| 476 | return `Object { ${Object.keys(item).join(", ")} }`;
|
|---|
| 477 | }
|
|---|
| 478 | if (item.constructor === Map) {
|
|---|
| 479 | return `Map { ${/** @type {Map<EXPECTED_ANY, EXPECTED_ANY>} */ (item).size} items }`;
|
|---|
| 480 | }
|
|---|
| 481 | if (item.constructor === Array) {
|
|---|
| 482 | return `Array { ${/** @type {EXPECTED_ANY[]} */ (item).length} items }`;
|
|---|
| 483 | }
|
|---|
| 484 | if (item.constructor === Set) {
|
|---|
| 485 | return `Set { ${/** @type {Set<EXPECTED_ANY>} */ (item).size} items }`;
|
|---|
| 486 | }
|
|---|
| 487 | if (item.constructor === RegExp) {
|
|---|
| 488 | return /** @type {RegExp} */ (item).toString();
|
|---|
| 489 | }
|
|---|
| 490 | return `${item.constructor.name}`;
|
|---|
| 491 | }
|
|---|
| 492 | return `Object [null prototype] { ${Object.keys(item).join(
|
|---|
| 493 | ", "
|
|---|
| 494 | )} }`;
|
|---|
| 495 | }
|
|---|
| 496 | if (typeof item === "bigint") {
|
|---|
| 497 | return `BigInt ${item}n`;
|
|---|
| 498 | }
|
|---|
| 499 | try {
|
|---|
| 500 | return `${item}`;
|
|---|
| 501 | } catch (err) {
|
|---|
| 502 | return `(${/** @type {Error} */ (err).message})`;
|
|---|
| 503 | }
|
|---|
| 504 | })
|
|---|
| 505 | .join(" -> ");
|
|---|
| 506 | };
|
|---|
| 507 | /** @type {undefined | WeakSet<Error>} */
|
|---|
| 508 | let hasDebugInfoAttached;
|
|---|
| 509 | /** @type {ObjectSerializerContext} */
|
|---|
| 510 | let ctx = {
|
|---|
| 511 | write(value) {
|
|---|
| 512 | try {
|
|---|
| 513 | process(value);
|
|---|
| 514 | } catch (err) {
|
|---|
| 515 | if (err !== NOT_SERIALIZABLE) {
|
|---|
| 516 | if (hasDebugInfoAttached === undefined) {
|
|---|
| 517 | hasDebugInfoAttached = new WeakSet();
|
|---|
| 518 | }
|
|---|
| 519 | if (!hasDebugInfoAttached.has(/** @type {Error} */ (err))) {
|
|---|
| 520 | /** @type {Error} */
|
|---|
| 521 | (err).message += `\nwhile serializing ${stackToString(value)}`;
|
|---|
| 522 | hasDebugInfoAttached.add(/** @type {Error} */ (err));
|
|---|
| 523 | }
|
|---|
| 524 | }
|
|---|
| 525 | throw err;
|
|---|
| 526 | }
|
|---|
| 527 | },
|
|---|
| 528 | setCircularReference(ref) {
|
|---|
| 529 | addReferenceable(ref);
|
|---|
| 530 | },
|
|---|
| 531 | snapshot() {
|
|---|
| 532 | return {
|
|---|
| 533 | length: result.length,
|
|---|
| 534 | cycleStackSize: cycleStack.size,
|
|---|
| 535 | referenceableSize: referenceable.size,
|
|---|
| 536 | currentPos,
|
|---|
| 537 | objectTypeLookupSize: objectTypeLookup.size,
|
|---|
| 538 | currentPosTypeLookup
|
|---|
| 539 | };
|
|---|
| 540 | },
|
|---|
| 541 | rollback(snapshot) {
|
|---|
| 542 | result.length = snapshot.length;
|
|---|
| 543 | setSetSize(cycleStack, snapshot.cycleStackSize);
|
|---|
| 544 | setMapSize(referenceable, snapshot.referenceableSize);
|
|---|
| 545 | currentPos = snapshot.currentPos;
|
|---|
| 546 | setMapSize(objectTypeLookup, snapshot.objectTypeLookupSize);
|
|---|
| 547 | currentPosTypeLookup = snapshot.currentPosTypeLookup;
|
|---|
| 548 | },
|
|---|
| 549 | ...context
|
|---|
| 550 | };
|
|---|
| 551 | this.extendContext(ctx);
|
|---|
| 552 | /**
|
|---|
| 553 | * Processes the provided item.
|
|---|
| 554 | * @param {ComplexSerializableType} item item to serialize
|
|---|
| 555 | */
|
|---|
| 556 | const process = (item) => {
|
|---|
| 557 | if (Buffer.isBuffer(item)) {
|
|---|
| 558 | // check if we can emit a reference
|
|---|
| 559 | const ref = referenceable.get(item);
|
|---|
| 560 | if (ref !== undefined) {
|
|---|
| 561 | result.push(ESCAPE, ref - currentPos);
|
|---|
| 562 | return;
|
|---|
| 563 | }
|
|---|
| 564 | const alreadyUsedBuffer = dedupeBuffer(item);
|
|---|
| 565 | if (alreadyUsedBuffer !== item) {
|
|---|
| 566 | const ref = referenceable.get(alreadyUsedBuffer);
|
|---|
| 567 | if (ref !== undefined) {
|
|---|
| 568 | referenceable.set(item, ref);
|
|---|
| 569 | result.push(ESCAPE, ref - currentPos);
|
|---|
| 570 | return;
|
|---|
| 571 | }
|
|---|
| 572 | item = alreadyUsedBuffer;
|
|---|
| 573 | }
|
|---|
| 574 | addReferenceable(item);
|
|---|
| 575 |
|
|---|
| 576 | result.push(/** @type {Buffer} */ (item));
|
|---|
| 577 | } else if (item === ESCAPE) {
|
|---|
| 578 | result.push(ESCAPE, ESCAPE_ESCAPE_VALUE);
|
|---|
| 579 | } else if (
|
|---|
| 580 | typeof item === "object"
|
|---|
| 581 | // We don't have to check for null as ESCAPE is null and this has been checked before
|
|---|
| 582 | ) {
|
|---|
| 583 | // check if we can emit a reference
|
|---|
| 584 | const ref = referenceable.get(item);
|
|---|
| 585 | if (ref !== undefined) {
|
|---|
| 586 | result.push(ESCAPE, ref - currentPos);
|
|---|
| 587 | return;
|
|---|
| 588 | }
|
|---|
| 589 |
|
|---|
| 590 | if (cycleStack.has(item)) {
|
|---|
| 591 | throw new Error(
|
|---|
| 592 | "This is a circular references. To serialize circular references use 'setCircularReference' somewhere in the circle during serialize and deserialize."
|
|---|
| 593 | );
|
|---|
| 594 | }
|
|---|
| 595 |
|
|---|
| 596 | const { request, name, serializer } = ObjectMiddleware.getSerializerFor(
|
|---|
| 597 | /** @type {Constructor} */
|
|---|
| 598 | (item)
|
|---|
| 599 | );
|
|---|
| 600 | const key = `${request}/${name}`;
|
|---|
| 601 | const lastIndex = objectTypeLookup.get(key);
|
|---|
| 602 |
|
|---|
| 603 | if (lastIndex === undefined) {
|
|---|
| 604 | objectTypeLookup.set(key, currentPosTypeLookup++);
|
|---|
| 605 |
|
|---|
| 606 | result.push(ESCAPE, request, name);
|
|---|
| 607 | } else {
|
|---|
| 608 | result.push(ESCAPE, currentPosTypeLookup - lastIndex);
|
|---|
| 609 | }
|
|---|
| 610 |
|
|---|
| 611 | cycleStack.add(item);
|
|---|
| 612 |
|
|---|
| 613 | try {
|
|---|
| 614 | serializer.serialize(item, ctx);
|
|---|
| 615 | } finally {
|
|---|
| 616 | cycleStack.delete(item);
|
|---|
| 617 | }
|
|---|
| 618 |
|
|---|
| 619 | result.push(ESCAPE, ESCAPE_END_OBJECT);
|
|---|
| 620 |
|
|---|
| 621 | addReferenceable(item);
|
|---|
| 622 | } else if (typeof item === "string") {
|
|---|
| 623 | if (item.length > 1) {
|
|---|
| 624 | // short strings are shorter when not emitting a reference (this saves 1 byte per empty string)
|
|---|
| 625 | // check if we can emit a reference
|
|---|
| 626 | const ref = referenceable.get(item);
|
|---|
| 627 | if (ref !== undefined) {
|
|---|
| 628 | result.push(ESCAPE, ref - currentPos);
|
|---|
| 629 | return;
|
|---|
| 630 | }
|
|---|
| 631 | addReferenceable(item);
|
|---|
| 632 | }
|
|---|
| 633 |
|
|---|
| 634 | if (item.length > 102400 && context.logger) {
|
|---|
| 635 | context.logger.warn(
|
|---|
| 636 | `Serializing big strings (${Math.round(
|
|---|
| 637 | item.length / 1024
|
|---|
| 638 | )}kiB) impacts deserialization performance (consider using Buffer instead and decode when needed)`
|
|---|
| 639 | );
|
|---|
| 640 | }
|
|---|
| 641 |
|
|---|
| 642 | result.push(item);
|
|---|
| 643 | } else if (typeof item === "function") {
|
|---|
| 644 | if (!SerializerMiddleware.isLazy(item)) {
|
|---|
| 645 | throw new Error(`Unexpected function ${item}`);
|
|---|
| 646 | }
|
|---|
| 647 |
|
|---|
| 648 | /** @type {SerializedType | undefined} */
|
|---|
| 649 | const serializedData =
|
|---|
| 650 | SerializerMiddleware.getLazySerializedValue(item);
|
|---|
| 651 |
|
|---|
| 652 | if (serializedData !== undefined) {
|
|---|
| 653 | if (typeof serializedData === "function") {
|
|---|
| 654 | result.push(serializedData);
|
|---|
| 655 | } else {
|
|---|
| 656 | throw new Error("Not implemented");
|
|---|
| 657 | }
|
|---|
| 658 | } else if (SerializerMiddleware.isLazy(item, this)) {
|
|---|
| 659 | throw new Error("Not implemented");
|
|---|
| 660 | } else {
|
|---|
| 661 | const data =
|
|---|
| 662 | /** @type {() => PrimitiveSerializableType[] | Promise<PrimitiveSerializableType[]>} */
|
|---|
| 663 | (
|
|---|
| 664 | SerializerMiddleware.serializeLazy(item, (data) =>
|
|---|
| 665 | this.serialize([data], context)
|
|---|
| 666 | )
|
|---|
| 667 | );
|
|---|
| 668 | SerializerMiddleware.setLazySerializedValue(item, data);
|
|---|
| 669 | result.push(data);
|
|---|
| 670 | }
|
|---|
| 671 | } else if (item === undefined) {
|
|---|
| 672 | result.push(ESCAPE, ESCAPE_UNDEFINED);
|
|---|
| 673 | } else {
|
|---|
| 674 | result.push(item);
|
|---|
| 675 | }
|
|---|
| 676 | };
|
|---|
| 677 |
|
|---|
| 678 | try {
|
|---|
| 679 | for (const item of data) {
|
|---|
| 680 | process(item);
|
|---|
| 681 | }
|
|---|
| 682 | return result;
|
|---|
| 683 | } catch (err) {
|
|---|
| 684 | if (err === NOT_SERIALIZABLE) return null;
|
|---|
| 685 |
|
|---|
| 686 | throw err;
|
|---|
| 687 | } finally {
|
|---|
| 688 | // Get rid of these references to avoid leaking memory
|
|---|
| 689 | // This happens because the optimized code v8 generates
|
|---|
| 690 | // is optimized for our "ctx.write" method so it will reference
|
|---|
| 691 | // it from e. g. Dependency.prototype.serialize -(IC)-> ctx.write
|
|---|
| 692 | data =
|
|---|
| 693 | result =
|
|---|
| 694 | referenceable =
|
|---|
| 695 | bufferDedupeMap =
|
|---|
| 696 | objectTypeLookup =
|
|---|
| 697 | ctx =
|
|---|
| 698 | /** @type {EXPECTED_ANY} */
|
|---|
| 699 | (undefined);
|
|---|
| 700 | }
|
|---|
| 701 | }
|
|---|
| 702 |
|
|---|
| 703 | /**
|
|---|
| 704 | * Restores this instance from the provided deserializer context.
|
|---|
| 705 | * @param {SerializedType} data data
|
|---|
| 706 | * @param {Context} context context object
|
|---|
| 707 | * @returns {DeserializedType | Promise<DeserializedType>} deserialized data
|
|---|
| 708 | */
|
|---|
| 709 | deserialize(data, context) {
|
|---|
| 710 | let currentDataPos = 0;
|
|---|
| 711 | const read = () => {
|
|---|
| 712 | if (currentDataPos >= data.length) {
|
|---|
| 713 | throw new Error("Unexpected end of stream");
|
|---|
| 714 | }
|
|---|
| 715 |
|
|---|
| 716 | return data[currentDataPos++];
|
|---|
| 717 | };
|
|---|
| 718 |
|
|---|
| 719 | if (read() !== CURRENT_VERSION) {
|
|---|
| 720 | throw new Error("Version mismatch, serializer changed");
|
|---|
| 721 | }
|
|---|
| 722 |
|
|---|
| 723 | let currentPos = 0;
|
|---|
| 724 | /** @type {ReferenceableItem[]} */
|
|---|
| 725 | let referenceable = [];
|
|---|
| 726 | /**
|
|---|
| 727 | * Adds referenceable.
|
|---|
| 728 | * @param {ReferenceableItem} item referenceable item
|
|---|
| 729 | */
|
|---|
| 730 | const addReferenceable = (item) => {
|
|---|
| 731 | referenceable.push(item);
|
|---|
| 732 | currentPos++;
|
|---|
| 733 | };
|
|---|
| 734 | let currentPosTypeLookup = 0;
|
|---|
| 735 | /** @type {ObjectSerializer[]} */
|
|---|
| 736 | let objectTypeLookup = [];
|
|---|
| 737 | /** @type {ComplexSerializableType[]} */
|
|---|
| 738 | let result = [];
|
|---|
| 739 | /** @type {ObjectDeserializerContext} */
|
|---|
| 740 | let ctx = {
|
|---|
| 741 | read() {
|
|---|
| 742 | return decodeValue();
|
|---|
| 743 | },
|
|---|
| 744 | setCircularReference(ref) {
|
|---|
| 745 | addReferenceable(ref);
|
|---|
| 746 | },
|
|---|
| 747 | ...context
|
|---|
| 748 | };
|
|---|
| 749 | this.extendContext(ctx);
|
|---|
| 750 | /**
|
|---|
| 751 | * Decodes the provided value.
|
|---|
| 752 | * @returns {ComplexSerializableType} deserialize value
|
|---|
| 753 | */
|
|---|
| 754 | const decodeValue = () => {
|
|---|
| 755 | const item = read();
|
|---|
| 756 |
|
|---|
| 757 | if (item === ESCAPE) {
|
|---|
| 758 | const nextItem = read();
|
|---|
| 759 |
|
|---|
| 760 | if (nextItem === ESCAPE_ESCAPE_VALUE) {
|
|---|
| 761 | return ESCAPE;
|
|---|
| 762 | } else if (nextItem === ESCAPE_UNDEFINED) {
|
|---|
| 763 | // Nothing
|
|---|
| 764 | } else if (nextItem === ESCAPE_END_OBJECT) {
|
|---|
| 765 | throw new Error(
|
|---|
| 766 | `Unexpected end of object at position ${currentDataPos - 1}`
|
|---|
| 767 | );
|
|---|
| 768 | } else {
|
|---|
| 769 | const request = nextItem;
|
|---|
| 770 | /** @type {undefined | ObjectSerializer} */
|
|---|
| 771 | let serializer;
|
|---|
| 772 |
|
|---|
| 773 | if (typeof request === "number") {
|
|---|
| 774 | if (request < 0) {
|
|---|
| 775 | // relative reference
|
|---|
| 776 | return referenceable[currentPos + request];
|
|---|
| 777 | }
|
|---|
| 778 | serializer = objectTypeLookup[currentPosTypeLookup - request];
|
|---|
| 779 | } else {
|
|---|
| 780 | if (typeof request !== "string") {
|
|---|
| 781 | throw new Error(
|
|---|
| 782 | `Unexpected type (${typeof request}) of request ` +
|
|---|
| 783 | `at position ${currentDataPos - 1}`
|
|---|
| 784 | );
|
|---|
| 785 | }
|
|---|
| 786 | const name = /** @type {string} */ (read());
|
|---|
| 787 |
|
|---|
| 788 | serializer = ObjectMiddleware._getDeserializerForWithoutError(
|
|---|
| 789 | request,
|
|---|
| 790 | name
|
|---|
| 791 | );
|
|---|
| 792 |
|
|---|
| 793 | if (serializer === undefined) {
|
|---|
| 794 | if (request && !loadedRequests.has(request)) {
|
|---|
| 795 | let loaded = false;
|
|---|
| 796 | for (const [regExp, loader] of loaders) {
|
|---|
| 797 | if (regExp.test(request) && loader(request)) {
|
|---|
| 798 | loaded = true;
|
|---|
| 799 | break;
|
|---|
| 800 | }
|
|---|
| 801 | }
|
|---|
| 802 | if (!loaded) {
|
|---|
| 803 | require(request);
|
|---|
| 804 | }
|
|---|
| 805 |
|
|---|
| 806 | loadedRequests.add(request);
|
|---|
| 807 | }
|
|---|
| 808 |
|
|---|
| 809 | serializer = ObjectMiddleware.getDeserializerFor(request, name);
|
|---|
| 810 | }
|
|---|
| 811 |
|
|---|
| 812 | objectTypeLookup.push(serializer);
|
|---|
| 813 | currentPosTypeLookup++;
|
|---|
| 814 | }
|
|---|
| 815 | try {
|
|---|
| 816 | const item = serializer.deserialize(ctx);
|
|---|
| 817 | const end1 = read();
|
|---|
| 818 |
|
|---|
| 819 | if (end1 !== ESCAPE) {
|
|---|
| 820 | throw new Error("Expected end of object");
|
|---|
| 821 | }
|
|---|
| 822 |
|
|---|
| 823 | const end2 = read();
|
|---|
| 824 |
|
|---|
| 825 | if (end2 !== ESCAPE_END_OBJECT) {
|
|---|
| 826 | throw new Error("Expected end of object");
|
|---|
| 827 | }
|
|---|
| 828 |
|
|---|
| 829 | addReferenceable(item);
|
|---|
| 830 |
|
|---|
| 831 | return item;
|
|---|
| 832 | } catch (err) {
|
|---|
| 833 | // As this is only for error handling, we omit creating a Map for
|
|---|
| 834 | // faster access to this information, as this would affect performance
|
|---|
| 835 | // in the good case
|
|---|
| 836 | /** @type {undefined | [Constructor | null, SerializerConfig]} */
|
|---|
| 837 | let serializerEntry;
|
|---|
| 838 | for (const entry of serializers) {
|
|---|
| 839 | if (entry[1].serializer === serializer) {
|
|---|
| 840 | serializerEntry = entry;
|
|---|
| 841 | break;
|
|---|
| 842 | }
|
|---|
| 843 | }
|
|---|
| 844 | const name = !serializerEntry
|
|---|
| 845 | ? "unknown"
|
|---|
| 846 | : !serializerEntry[1].request
|
|---|
| 847 | ? /** @type {Constructor[]} */ (serializerEntry)[0].name
|
|---|
| 848 | : serializerEntry[1].name
|
|---|
| 849 | ? `${serializerEntry[1].request} ${serializerEntry[1].name}`
|
|---|
| 850 | : serializerEntry[1].request;
|
|---|
| 851 | /** @type {Error} */
|
|---|
| 852 | (err).message += `\n(during deserialization of ${name})`;
|
|---|
| 853 | throw err;
|
|---|
| 854 | }
|
|---|
| 855 | }
|
|---|
| 856 | } else if (typeof item === "string") {
|
|---|
| 857 | if (item.length > 1) {
|
|---|
| 858 | addReferenceable(item);
|
|---|
| 859 | }
|
|---|
| 860 |
|
|---|
| 861 | return item;
|
|---|
| 862 | } else if (Buffer.isBuffer(item)) {
|
|---|
| 863 | addReferenceable(item);
|
|---|
| 864 |
|
|---|
| 865 | return item;
|
|---|
| 866 | } else if (typeof item === "function") {
|
|---|
| 867 | return SerializerMiddleware.deserializeLazy(
|
|---|
| 868 | item,
|
|---|
| 869 | (data) =>
|
|---|
| 870 | /** @type {[DeserializedType]} */
|
|---|
| 871 | (this.deserialize(data, context))[0]
|
|---|
| 872 | );
|
|---|
| 873 | } else {
|
|---|
| 874 | return item;
|
|---|
| 875 | }
|
|---|
| 876 | };
|
|---|
| 877 |
|
|---|
| 878 | try {
|
|---|
| 879 | while (currentDataPos < data.length) {
|
|---|
| 880 | result.push(decodeValue());
|
|---|
| 881 | }
|
|---|
| 882 | return result;
|
|---|
| 883 | } finally {
|
|---|
| 884 | // Get rid of these references to avoid leaking memory
|
|---|
| 885 | // This happens because the optimized code v8 generates
|
|---|
| 886 | // is optimized for our "ctx.read" method so it will reference
|
|---|
| 887 | // it from e. g. Dependency.prototype.deserialize -(IC)-> ctx.read
|
|---|
| 888 | result =
|
|---|
| 889 | referenceable =
|
|---|
| 890 | data =
|
|---|
| 891 | objectTypeLookup =
|
|---|
| 892 | ctx =
|
|---|
| 893 | /** @type {EXPECTED_ANY} */
|
|---|
| 894 | (undefined);
|
|---|
| 895 | }
|
|---|
| 896 | }
|
|---|
| 897 | }
|
|---|
| 898 |
|
|---|
| 899 | module.exports = ObjectMiddleware;
|
|---|
| 900 | module.exports.NOT_SERIALIZABLE = NOT_SERIALIZABLE;
|
|---|