UNPKG

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