UNPKG

5.76 kBJavaScriptView Raw
1// @flow
2import v8 from 'v8';
3
4const nameToCtor: Map<string, Class<*>> = new Map();
5const ctorToName: Map<Class<*>, string> = new Map();
6
7export function registerSerializableClass(name: string, ctor: Class<*>) {
8 if (nameToCtor.has(name)) {
9 throw new Error('Name already registered with serializer');
10 }
11
12 if (ctorToName.has(ctor)) {
13 throw new Error('Class already registered with serializer');
14 }
15
16 nameToCtor.set(name, ctor);
17 ctorToName.set(ctor, name);
18}
19
20export function unregisterSerializableClass(name: string, ctor: Class<*>) {
21 if (nameToCtor.get(name) === ctor) {
22 nameToCtor.delete(name);
23 }
24
25 if (ctorToName.get(ctor) === name) {
26 ctorToName.delete(ctor);
27 }
28}
29
30function shallowCopy(object: any) {
31 if (object && typeof object === 'object') {
32 if (Array.isArray(object)) {
33 return [...object];
34 }
35
36 if (object instanceof Map) {
37 return new Map(object);
38 }
39
40 if (object instanceof Set) {
41 return new Set(object);
42 }
43
44 return Object.create(
45 Object.getPrototypeOf(object),
46 Object.getOwnPropertyDescriptors(object),
47 );
48 }
49
50 return object;
51}
52
53function isBuffer(object) {
54 return (
55 object.buffer instanceof ArrayBuffer ||
56 object.buffer instanceof SharedArrayBuffer
57 );
58}
59
60function mapObject(object: any, fn: (val: any) => any, preOrder = false): any {
61 let cache = new Map();
62 let memo = new Map();
63
64 // Memoize the passed function to ensure it always returns the exact same
65 // output by reference for the same input. This is important to maintain
66 // reference integrity when deserializing rather than cloning.
67 let memoizedFn = (val: any) => {
68 let res = memo.get(val);
69 if (res == null) {
70 res = fn(val);
71 memo.set(val, res);
72 }
73
74 return res;
75 };
76
77 let walk = (object: any, shouldCopy = false) => {
78 // Check the cache first, both for performance and cycle detection.
79 if (cache.has(object)) {
80 return cache.get(object);
81 }
82
83 let result = object;
84 cache.set(object, result);
85
86 let processKey = (key: any, value: any) => {
87 let newValue = value;
88 if (preOrder && value && typeof value === 'object') {
89 newValue = memoizedFn(value);
90 }
91
92 // Recursively walk the children
93 if (newValue && typeof newValue === 'object' && newValue.$$raw !== true) {
94 newValue = walk(newValue, newValue === value);
95 }
96
97 if (!preOrder && newValue && typeof newValue === 'object') {
98 newValue = memoizedFn(newValue);
99 }
100
101 if (newValue !== value) {
102 // Copy on write. We only need to do this when serializing, not deserializing.
103 if (object === result && preOrder && shouldCopy) {
104 result = shallowCopy(object);
105 cache.set(object, result);
106 }
107
108 // Replace the key with the new value
109 if (result instanceof Map) {
110 result.set(key, newValue);
111 } else if (result instanceof Set) {
112 // TODO: do we care about iteration order??
113 result.delete(value);
114 result.add(newValue);
115 } else {
116 result[key] = newValue;
117 }
118 }
119 };
120
121 // Iterate in various ways depending on type.
122 if (Array.isArray(object)) {
123 for (let i = 0; i < object.length; i++) {
124 processKey(i, object[i]);
125 }
126 } else if (object instanceof Map || object instanceof Set) {
127 for (let [key, val] of object.entries()) {
128 processKey(key, val);
129 }
130 } else if (!isBuffer(object)) {
131 for (let key in object) {
132 processKey(key, object[key]);
133 }
134 }
135
136 return result;
137 };
138
139 let mapped = memoizedFn(object);
140 if (mapped && typeof mapped === 'object' && mapped.$$raw !== true) {
141 return walk(mapped, mapped === object);
142 }
143
144 return mapped;
145}
146
147export function prepareForSerialization(object: any) {
148 return mapObject(
149 object,
150 value => {
151 // Add a $$type property with the name of this class, if any is registered.
152 if (
153 value &&
154 typeof value === 'object' &&
155 typeof value.constructor === 'function'
156 ) {
157 let type = ctorToName.get(value.constructor);
158 if (type != null) {
159 let serialized = value;
160 let raw = false;
161 if (value && typeof value.serialize === 'function') {
162 // If the object has a serialize method, call it
163 serialized = value.serialize();
164 raw = (serialized && serialized.$$raw) ?? true;
165 if (serialized) {
166 delete serialized.$$raw;
167 }
168 }
169
170 return {
171 $$type: type,
172 $$raw: raw,
173 value: {...serialized},
174 };
175 }
176 }
177
178 return value;
179 },
180 true,
181 );
182}
183
184export function restoreDeserializedObject(object: any) {
185 return mapObject(object, value => {
186 // If the value has a $$type property, use it to restore the object type
187 if (value && value.$$type) {
188 let ctor = nameToCtor.get(value.$$type);
189 if (ctor == null) {
190 throw new Error(
191 `Expected constructor ${value.$$type} to be registered with serializer to deserialize`,
192 );
193 }
194
195 if (typeof ctor.deserialize === 'function') {
196 return ctor.deserialize(value.value);
197 }
198
199 value = value.value;
200 Object.setPrototypeOf(value, ctor.prototype);
201 }
202
203 return value;
204 });
205}
206
207export function serialize(object: any): Buffer {
208 let mapped = prepareForSerialization(object);
209 // $FlowFixMe - flow doesn't know about this method yet
210 return v8.serialize(mapped);
211}
212
213export function deserialize(buffer: Buffer): any {
214 // $FlowFixMe - flow doesn't know about this method yet
215 let obj = v8.deserialize(buffer);
216 return restoreDeserializedObject(obj);
217}