codeflash 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -0
- package/bin/codeflash-setup.js +13 -0
- package/bin/codeflash.js +131 -0
- package/package.json +71 -6
- package/runtime/capture.js +707 -0
- package/runtime/comparator.js +406 -0
- package/runtime/compare-results.js +329 -0
- package/runtime/index.js +79 -0
- package/runtime/serializer.js +851 -0
- package/scripts/postinstall.js +265 -0
- package/index.js +0 -7
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codeflash Universal Serializer
|
|
3
|
+
*
|
|
4
|
+
* A robust serialization system for JavaScript values that:
|
|
5
|
+
* 1. Prefers V8 serialization (Node.js native) - fastest, handles all JS types
|
|
6
|
+
* 2. Falls back to msgpack with custom extensions (for Bun/browser environments)
|
|
7
|
+
*
|
|
8
|
+
* Supports:
|
|
9
|
+
* - All primitive types (null, undefined, boolean, number, string, bigint, symbol)
|
|
10
|
+
* - Special numbers (NaN, Infinity, -Infinity)
|
|
11
|
+
* - Objects, Arrays (including sparse arrays)
|
|
12
|
+
* - Map, Set, WeakMap references, WeakSet references
|
|
13
|
+
* - Date, RegExp, Error (and subclasses)
|
|
14
|
+
* - TypedArrays (Int8Array, Uint8Array, Float32Array, etc.)
|
|
15
|
+
* - ArrayBuffer, SharedArrayBuffer, DataView
|
|
16
|
+
* - Circular references
|
|
17
|
+
* - Functions (by reference/name only)
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* const { serialize, deserialize, getSerializerType } = require('./codeflash-serializer');
|
|
21
|
+
*
|
|
22
|
+
* const buffer = serialize(value);
|
|
23
|
+
* const restored = deserialize(buffer);
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// SERIALIZER DETECTION
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
let useV8 = false;
|
|
33
|
+
let v8Module = null;
|
|
34
|
+
|
|
35
|
+
// Try to load V8 module (available in Node.js)
|
|
36
|
+
try {
|
|
37
|
+
v8Module = require('v8');
|
|
38
|
+
// Verify serialize/deserialize are available
|
|
39
|
+
if (typeof v8Module.serialize === 'function' && typeof v8Module.deserialize === 'function') {
|
|
40
|
+
// Perform a self-test to verify V8 serialization works correctly
|
|
41
|
+
// This catches cases like Jest's VM context where V8 serialization
|
|
42
|
+
// produces data that deserializes incorrectly (Maps become plain objects)
|
|
43
|
+
const testMap = new Map([['__test__', 1]]);
|
|
44
|
+
const testBuffer = v8Module.serialize(testMap);
|
|
45
|
+
const testRestored = v8Module.deserialize(testBuffer);
|
|
46
|
+
|
|
47
|
+
if (testRestored instanceof Map && testRestored.get('__test__') === 1) {
|
|
48
|
+
useV8 = true;
|
|
49
|
+
} else {
|
|
50
|
+
// V8 serialization is broken in this environment (e.g., Jest)
|
|
51
|
+
useV8 = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
// V8 not available (Bun, browser, etc.)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Load msgpack as fallback
|
|
59
|
+
let msgpack = null;
|
|
60
|
+
try {
|
|
61
|
+
msgpack = require('@msgpack/msgpack');
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// msgpack not installed
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the serializer type being used.
|
|
68
|
+
* @returns {string} - 'v8' or 'msgpack'
|
|
69
|
+
*/
|
|
70
|
+
function getSerializerType() {
|
|
71
|
+
return useV8 ? 'v8' : 'msgpack';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// V8 SERIALIZATION (PRIMARY)
|
|
76
|
+
// ============================================================================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Serialize a value using V8's native serialization.
|
|
80
|
+
* This handles all JavaScript types including:
|
|
81
|
+
* - Primitives, Objects, Arrays
|
|
82
|
+
* - Map, Set, Date, RegExp, Error
|
|
83
|
+
* - TypedArrays, ArrayBuffer
|
|
84
|
+
* - Circular references
|
|
85
|
+
*
|
|
86
|
+
* @param {any} value - Value to serialize
|
|
87
|
+
* @returns {Buffer} - Serialized buffer
|
|
88
|
+
*/
|
|
89
|
+
function serializeV8(value) {
|
|
90
|
+
try {
|
|
91
|
+
return v8Module.serialize(value);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// V8 can't serialize some things (functions, symbols in some contexts)
|
|
94
|
+
// Fall back to wrapped serialization
|
|
95
|
+
return v8Module.serialize(wrapForV8(value));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Deserialize a V8-serialized buffer.
|
|
101
|
+
*
|
|
102
|
+
* @param {Buffer} buffer - Serialized buffer
|
|
103
|
+
* @returns {any} - Deserialized value
|
|
104
|
+
*/
|
|
105
|
+
function deserializeV8(buffer) {
|
|
106
|
+
const value = v8Module.deserialize(buffer);
|
|
107
|
+
return unwrapFromV8(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Wrap values that V8 can't serialize natively.
|
|
112
|
+
* V8 can't serialize: functions, symbols (in some cases)
|
|
113
|
+
*/
|
|
114
|
+
function wrapForV8(value, seen = new WeakMap()) {
|
|
115
|
+
if (value === null || value === undefined) return value;
|
|
116
|
+
|
|
117
|
+
const type = typeof value;
|
|
118
|
+
|
|
119
|
+
// Primitives that V8 handles
|
|
120
|
+
if (type === 'number' || type === 'string' || type === 'boolean' || type === 'bigint') {
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Symbols - wrap with marker
|
|
125
|
+
if (type === 'symbol') {
|
|
126
|
+
return { __codeflash_type__: 'Symbol', description: value.description };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Functions - wrap with marker
|
|
130
|
+
if (type === 'function') {
|
|
131
|
+
return {
|
|
132
|
+
__codeflash_type__: 'Function',
|
|
133
|
+
name: value.name || 'anonymous',
|
|
134
|
+
// Can't serialize function body reliably
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Objects
|
|
139
|
+
if (type === 'object') {
|
|
140
|
+
// Check for circular reference
|
|
141
|
+
if (seen.has(value)) {
|
|
142
|
+
return seen.get(value);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// V8 handles most objects natively
|
|
146
|
+
// Just need to recurse into arrays and plain objects to wrap nested functions/symbols
|
|
147
|
+
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
const wrapped = [];
|
|
150
|
+
seen.set(value, wrapped);
|
|
151
|
+
for (let i = 0; i < value.length; i++) {
|
|
152
|
+
if (i in value) {
|
|
153
|
+
wrapped[i] = wrapForV8(value[i], seen);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return wrapped;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// V8 handles these natively
|
|
160
|
+
if (value instanceof Date || value instanceof RegExp || value instanceof Error ||
|
|
161
|
+
value instanceof Map || value instanceof Set ||
|
|
162
|
+
ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Plain objects - recurse
|
|
167
|
+
const wrapped = {};
|
|
168
|
+
seen.set(value, wrapped);
|
|
169
|
+
for (const key of Object.keys(value)) {
|
|
170
|
+
wrapped[key] = wrapForV8(value[key], seen);
|
|
171
|
+
}
|
|
172
|
+
return wrapped;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Unwrap values that were wrapped for V8 serialization.
|
|
180
|
+
*/
|
|
181
|
+
function unwrapFromV8(value, seen = new WeakMap()) {
|
|
182
|
+
if (value === null || value === undefined) return value;
|
|
183
|
+
|
|
184
|
+
const type = typeof value;
|
|
185
|
+
|
|
186
|
+
if (type !== 'object') return value;
|
|
187
|
+
|
|
188
|
+
// Check for circular reference
|
|
189
|
+
if (seen.has(value)) {
|
|
190
|
+
return seen.get(value);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check for wrapped types
|
|
194
|
+
if (value.__codeflash_type__) {
|
|
195
|
+
switch (value.__codeflash_type__) {
|
|
196
|
+
case 'Symbol':
|
|
197
|
+
return Symbol(value.description);
|
|
198
|
+
case 'Function':
|
|
199
|
+
// Can't restore function body, return a placeholder
|
|
200
|
+
const fn = function() { throw new Error(`Deserialized function placeholder: ${value.name}`); };
|
|
201
|
+
Object.defineProperty(fn, 'name', { value: value.name });
|
|
202
|
+
return fn;
|
|
203
|
+
default:
|
|
204
|
+
// Unknown wrapped type, return as-is
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Arrays
|
|
210
|
+
if (Array.isArray(value)) {
|
|
211
|
+
const unwrapped = [];
|
|
212
|
+
seen.set(value, unwrapped);
|
|
213
|
+
for (let i = 0; i < value.length; i++) {
|
|
214
|
+
if (i in value) {
|
|
215
|
+
unwrapped[i] = unwrapFromV8(value[i], seen);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return unwrapped;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// V8 restores these natively
|
|
222
|
+
if (value instanceof Date || value instanceof RegExp || value instanceof Error ||
|
|
223
|
+
value instanceof Map || value instanceof Set ||
|
|
224
|
+
ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Plain objects - recurse
|
|
229
|
+
const unwrapped = {};
|
|
230
|
+
seen.set(value, unwrapped);
|
|
231
|
+
for (const key of Object.keys(value)) {
|
|
232
|
+
unwrapped[key] = unwrapFromV8(value[key], seen);
|
|
233
|
+
}
|
|
234
|
+
return unwrapped;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ============================================================================
|
|
238
|
+
// MSGPACK SERIALIZATION (FALLBACK)
|
|
239
|
+
// ============================================================================
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Extension type IDs for msgpack.
|
|
243
|
+
* Using negative IDs to avoid conflicts with user-defined extensions.
|
|
244
|
+
*/
|
|
245
|
+
const EXT_TYPES = {
|
|
246
|
+
UNDEFINED: 0x01,
|
|
247
|
+
NAN: 0x02,
|
|
248
|
+
INFINITY_POS: 0x03,
|
|
249
|
+
INFINITY_NEG: 0x04,
|
|
250
|
+
BIGINT: 0x05,
|
|
251
|
+
SYMBOL: 0x06,
|
|
252
|
+
DATE: 0x07,
|
|
253
|
+
REGEXP: 0x08,
|
|
254
|
+
ERROR: 0x09,
|
|
255
|
+
MAP: 0x0A,
|
|
256
|
+
SET: 0x0B,
|
|
257
|
+
INT8ARRAY: 0x10,
|
|
258
|
+
UINT8ARRAY: 0x11,
|
|
259
|
+
UINT8CLAMPEDARRAY: 0x12,
|
|
260
|
+
INT16ARRAY: 0x13,
|
|
261
|
+
UINT16ARRAY: 0x14,
|
|
262
|
+
INT32ARRAY: 0x15,
|
|
263
|
+
UINT32ARRAY: 0x16,
|
|
264
|
+
FLOAT32ARRAY: 0x17,
|
|
265
|
+
FLOAT64ARRAY: 0x18,
|
|
266
|
+
BIGINT64ARRAY: 0x19,
|
|
267
|
+
BIGUINT64ARRAY: 0x1A,
|
|
268
|
+
ARRAYBUFFER: 0x1B,
|
|
269
|
+
DATAVIEW: 0x1C,
|
|
270
|
+
FUNCTION: 0x1D,
|
|
271
|
+
CIRCULAR_REF: 0x1E,
|
|
272
|
+
SPARSE_ARRAY: 0x1F,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Create msgpack extension codec for JavaScript types.
|
|
277
|
+
*/
|
|
278
|
+
function createMsgpackCodec() {
|
|
279
|
+
const extensionCodec = new msgpack.ExtensionCodec();
|
|
280
|
+
|
|
281
|
+
// Undefined
|
|
282
|
+
extensionCodec.register({
|
|
283
|
+
type: EXT_TYPES.UNDEFINED,
|
|
284
|
+
encode: (value) => {
|
|
285
|
+
if (value === undefined) return new Uint8Array(0);
|
|
286
|
+
return null;
|
|
287
|
+
},
|
|
288
|
+
decode: () => undefined,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// NaN
|
|
292
|
+
extensionCodec.register({
|
|
293
|
+
type: EXT_TYPES.NAN,
|
|
294
|
+
encode: (value) => {
|
|
295
|
+
if (typeof value === 'number' && Number.isNaN(value)) return new Uint8Array(0);
|
|
296
|
+
return null;
|
|
297
|
+
},
|
|
298
|
+
decode: () => NaN,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Positive Infinity
|
|
302
|
+
extensionCodec.register({
|
|
303
|
+
type: EXT_TYPES.INFINITY_POS,
|
|
304
|
+
encode: (value) => {
|
|
305
|
+
if (value === Infinity) return new Uint8Array(0);
|
|
306
|
+
return null;
|
|
307
|
+
},
|
|
308
|
+
decode: () => Infinity,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Negative Infinity
|
|
312
|
+
extensionCodec.register({
|
|
313
|
+
type: EXT_TYPES.INFINITY_NEG,
|
|
314
|
+
encode: (value) => {
|
|
315
|
+
if (value === -Infinity) return new Uint8Array(0);
|
|
316
|
+
return null;
|
|
317
|
+
},
|
|
318
|
+
decode: () => -Infinity,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// BigInt
|
|
322
|
+
extensionCodec.register({
|
|
323
|
+
type: EXT_TYPES.BIGINT,
|
|
324
|
+
encode: (value) => {
|
|
325
|
+
if (typeof value === 'bigint') {
|
|
326
|
+
const str = value.toString();
|
|
327
|
+
return new TextEncoder().encode(str);
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
},
|
|
331
|
+
decode: (data) => {
|
|
332
|
+
const str = new TextDecoder().decode(data);
|
|
333
|
+
return BigInt(str);
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Symbol
|
|
338
|
+
extensionCodec.register({
|
|
339
|
+
type: EXT_TYPES.SYMBOL,
|
|
340
|
+
encode: (value) => {
|
|
341
|
+
if (typeof value === 'symbol') {
|
|
342
|
+
// Distinguish between undefined description and empty string
|
|
343
|
+
// Use a special marker for undefined description
|
|
344
|
+
const desc = value.description;
|
|
345
|
+
if (desc === undefined) {
|
|
346
|
+
return new TextEncoder().encode('\x00__UNDEF__');
|
|
347
|
+
}
|
|
348
|
+
return new TextEncoder().encode(desc);
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
},
|
|
352
|
+
decode: (data) => {
|
|
353
|
+
const description = new TextDecoder().decode(data);
|
|
354
|
+
// Check for undefined marker
|
|
355
|
+
if (description === '\x00__UNDEF__') {
|
|
356
|
+
return Symbol();
|
|
357
|
+
}
|
|
358
|
+
return Symbol(description);
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Note: Date is handled via marker objects in prepareForMsgpack/restoreFromMsgpack
|
|
363
|
+
// because msgpack's built-in timestamp extension doesn't properly handle NaN (Invalid Date)
|
|
364
|
+
|
|
365
|
+
// RegExp - use Object.prototype.toString for cross-context detection
|
|
366
|
+
extensionCodec.register({
|
|
367
|
+
type: EXT_TYPES.REGEXP,
|
|
368
|
+
encode: (value) => {
|
|
369
|
+
if (Object.prototype.toString.call(value) === '[object RegExp]') {
|
|
370
|
+
const obj = { source: value.source, flags: value.flags };
|
|
371
|
+
return msgpack.encode(obj);
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
},
|
|
375
|
+
decode: (data) => {
|
|
376
|
+
const obj = msgpack.decode(data);
|
|
377
|
+
return new RegExp(obj.source, obj.flags);
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Error - use Object.prototype.toString for cross-context detection
|
|
382
|
+
extensionCodec.register({
|
|
383
|
+
type: EXT_TYPES.ERROR,
|
|
384
|
+
encode: (value) => {
|
|
385
|
+
// Check for Error-like objects (cross-VM-context compatible)
|
|
386
|
+
if (Object.prototype.toString.call(value) === '[object Error]' ||
|
|
387
|
+
(value && value.name && value.message !== undefined && value.stack !== undefined)) {
|
|
388
|
+
const obj = {
|
|
389
|
+
name: value.name,
|
|
390
|
+
message: value.message,
|
|
391
|
+
stack: value.stack,
|
|
392
|
+
// Include custom properties
|
|
393
|
+
...Object.fromEntries(
|
|
394
|
+
Object.entries(value).filter(([k]) => !['name', 'message', 'stack'].includes(k))
|
|
395
|
+
),
|
|
396
|
+
};
|
|
397
|
+
return msgpack.encode(obj);
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
},
|
|
401
|
+
decode: (data) => {
|
|
402
|
+
const obj = msgpack.decode(data);
|
|
403
|
+
let ErrorClass = Error;
|
|
404
|
+
// Try to use the appropriate error class
|
|
405
|
+
const errorClasses = {
|
|
406
|
+
TypeError, RangeError, SyntaxError, ReferenceError,
|
|
407
|
+
URIError, EvalError, Error
|
|
408
|
+
};
|
|
409
|
+
if (obj.name in errorClasses) {
|
|
410
|
+
ErrorClass = errorClasses[obj.name];
|
|
411
|
+
}
|
|
412
|
+
const error = new ErrorClass(obj.message);
|
|
413
|
+
error.stack = obj.stack;
|
|
414
|
+
// Restore custom properties
|
|
415
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
416
|
+
if (!['name', 'message', 'stack'].includes(key)) {
|
|
417
|
+
error[key] = val;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return error;
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Function (limited - can't serialize body)
|
|
425
|
+
extensionCodec.register({
|
|
426
|
+
type: EXT_TYPES.FUNCTION,
|
|
427
|
+
encode: (value) => {
|
|
428
|
+
if (typeof value === 'function') {
|
|
429
|
+
return new TextEncoder().encode(value.name || 'anonymous');
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
},
|
|
433
|
+
decode: (data) => {
|
|
434
|
+
const name = new TextDecoder().decode(data);
|
|
435
|
+
const fn = function() { throw new Error(`Deserialized function placeholder: ${name}`); };
|
|
436
|
+
Object.defineProperty(fn, 'name', { value: name });
|
|
437
|
+
return fn;
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
return extensionCodec;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Singleton codec instance
|
|
445
|
+
let msgpackCodec = null;
|
|
446
|
+
|
|
447
|
+
function getMsgpackCodec() {
|
|
448
|
+
if (!msgpackCodec && msgpack) {
|
|
449
|
+
msgpackCodec = createMsgpackCodec();
|
|
450
|
+
}
|
|
451
|
+
return msgpackCodec;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Prepare a value for msgpack serialization.
|
|
456
|
+
* Handles types that need special treatment beyond extensions.
|
|
457
|
+
*/
|
|
458
|
+
function prepareForMsgpack(value, seen = new Map(), refId = { current: 0 }) {
|
|
459
|
+
if (value === null) return null;
|
|
460
|
+
// undefined needs special handling because msgpack converts it to null
|
|
461
|
+
if (value === undefined) return { __codeflash_undefined__: true };
|
|
462
|
+
|
|
463
|
+
const type = typeof value;
|
|
464
|
+
|
|
465
|
+
// Special number values that msgpack doesn't handle correctly
|
|
466
|
+
if (type === 'number') {
|
|
467
|
+
if (Number.isNaN(value)) return { __codeflash_nan__: true };
|
|
468
|
+
if (value === Infinity) return { __codeflash_infinity__: true };
|
|
469
|
+
if (value === -Infinity) return { __codeflash_neg_infinity__: true };
|
|
470
|
+
return value;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Primitives that msgpack handles or our extensions handle
|
|
474
|
+
if (type === 'string' || type === 'boolean' ||
|
|
475
|
+
type === 'bigint' || type === 'symbol' || type === 'function') {
|
|
476
|
+
return value;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (type !== 'object') return value;
|
|
480
|
+
|
|
481
|
+
// Check for circular reference
|
|
482
|
+
if (seen.has(value)) {
|
|
483
|
+
return { __codeflash_circular__: seen.get(value) };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Assign reference ID for potential circular refs
|
|
487
|
+
const id = refId.current++;
|
|
488
|
+
seen.set(value, id);
|
|
489
|
+
|
|
490
|
+
// Use toString for cross-VM-context type detection
|
|
491
|
+
const tag = Object.prototype.toString.call(value);
|
|
492
|
+
|
|
493
|
+
// Date - handle specially because msgpack's built-in timestamp doesn't handle NaN
|
|
494
|
+
if (tag === '[object Date]') {
|
|
495
|
+
const time = value.getTime();
|
|
496
|
+
// Store as marker object with the timestamp
|
|
497
|
+
// We use a string representation to preserve NaN
|
|
498
|
+
return {
|
|
499
|
+
__codeflash_date__: Number.isNaN(time) ? '__NAN__' : time,
|
|
500
|
+
__id__: id,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// RegExp, Error - handled by extensions
|
|
505
|
+
if (tag === '[object RegExp]' || tag === '[object Error]') {
|
|
506
|
+
return value;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Map (use toString for cross-VM-context)
|
|
510
|
+
if (tag === '[object Map]') {
|
|
511
|
+
const entries = [];
|
|
512
|
+
for (const [k, v] of value) {
|
|
513
|
+
entries.push([prepareForMsgpack(k, seen, refId), prepareForMsgpack(v, seen, refId)]);
|
|
514
|
+
}
|
|
515
|
+
return { __codeflash_map__: entries, __id__: id };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Set (use toString for cross-VM-context)
|
|
519
|
+
if (tag === '[object Set]') {
|
|
520
|
+
const values = [];
|
|
521
|
+
for (const v of value) {
|
|
522
|
+
values.push(prepareForMsgpack(v, seen, refId));
|
|
523
|
+
}
|
|
524
|
+
return { __codeflash_set__: values, __id__: id };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// TypedArrays (use ArrayBuffer.isView which works cross-context)
|
|
528
|
+
if (ArrayBuffer.isView(value) && tag !== '[object DataView]') {
|
|
529
|
+
return {
|
|
530
|
+
__codeflash_typedarray__: value.constructor.name,
|
|
531
|
+
data: Array.from(value),
|
|
532
|
+
__id__: id,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// DataView (use toString for cross-VM-context)
|
|
537
|
+
if (tag === '[object DataView]') {
|
|
538
|
+
return {
|
|
539
|
+
__codeflash_dataview__: true,
|
|
540
|
+
data: Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)),
|
|
541
|
+
__id__: id,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ArrayBuffer (use toString for cross-VM-context)
|
|
546
|
+
if (tag === '[object ArrayBuffer]') {
|
|
547
|
+
return {
|
|
548
|
+
__codeflash_arraybuffer__: true,
|
|
549
|
+
data: Array.from(new Uint8Array(value)),
|
|
550
|
+
__id__: id,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Arrays - always wrap in marker to preserve __id__ for circular references
|
|
555
|
+
// (msgpack doesn't preserve non-numeric properties on arrays)
|
|
556
|
+
if (Array.isArray(value)) {
|
|
557
|
+
const isSparse = value.length > 0 && Object.keys(value).length !== value.length;
|
|
558
|
+
if (isSparse) {
|
|
559
|
+
// Sparse array - store as object with indices
|
|
560
|
+
const sparse = { __codeflash_sparse_array__: true, length: value.length, elements: {}, __id__: id };
|
|
561
|
+
for (const key of Object.keys(value)) {
|
|
562
|
+
sparse.elements[key] = prepareForMsgpack(value[key], seen, refId);
|
|
563
|
+
}
|
|
564
|
+
return sparse;
|
|
565
|
+
}
|
|
566
|
+
// Dense array - wrap in marker object to preserve __id__
|
|
567
|
+
const elements = [];
|
|
568
|
+
for (let i = 0; i < value.length; i++) {
|
|
569
|
+
elements[i] = prepareForMsgpack(value[i], seen, refId);
|
|
570
|
+
}
|
|
571
|
+
return { __codeflash_array__: elements, __id__: id };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Plain objects
|
|
575
|
+
const obj = { __id__: id };
|
|
576
|
+
for (const key of Object.keys(value)) {
|
|
577
|
+
obj[key] = prepareForMsgpack(value[key], seen, refId);
|
|
578
|
+
}
|
|
579
|
+
return obj;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Restore a value after msgpack deserialization.
|
|
584
|
+
*/
|
|
585
|
+
function restoreFromMsgpack(value, refs = new Map()) {
|
|
586
|
+
if (value === null || value === undefined) return value;
|
|
587
|
+
|
|
588
|
+
const type = typeof value;
|
|
589
|
+
if (type !== 'object') return value;
|
|
590
|
+
|
|
591
|
+
// Built-in types that msgpack handles via extensions - return as-is
|
|
592
|
+
// These should NOT be treated as plain objects (use toString for cross-VM-context)
|
|
593
|
+
// Note: Date is handled via marker objects, so not included here
|
|
594
|
+
const tag = Object.prototype.toString.call(value);
|
|
595
|
+
if (tag === '[object RegExp]' || tag === '[object Error]') {
|
|
596
|
+
return value;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Special value markers
|
|
600
|
+
if (value.__codeflash_undefined__) return undefined;
|
|
601
|
+
if (value.__codeflash_nan__) return NaN;
|
|
602
|
+
if (value.__codeflash_infinity__) return Infinity;
|
|
603
|
+
if (value.__codeflash_neg_infinity__) return -Infinity;
|
|
604
|
+
|
|
605
|
+
// Date marker
|
|
606
|
+
if (value.__codeflash_date__ !== undefined) {
|
|
607
|
+
const time = value.__codeflash_date__ === '__NAN__' ? NaN : value.__codeflash_date__;
|
|
608
|
+
const date = new Date(time);
|
|
609
|
+
const id = value.__id__;
|
|
610
|
+
if (id !== undefined) refs.set(id, date);
|
|
611
|
+
return date;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Check for circular reference marker
|
|
615
|
+
if (value.__codeflash_circular__ !== undefined) {
|
|
616
|
+
return refs.get(value.__codeflash_circular__);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Store reference if this object has an ID
|
|
620
|
+
const id = value.__id__;
|
|
621
|
+
|
|
622
|
+
// Map
|
|
623
|
+
if (value.__codeflash_map__) {
|
|
624
|
+
const map = new Map();
|
|
625
|
+
if (id !== undefined) refs.set(id, map);
|
|
626
|
+
for (const [k, v] of value.__codeflash_map__) {
|
|
627
|
+
map.set(restoreFromMsgpack(k, refs), restoreFromMsgpack(v, refs));
|
|
628
|
+
}
|
|
629
|
+
return map;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Set
|
|
633
|
+
if (value.__codeflash_set__) {
|
|
634
|
+
const set = new Set();
|
|
635
|
+
if (id !== undefined) refs.set(id, set);
|
|
636
|
+
for (const v of value.__codeflash_set__) {
|
|
637
|
+
set.add(restoreFromMsgpack(v, refs));
|
|
638
|
+
}
|
|
639
|
+
return set;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// TypedArrays
|
|
643
|
+
if (value.__codeflash_typedarray__) {
|
|
644
|
+
const TypedArrayClass = globalThis[value.__codeflash_typedarray__];
|
|
645
|
+
if (TypedArrayClass) {
|
|
646
|
+
const arr = new TypedArrayClass(value.data);
|
|
647
|
+
if (id !== undefined) refs.set(id, arr);
|
|
648
|
+
return arr;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// DataView
|
|
653
|
+
if (value.__codeflash_dataview__) {
|
|
654
|
+
const buffer = new ArrayBuffer(value.data.length);
|
|
655
|
+
new Uint8Array(buffer).set(value.data);
|
|
656
|
+
const view = new DataView(buffer);
|
|
657
|
+
if (id !== undefined) refs.set(id, view);
|
|
658
|
+
return view;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ArrayBuffer
|
|
662
|
+
if (value.__codeflash_arraybuffer__) {
|
|
663
|
+
const buffer = new ArrayBuffer(value.data.length);
|
|
664
|
+
new Uint8Array(buffer).set(value.data);
|
|
665
|
+
if (id !== undefined) refs.set(id, buffer);
|
|
666
|
+
return buffer;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Dense array marker
|
|
670
|
+
if (value.__codeflash_array__) {
|
|
671
|
+
const arr = [];
|
|
672
|
+
if (id !== undefined) refs.set(id, arr);
|
|
673
|
+
const elements = value.__codeflash_array__;
|
|
674
|
+
for (let i = 0; i < elements.length; i++) {
|
|
675
|
+
arr[i] = restoreFromMsgpack(elements[i], refs);
|
|
676
|
+
}
|
|
677
|
+
return arr;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Sparse array
|
|
681
|
+
if (value.__codeflash_sparse_array__) {
|
|
682
|
+
const arr = new Array(value.length);
|
|
683
|
+
if (id !== undefined) refs.set(id, arr);
|
|
684
|
+
for (const [key, val] of Object.entries(value.elements)) {
|
|
685
|
+
arr[parseInt(key, 10)] = restoreFromMsgpack(val, refs);
|
|
686
|
+
}
|
|
687
|
+
return arr;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Arrays (legacy - shouldn't happen with new format, but keep for safety)
|
|
691
|
+
if (Array.isArray(value)) {
|
|
692
|
+
const arr = [];
|
|
693
|
+
if (id !== undefined) refs.set(id, arr);
|
|
694
|
+
for (let i = 0; i < value.length; i++) {
|
|
695
|
+
if (i in value) {
|
|
696
|
+
arr[i] = restoreFromMsgpack(value[i], refs);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return arr;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Plain objects - remove __id__ from result
|
|
703
|
+
const obj = {};
|
|
704
|
+
if (id !== undefined) refs.set(id, obj);
|
|
705
|
+
for (const [key, val] of Object.entries(value)) {
|
|
706
|
+
if (key !== '__id__') {
|
|
707
|
+
obj[key] = restoreFromMsgpack(val, refs);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return obj;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Serialize a value using msgpack with extensions.
|
|
715
|
+
*
|
|
716
|
+
* @param {any} value - Value to serialize
|
|
717
|
+
* @returns {Buffer} - Serialized buffer
|
|
718
|
+
*/
|
|
719
|
+
function serializeMsgpack(value) {
|
|
720
|
+
if (!msgpack) {
|
|
721
|
+
throw new Error('msgpack not available and V8 serialization not available');
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const codec = getMsgpackCodec();
|
|
725
|
+
const prepared = prepareForMsgpack(value);
|
|
726
|
+
const encoded = msgpack.encode(prepared, { extensionCodec: codec });
|
|
727
|
+
return Buffer.from(encoded);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Deserialize a msgpack-serialized buffer.
|
|
732
|
+
*
|
|
733
|
+
* @param {Buffer|Uint8Array} buffer - Serialized buffer
|
|
734
|
+
* @returns {any} - Deserialized value
|
|
735
|
+
*/
|
|
736
|
+
function deserializeMsgpack(buffer) {
|
|
737
|
+
if (!msgpack) {
|
|
738
|
+
throw new Error('msgpack not available');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const codec = getMsgpackCodec();
|
|
742
|
+
const decoded = msgpack.decode(buffer, { extensionCodec: codec });
|
|
743
|
+
return restoreFromMsgpack(decoded);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// ============================================================================
|
|
747
|
+
// PUBLIC API
|
|
748
|
+
// ============================================================================
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Serialize a value using the best available method.
|
|
752
|
+
* Prefers V8 serialization, falls back to msgpack.
|
|
753
|
+
*
|
|
754
|
+
* @param {any} value - Value to serialize
|
|
755
|
+
* @returns {Buffer} - Serialized buffer with format marker
|
|
756
|
+
*/
|
|
757
|
+
function serialize(value) {
|
|
758
|
+
// Add a format marker byte at the start
|
|
759
|
+
// 0x01 = V8, 0x02 = msgpack
|
|
760
|
+
if (useV8) {
|
|
761
|
+
const serialized = serializeV8(value);
|
|
762
|
+
const result = Buffer.allocUnsafe(serialized.length + 1);
|
|
763
|
+
result[0] = 0x01;
|
|
764
|
+
serialized.copy(result, 1);
|
|
765
|
+
return result;
|
|
766
|
+
} else {
|
|
767
|
+
const serialized = serializeMsgpack(value);
|
|
768
|
+
const result = Buffer.allocUnsafe(serialized.length + 1);
|
|
769
|
+
result[0] = 0x02;
|
|
770
|
+
serialized.copy(result, 1);
|
|
771
|
+
return result;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Deserialize a buffer that was serialized with serialize().
|
|
777
|
+
* Automatically detects the format from the marker byte.
|
|
778
|
+
*
|
|
779
|
+
* @param {Buffer|Uint8Array} buffer - Serialized buffer
|
|
780
|
+
* @returns {any} - Deserialized value
|
|
781
|
+
*/
|
|
782
|
+
function deserialize(buffer) {
|
|
783
|
+
if (!buffer || buffer.length === 0) {
|
|
784
|
+
throw new Error('Empty buffer cannot be deserialized');
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const format = buffer[0];
|
|
788
|
+
const data = buffer.slice(1);
|
|
789
|
+
|
|
790
|
+
if (format === 0x01) {
|
|
791
|
+
// V8 format
|
|
792
|
+
if (!useV8) {
|
|
793
|
+
throw new Error('Buffer was serialized with V8 but V8 is not available');
|
|
794
|
+
}
|
|
795
|
+
return deserializeV8(data);
|
|
796
|
+
} else if (format === 0x02) {
|
|
797
|
+
// msgpack format
|
|
798
|
+
return deserializeMsgpack(data);
|
|
799
|
+
} else {
|
|
800
|
+
throw new Error(`Unknown serialization format: ${format}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Force serialization using a specific method.
|
|
806
|
+
* Useful for testing or cross-environment compatibility.
|
|
807
|
+
*/
|
|
808
|
+
const serializeWith = {
|
|
809
|
+
v8: useV8 ? (value) => {
|
|
810
|
+
const serialized = serializeV8(value);
|
|
811
|
+
const result = Buffer.allocUnsafe(serialized.length + 1);
|
|
812
|
+
result[0] = 0x01;
|
|
813
|
+
serialized.copy(result, 1);
|
|
814
|
+
return result;
|
|
815
|
+
} : null,
|
|
816
|
+
|
|
817
|
+
msgpack: msgpack ? (value) => {
|
|
818
|
+
const serialized = serializeMsgpack(value);
|
|
819
|
+
const result = Buffer.allocUnsafe(serialized.length + 1);
|
|
820
|
+
result[0] = 0x02;
|
|
821
|
+
serialized.copy(result, 1);
|
|
822
|
+
return result;
|
|
823
|
+
} : null,
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
// ============================================================================
|
|
827
|
+
// EXPORTS
|
|
828
|
+
// ============================================================================
|
|
829
|
+
|
|
830
|
+
module.exports = {
|
|
831
|
+
// Main API
|
|
832
|
+
serialize,
|
|
833
|
+
deserialize,
|
|
834
|
+
getSerializerType,
|
|
835
|
+
|
|
836
|
+
// Force specific serializer
|
|
837
|
+
serializeWith,
|
|
838
|
+
|
|
839
|
+
// Low-level (for testing)
|
|
840
|
+
serializeV8: useV8 ? serializeV8 : null,
|
|
841
|
+
deserializeV8: useV8 ? deserializeV8 : null,
|
|
842
|
+
serializeMsgpack: msgpack ? serializeMsgpack : null,
|
|
843
|
+
deserializeMsgpack: msgpack ? deserializeMsgpack : null,
|
|
844
|
+
|
|
845
|
+
// Feature detection
|
|
846
|
+
hasV8: useV8,
|
|
847
|
+
hasMsgpack: !!msgpack,
|
|
848
|
+
|
|
849
|
+
// Extension types (for reference)
|
|
850
|
+
EXT_TYPES,
|
|
851
|
+
};
|