Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/commons/TypeSerializer.ts
1028 views
1
const Types = {
2
number: 'number',
3
string: 'string',
4
boolean: 'boolean',
5
object: 'object',
6
bigint: 'bigint',
7
NaN: 'NaN',
8
Infinity: 'Infinity',
9
NegativeInfinity: '-Infinity',
10
DateIso: 'DateIso',
11
Buffer64: 'Buffer64',
12
ArrayBuffer64: 'ArrayBuffer64',
13
RegExp: 'RegExp',
14
Map: 'Map',
15
Set: 'Set',
16
Error: 'Error',
17
};
18
19
export default class TypeSerializer {
20
public static errorTypes = new Map<string, { new (message?: string): Error }>();
21
private static isNodejs = typeof process !== 'undefined' && process.release.name === 'node';
22
23
public static parse(stringified: string, stackMarker = 'SERIALIZER'): any {
24
return JSON.parse(stringified, this.reviver.bind(this, stackMarker));
25
}
26
27
public static revive(object: any, objectKey?: string): any {
28
if (!object) return object;
29
const marker = 'SERIALIZER';
30
const revived = this.reviver(marker, objectKey, object);
31
32
if (revived !== object) {
33
if (revived instanceof Map) {
34
return new Map([...revived].map(([key, value]) => this.reviver(marker, key, value)));
35
}
36
if (revived instanceof Set) {
37
return new Set([...revived].map(value => this.reviver(marker, '', value)));
38
}
39
return revived;
40
}
41
42
if (object && typeof object === Types.object) {
43
if (Array.isArray(object)) {
44
object = object.map(x => this.revive(x));
45
}
46
const response = {};
47
for (const [key, value] of Object.entries(object)) {
48
response[key] = this.revive(value, key);
49
}
50
object = response;
51
}
52
return object;
53
}
54
55
public static stringify(object: any): string {
56
const final = TypeSerializer.replace(object);
57
return JSON.stringify(final);
58
}
59
60
public static replace(object: any): object {
61
if (!object) return object;
62
const replaced = this.replacer(null, object);
63
if (replaced !== object || (typeof replaced === 'object' && '__type' in replaced)) {
64
return replaced;
65
}
66
if (object && typeof object === Types.object) {
67
if (Array.isArray(object)) return object.map(x => this.replace(x));
68
const response = {};
69
for (const [key, value] of Object.entries(object)) {
70
response[key] = this.replace(value);
71
}
72
return response;
73
}
74
return object;
75
}
76
77
private static replacer(_: string, value: any): any {
78
if (value === null || value === undefined) return value;
79
80
if (Number.isNaN(value)) {
81
return { __type: Types.NaN };
82
}
83
84
if (value === Number.POSITIVE_INFINITY) {
85
return { __type: Types.Infinity };
86
}
87
88
if (value === Number.NEGATIVE_INFINITY) {
89
return { __type: Types.NegativeInfinity };
90
}
91
92
const type = typeof value;
93
if (type === Types.boolean || type === Types.string || type === Types.number) return value;
94
if (type === Types.bigint) {
95
return { __type: Types.bigint, value: value.toString() };
96
}
97
98
if (value instanceof Date) {
99
return { __type: Types.DateIso, value: value.toISOString() };
100
}
101
102
if (value instanceof RegExp) {
103
return { __type: Types.RegExp, value: [value.source, value.flags] };
104
}
105
106
if (value instanceof Error) {
107
const { name, message, stack, ...data } = value;
108
return { __type: Types.Error, value: { name, message, stack, ...data } };
109
}
110
111
if (value instanceof Map) {
112
return { __type: Types.Map, value: [...value.entries()] };
113
}
114
115
if (value instanceof Set) {
116
return { __type: Types.Set, value: [...value] };
117
}
118
119
if (this.isNodejs) {
120
if (value instanceof Buffer || Buffer.isBuffer(value)) {
121
return { __type: Types.Buffer64, value: value.toString('base64') };
122
}
123
} else {
124
// @ts-ignore
125
if (value instanceof DOMRect) {
126
return value.toJSON();
127
}
128
// @ts-ignore
129
if (value instanceof CSSStyleDeclaration) {
130
const isNumber = new RegExp(/^\d+$/);
131
const result = {};
132
for (const key of Object.keys(value)) {
133
if (isNumber.test(key)) continue;
134
result[key] = value[key];
135
}
136
return result;
137
}
138
139
if (ArrayBuffer.isView(value)) {
140
// @ts-ignore
141
const binary = new TextDecoder('utf8').decode(value.buffer);
142
return {
143
__type: Types.ArrayBuffer64,
144
value: globalThis.btoa(binary),
145
args: {
146
arrayType: value[Symbol.toStringTag],
147
byteOffset: value.byteOffset,
148
byteLength: value.byteLength,
149
},
150
};
151
}
152
if (value instanceof ArrayBuffer) {
153
// @ts-ignore
154
const binary = new TextDecoder('utf8').decode(value);
155
return {
156
__type: Types.ArrayBuffer64,
157
value: globalThis.btoa(binary),
158
};
159
}
160
}
161
162
if ('toJSON' in value) {
163
return value.toJSON();
164
}
165
166
return value;
167
}
168
169
private static reviver(stackMarker: string, key: string, entry: any): any {
170
if (!entry || !entry.__type) return entry;
171
172
const { value, __type: type } = entry;
173
174
if (type === Types.number || type === Types.string || type === Types.boolean) return value;
175
if (type === Types.bigint) return BigInt(value);
176
if (type === Types.NaN) return Number.NaN;
177
if (type === Types.Infinity) return Number.POSITIVE_INFINITY;
178
if (type === Types.NegativeInfinity) return Number.NEGATIVE_INFINITY;
179
if (type === Types.DateIso) return new Date(value);
180
if (type === Types.Buffer64 || type === Types.ArrayBuffer64) {
181
if (this.isNodejs) {
182
return Buffer.from(value, 'base64');
183
}
184
185
const decoded = globalThis.atob(value);
186
// @ts-ignore
187
const uint8Array = new TextEncoder().encode(decoded);
188
if (!entry.args) return uint8Array;
189
190
const { arrayType, byteOffset, byteLength } = entry.args;
191
192
return new globalThis[arrayType](uint8Array.buffer, byteOffset, byteLength);
193
}
194
if (type === Types.RegExp) return new RegExp(value[0], value[1]);
195
if (type === Types.Map) return new Map(value);
196
if (type === Types.Set) return new Set(value);
197
if (type === Types.Error) {
198
const { name, message, stack, ...data } = value;
199
let Constructor = this.errorTypes && this.errorTypes.get(name);
200
if (!Constructor) {
201
if (this.isNodejs) {
202
Constructor = global[name] || Error;
203
} else {
204
Constructor = globalThis[name] || Error;
205
}
206
}
207
208
const startStack = new Error('').stack.slice(8); // "Error: \n" is 8 chars
209
210
const e = new Constructor(message);
211
e.name = name;
212
Object.assign(e, data);
213
if (stack) {
214
e.stack = `${stack}\n${`------${stackMarker}`.padEnd(50, '-')}\n${startStack}`;
215
}
216
return e;
217
}
218
219
return entry;
220
}
221
}
222
223
export function registerSerializableErrorType(errorConstructor: {
224
new (message?: string): Error;
225
}): void {
226
TypeSerializer.errorTypes.set(errorConstructor.name, errorConstructor);
227
}
228
229
export const stringifiedTypeSerializerClass = `const Types = ${JSON.stringify(
230
Types,
231
)};\n${TypeSerializer.toString()}`;
232
233