first commit

This commit is contained in:
monjack
2025-06-20 18:01:48 +08:00
commit 6daa6d65c1
24611 changed files with 2512443 additions and 0 deletions

View File

@ -0,0 +1,42 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
/** @typedef {import("./ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
/** @typedef {import("./ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
/** @typedef {Error & { cause: unknown, errors: EXPECTED_ANY[] }} AggregateError */
class AggregateErrorSerializer {
/**
* @param {AggregateError} obj error
* @param {ObjectSerializerContext} context context
*/
serialize(obj, context) {
context.write(obj.errors);
context.write(obj.message);
context.write(obj.stack);
context.write(obj.cause);
}
/**
* @param {ObjectDeserializerContext} context context
* @returns {AggregateError} error
*/
deserialize(context) {
const errors = context.read();
// @ts-expect-error ES2018 doesn't `AggregateError`, but it can be used by developers
// eslint-disable-next-line n/no-unsupported-features/es-builtins, n/no-unsupported-features/es-syntax
const err = new AggregateError(errors);
err.message = context.read();
err.stack = context.read();
err.cause = context.read();
return err;
}
}
module.exports = AggregateErrorSerializer;

View File

@ -0,0 +1,38 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
/** @typedef {import("./ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
/** @typedef {import("./ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
class ArraySerializer {
/**
* @template T
* @param {T[]} array array
* @param {ObjectSerializerContext} context context
*/
serialize(array, context) {
context.write(array.length);
for (const item of array) context.write(item);
}
/**
* @template T
* @param {ObjectDeserializerContext} context context
* @returns {T[]} array
*/
deserialize(context) {
/** @type {number} */
const length = context.read();
/** @type {T[]} */
const array = [];
for (let i = 0; i < length; i++) {
array.push(context.read());
}
return array;
}
}
module.exports = ArraySerializer;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
/** @typedef {import("./ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
/** @typedef {import("./ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
class DateObjectSerializer {
/**
* @param {Date} obj date
* @param {ObjectSerializerContext} context context
*/
serialize(obj, context) {
context.write(obj.getTime());
}
/**
* @param {ObjectDeserializerContext} context context
* @returns {Date} date
*/
deserialize(context) {
return new Date(context.read());
}
}
module.exports = DateObjectSerializer;

View File

@ -0,0 +1,49 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
/** @typedef {import("./ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
/** @typedef {import("./ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
/** @typedef {Error & { cause?: unknown }} ErrorWithCause */
class ErrorObjectSerializer {
/**
* @param {ErrorConstructor | EvalErrorConstructor | RangeErrorConstructor | ReferenceErrorConstructor | SyntaxErrorConstructor | TypeErrorConstructor} Type error type
*/
constructor(Type) {
this.Type = Type;
}
/**
* @param {Error | EvalError | RangeError | ReferenceError | SyntaxError | TypeError} obj error
* @param {ObjectSerializerContext} context context
*/
serialize(obj, context) {
context.write(obj.message);
context.write(obj.stack);
context.write(
/** @type {ErrorWithCause} */
(obj).cause
);
}
/**
* @param {ObjectDeserializerContext} context context
* @returns {Error | EvalError | RangeError | ReferenceError | SyntaxError | TypeError} error
*/
deserialize(context) {
const err = new this.Type();
err.message = context.read();
err.stack = context.read();
/** @type {ErrorWithCause} */
(err).cause = context.read();
return err;
}
}
module.exports = ErrorObjectSerializer;

View File

@ -0,0 +1,757 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
const { constants } = require("buffer");
const { pipeline } = require("stream");
const {
createBrotliCompress,
createBrotliDecompress,
createGzip,
createGunzip,
constants: zConstants
} = require("zlib");
const { DEFAULTS } = require("../config/defaults");
const createHash = require("../util/createHash");
const { dirname, join, mkdirp } = require("../util/fs");
const memoize = require("../util/memoize");
const SerializerMiddleware = require("./SerializerMiddleware");
/** @typedef {typeof import("../util/Hash")} Hash */
/** @typedef {import("../util/fs").IStats} IStats */
/** @typedef {import("../util/fs").IntermediateFileSystem} IntermediateFileSystem */
/** @typedef {import("./types").BufferSerializableType} BufferSerializableType */
/*
Format:
File -> Header Section*
Version -> u32
AmountOfSections -> u32
SectionSize -> i32 (if less than zero represents lazy value)
Header -> Version AmountOfSections SectionSize*
Buffer -> n bytes
Section -> Buffer
*/
// "wpc" + 1 in little-endian
const VERSION = 0x01637077;
const WRITE_LIMIT_TOTAL = 0x7fff0000;
const WRITE_LIMIT_CHUNK = 511 * 1024 * 1024;
/**
* @param {Buffer[]} buffers buffers
* @param {string | Hash} hashFunction hash function to use
* @returns {string} hash
*/
const hashForName = (buffers, hashFunction) => {
const hash = createHash(hashFunction);
for (const buf of buffers) hash.update(buf);
return /** @type {string} */ (hash.digest("hex"));
};
const COMPRESSION_CHUNK_SIZE = 100 * 1024 * 1024;
const DECOMPRESSION_CHUNK_SIZE = 100 * 1024 * 1024;
/** @type {(buffer: Buffer, value: number, offset: number) => void} */
const writeUInt64LE = Buffer.prototype.writeBigUInt64LE
? (buf, value, offset) => {
buf.writeBigUInt64LE(BigInt(value), offset);
}
: (buf, value, offset) => {
const low = value % 0x100000000;
const high = (value - low) / 0x100000000;
buf.writeUInt32LE(low, offset);
buf.writeUInt32LE(high, offset + 4);
};
/** @type {(buffer: Buffer, offset: number) => void} */
const readUInt64LE = Buffer.prototype.readBigUInt64LE
? (buf, offset) => Number(buf.readBigUInt64LE(offset))
: (buf, offset) => {
const low = buf.readUInt32LE(offset);
const high = buf.readUInt32LE(offset + 4);
return high * 0x100000000 + low;
};
/** @typedef {Promise<void | void[]>} BackgroundJob */
/**
* @typedef {object} SerializeResult
* @property {string | false} name
* @property {number} size
* @property {BackgroundJob=} backgroundJob
*/
/** @typedef {{ name: string, size: number }} LazyOptions */
/**
* @typedef {import("./SerializerMiddleware").LazyFunction<BufferSerializableType[], Buffer, FileMiddleware, LazyOptions>} LazyFunction
*/
/**
* @param {FileMiddleware} middleware this
* @param {(BufferSerializableType | LazyFunction)[]} data data to be serialized
* @param {string | boolean} name file base name
* @param {(name: string | false, buffers: Buffer[], size: number) => Promise<void>} writeFile writes a file
* @param {string | Hash} hashFunction hash function to use
* @returns {Promise<SerializeResult>} resulting file pointer and promise
*/
const serialize = async (
middleware,
data,
name,
writeFile,
hashFunction = DEFAULTS.HASH_FUNCTION
) => {
/** @type {(Buffer[] | Buffer | Promise<SerializeResult>)[]} */
const processedData = [];
/** @type {WeakMap<SerializeResult, LazyFunction>} */
const resultToLazy = new WeakMap();
/** @type {Buffer[] | undefined} */
let lastBuffers;
for (const item of await data) {
if (typeof item === "function") {
if (!SerializerMiddleware.isLazy(item))
throw new Error("Unexpected function");
if (!SerializerMiddleware.isLazy(item, middleware)) {
throw new Error(
"Unexpected lazy value with non-this target (can't pass through lazy values)"
);
}
lastBuffers = undefined;
const serializedInfo = SerializerMiddleware.getLazySerializedValue(item);
if (serializedInfo) {
if (typeof serializedInfo === "function") {
throw new Error(
"Unexpected lazy value with non-this target (can't pass through lazy values)"
);
} else {
processedData.push(serializedInfo);
}
} else {
const content = item();
if (content) {
const options = SerializerMiddleware.getLazyOptions(item);
processedData.push(
serialize(
middleware,
/** @type {BufferSerializableType[]} */
(content),
(options && options.name) || true,
writeFile,
hashFunction
).then(result => {
/** @type {LazyOptions} */
(item.options).size = result.size;
resultToLazy.set(result, item);
return result;
})
);
} else {
throw new Error(
"Unexpected falsy value returned by lazy value function"
);
}
}
} else if (item) {
if (lastBuffers) {
lastBuffers.push(item);
} else {
lastBuffers = [item];
processedData.push(lastBuffers);
}
} else {
throw new Error("Unexpected falsy value in items array");
}
}
/** @type {BackgroundJob[]} */
const backgroundJobs = [];
const resolvedData = (await Promise.all(processedData)).map(item => {
if (Array.isArray(item) || Buffer.isBuffer(item)) return item;
backgroundJobs.push(
/** @type {BackgroundJob} */
(item.backgroundJob)
);
// create pointer buffer from size and name
const name = /** @type {string} */ (item.name);
const nameBuffer = Buffer.from(name);
const buf = Buffer.allocUnsafe(8 + nameBuffer.length);
writeUInt64LE(buf, item.size, 0);
nameBuffer.copy(buf, 8, 0);
const lazy =
/** @type {LazyFunction} */
(resultToLazy.get(item));
SerializerMiddleware.setLazySerializedValue(lazy, buf);
return buf;
});
/** @type {number[]} */
const lengths = [];
for (const item of resolvedData) {
if (Array.isArray(item)) {
let l = 0;
for (const b of item) l += b.length;
while (l > 0x7fffffff) {
lengths.push(0x7fffffff);
l -= 0x7fffffff;
}
lengths.push(l);
} else if (item) {
lengths.push(-item.length);
} else {
throw new Error(`Unexpected falsy value in resolved data ${item}`);
}
}
const header = Buffer.allocUnsafe(8 + lengths.length * 4);
header.writeUInt32LE(VERSION, 0);
header.writeUInt32LE(lengths.length, 4);
for (let i = 0; i < lengths.length; i++) {
header.writeInt32LE(lengths[i], 8 + i * 4);
}
/** @type {Buffer[]} */
const buf = [header];
for (const item of resolvedData) {
if (Array.isArray(item)) {
for (const b of item) buf.push(b);
} else if (item) {
buf.push(item);
}
}
if (name === true) {
name = hashForName(buf, hashFunction);
}
let size = 0;
for (const b of buf) size += b.length;
backgroundJobs.push(writeFile(name, buf, size));
return {
size,
name,
backgroundJob:
backgroundJobs.length === 1
? backgroundJobs[0]
: /** @type {BackgroundJob} */ (Promise.all(backgroundJobs))
};
};
/**
* @param {FileMiddleware} middleware this
* @param {string | false} name filename
* @param {(name: string | false) => Promise<Buffer[]>} readFile read content of a file
* @returns {Promise<BufferSerializableType[]>} deserialized data
*/
const deserialize = async (middleware, name, readFile) => {
const contents = await readFile(name);
if (contents.length === 0) throw new Error(`Empty file ${name}`);
let contentsIndex = 0;
let contentItem = contents[0];
let contentItemLength = contentItem.length;
let contentPosition = 0;
if (contentItemLength === 0) throw new Error(`Empty file ${name}`);
const nextContent = () => {
contentsIndex++;
contentItem = contents[contentsIndex];
contentItemLength = contentItem.length;
contentPosition = 0;
};
/**
* @param {number} n number of bytes to ensure
*/
const ensureData = n => {
if (contentPosition === contentItemLength) {
nextContent();
}
while (contentItemLength - contentPosition < n) {
const remaining = contentItem.slice(contentPosition);
let lengthFromNext = n - remaining.length;
const buffers = [remaining];
for (let i = contentsIndex + 1; i < contents.length; i++) {
const l = contents[i].length;
if (l > lengthFromNext) {
buffers.push(contents[i].slice(0, lengthFromNext));
contents[i] = contents[i].slice(lengthFromNext);
lengthFromNext = 0;
break;
} else {
buffers.push(contents[i]);
contentsIndex = i;
lengthFromNext -= l;
}
}
if (lengthFromNext > 0) throw new Error("Unexpected end of data");
contentItem = Buffer.concat(buffers, n);
contentItemLength = n;
contentPosition = 0;
}
};
/**
* @returns {number} value value
*/
const readUInt32LE = () => {
ensureData(4);
const value = contentItem.readUInt32LE(contentPosition);
contentPosition += 4;
return value;
};
/**
* @returns {number} value value
*/
const readInt32LE = () => {
ensureData(4);
const value = contentItem.readInt32LE(contentPosition);
contentPosition += 4;
return value;
};
/**
* @param {number} l length
* @returns {Buffer} buffer
*/
const readSlice = l => {
ensureData(l);
if (contentPosition === 0 && contentItemLength === l) {
const result = contentItem;
if (contentsIndex + 1 < contents.length) {
nextContent();
} else {
contentPosition = l;
}
return result;
}
const result = contentItem.slice(contentPosition, contentPosition + l);
contentPosition += l;
// we clone the buffer here to allow the original content to be garbage collected
return l * 2 < contentItem.buffer.byteLength ? Buffer.from(result) : result;
};
const version = readUInt32LE();
if (version !== VERSION) {
throw new Error("Invalid file version");
}
const sectionCount = readUInt32LE();
const lengths = [];
let lastLengthPositive = false;
for (let i = 0; i < sectionCount; i++) {
const value = readInt32LE();
const valuePositive = value >= 0;
if (lastLengthPositive && valuePositive) {
lengths[lengths.length - 1] += value;
} else {
lengths.push(value);
lastLengthPositive = valuePositive;
}
}
/** @type {BufferSerializableType[]} */
const result = [];
for (let length of lengths) {
if (length < 0) {
const slice = readSlice(-length);
const size = Number(readUInt64LE(slice, 0));
const nameBuffer = slice.slice(8);
const name = nameBuffer.toString();
const lazy =
/** @type {LazyFunction} */
(
SerializerMiddleware.createLazy(
memoize(() => deserialize(middleware, name, readFile)),
middleware,
{ name, size },
slice
)
);
result.push(lazy);
} else {
if (contentPosition === contentItemLength) {
nextContent();
} else if (contentPosition !== 0) {
if (length <= contentItemLength - contentPosition) {
result.push(
Buffer.from(
contentItem.buffer,
contentItem.byteOffset + contentPosition,
length
)
);
contentPosition += length;
length = 0;
} else {
const l = contentItemLength - contentPosition;
result.push(
Buffer.from(
contentItem.buffer,
contentItem.byteOffset + contentPosition,
l
)
);
length -= l;
contentPosition = contentItemLength;
}
} else if (length >= contentItemLength) {
result.push(contentItem);
length -= contentItemLength;
contentPosition = contentItemLength;
} else {
result.push(
Buffer.from(contentItem.buffer, contentItem.byteOffset, length)
);
contentPosition += length;
length = 0;
}
while (length > 0) {
nextContent();
if (length >= contentItemLength) {
result.push(contentItem);
length -= contentItemLength;
contentPosition = contentItemLength;
} else {
result.push(
Buffer.from(contentItem.buffer, contentItem.byteOffset, length)
);
contentPosition += length;
length = 0;
}
}
}
}
return result;
};
/** @typedef {BufferSerializableType[]} DeserializedType */
/** @typedef {true} SerializedType */
/** @typedef {{ filename: string, extension?: string }} Context */
/**
* @extends {SerializerMiddleware<DeserializedType, SerializedType, Context>}
*/
class FileMiddleware extends SerializerMiddleware {
/**
* @param {IntermediateFileSystem} fs filesystem
* @param {string | Hash} hashFunction hash function to use
*/
constructor(fs, hashFunction = DEFAULTS.HASH_FUNCTION) {
super();
this.fs = fs;
this._hashFunction = hashFunction;
}
/**
* @param {DeserializedType} data data
* @param {Context} context context object
* @returns {SerializedType | Promise<SerializedType> | null} serialized data
*/
serialize(data, context) {
const { filename, extension = "" } = context;
return new Promise((resolve, reject) => {
mkdirp(this.fs, dirname(this.fs, filename), err => {
if (err) return reject(err);
// It's important that we don't touch existing files during serialization
// because serialize may read existing files (when deserializing)
const allWrittenFiles = new Set();
/**
* @param {string | false} name name
* @param {Buffer[]} content content
* @param {number} size size
* @returns {Promise<void>}
*/
const writeFile = async (name, content, size) => {
const file = name
? join(this.fs, filename, `../${name}${extension}`)
: filename;
await new Promise(
/**
* @param {(value?: undefined) => void} resolve resolve
* @param {(reason?: Error | null) => void} reject reject
*/
(resolve, reject) => {
let stream = this.fs.createWriteStream(`${file}_`);
let compression;
if (file.endsWith(".gz")) {
compression = createGzip({
chunkSize: COMPRESSION_CHUNK_SIZE,
level: zConstants.Z_BEST_SPEED
});
} else if (file.endsWith(".br")) {
compression = createBrotliCompress({
chunkSize: COMPRESSION_CHUNK_SIZE,
params: {
[zConstants.BROTLI_PARAM_MODE]: zConstants.BROTLI_MODE_TEXT,
[zConstants.BROTLI_PARAM_QUALITY]: 2,
[zConstants.BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING]: true,
[zConstants.BROTLI_PARAM_SIZE_HINT]: size
}
});
}
if (compression) {
pipeline(compression, stream, reject);
stream = compression;
stream.on("finish", () => resolve());
} else {
stream.on("error", err => reject(err));
stream.on("finish", () => resolve());
}
// split into chunks for WRITE_LIMIT_CHUNK size
/** @type {Buffer[]} */
const chunks = [];
for (const b of content) {
if (b.length < WRITE_LIMIT_CHUNK) {
chunks.push(b);
} else {
for (let i = 0; i < b.length; i += WRITE_LIMIT_CHUNK) {
chunks.push(b.slice(i, i + WRITE_LIMIT_CHUNK));
}
}
}
const len = chunks.length;
let i = 0;
/**
* @param {(Error | null)=} err err
*/
const batchWrite = err => {
// will be handled in "on" error handler
if (err) return;
if (i === len) {
stream.end();
return;
}
// queue up a batch of chunks up to the write limit
// end is exclusive
let end = i;
let sum = chunks[end++].length;
while (end < len) {
sum += chunks[end].length;
if (sum > WRITE_LIMIT_TOTAL) break;
end++;
}
while (i < end - 1) {
stream.write(chunks[i++]);
}
stream.write(chunks[i++], batchWrite);
};
batchWrite();
}
);
if (name) allWrittenFiles.add(file);
};
resolve(
serialize(this, data, false, writeFile, this._hashFunction).then(
async ({ backgroundJob }) => {
await backgroundJob;
// Rename the index file to disallow access during inconsistent file state
await new Promise(
/**
* @param {(value?: undefined) => void} resolve resolve
*/
resolve => {
this.fs.rename(filename, `${filename}.old`, err => {
resolve();
});
}
);
// update all written files
await Promise.all(
Array.from(
allWrittenFiles,
file =>
new Promise(
/**
* @param {(value?: undefined) => void} resolve resolve
* @param {(reason?: Error | null) => void} reject reject
* @returns {void}
*/
(resolve, reject) => {
this.fs.rename(`${file}_`, file, err => {
if (err) return reject(err);
resolve();
});
}
)
)
);
// As final step automatically update the index file to have a consistent pack again
await new Promise(
/**
* @param {(value?: undefined) => void} resolve resolve
* @returns {void}
*/
resolve => {
this.fs.rename(`${filename}_`, filename, err => {
if (err) return reject(err);
resolve();
});
}
);
return /** @type {true} */ (true);
}
)
);
});
});
}
/**
* @param {SerializedType} data data
* @param {Context} context context object
* @returns {DeserializedType | Promise<DeserializedType>} deserialized data
*/
deserialize(data, context) {
const { filename, extension = "" } = context;
/**
* @param {string | boolean} name name
* @returns {Promise<Buffer[]>} result
*/
const readFile = name =>
new Promise((resolve, reject) => {
const file = name
? join(this.fs, filename, `../${name}${extension}`)
: filename;
this.fs.stat(file, (err, stats) => {
if (err) {
reject(err);
return;
}
let remaining = /** @type {IStats} */ (stats).size;
/** @type {Buffer | undefined} */
let currentBuffer;
/** @type {number | undefined} */
let currentBufferUsed;
/** @type {Buffer[]} */
const buf = [];
/** @type {import("zlib").Zlib & import("stream").Transform | undefined} */
let decompression;
if (file.endsWith(".gz")) {
decompression = createGunzip({
chunkSize: DECOMPRESSION_CHUNK_SIZE
});
} else if (file.endsWith(".br")) {
decompression = createBrotliDecompress({
chunkSize: DECOMPRESSION_CHUNK_SIZE
});
}
if (decompression) {
/** @typedef {(value: Buffer[] | PromiseLike<Buffer[]>) => void} NewResolve */
/** @typedef {(reason?: Error) => void} NewReject */
/** @type {NewResolve | undefined} */
let newResolve;
/** @type {NewReject | undefined} */
let newReject;
resolve(
Promise.all([
new Promise((rs, rj) => {
newResolve = rs;
newReject = rj;
}),
new Promise(
/**
* @param {(value?: undefined) => void} resolve resolve
* @param {(reason?: Error) => void} reject reject
*/
(resolve, reject) => {
decompression.on("data", chunk => buf.push(chunk));
decompression.on("end", () => resolve());
decompression.on("error", err => reject(err));
}
)
]).then(() => buf)
);
resolve = /** @type {NewResolve} */ (newResolve);
reject = /** @type {NewReject} */ (newReject);
}
this.fs.open(file, "r", (err, _fd) => {
if (err) {
reject(err);
return;
}
const fd = /** @type {number} */ (_fd);
const read = () => {
if (currentBuffer === undefined) {
currentBuffer = Buffer.allocUnsafeSlow(
Math.min(
constants.MAX_LENGTH,
remaining,
decompression ? DECOMPRESSION_CHUNK_SIZE : Infinity
)
);
currentBufferUsed = 0;
}
let readBuffer = currentBuffer;
let readOffset = /** @type {number} */ (currentBufferUsed);
let readLength =
currentBuffer.length -
/** @type {number} */ (currentBufferUsed);
// values passed to fs.read must be valid int32 values
if (readOffset > 0x7fffffff) {
readBuffer = currentBuffer.slice(readOffset);
readOffset = 0;
}
if (readLength > 0x7fffffff) {
readLength = 0x7fffffff;
}
this.fs.read(
fd,
readBuffer,
readOffset,
readLength,
null,
(err, bytesRead) => {
if (err) {
this.fs.close(fd, () => {
reject(err);
});
return;
}
/** @type {number} */
(currentBufferUsed) += bytesRead;
remaining -= bytesRead;
if (
currentBufferUsed ===
/** @type {Buffer} */
(currentBuffer).length
) {
if (decompression) {
decompression.write(currentBuffer);
} else {
buf.push(
/** @type {Buffer} */
(currentBuffer)
);
}
currentBuffer = undefined;
if (remaining === 0) {
if (decompression) {
decompression.end();
}
this.fs.close(fd, err => {
if (err) {
reject(err);
return;
}
resolve(buf);
});
return;
}
}
read();
}
);
};
read();
});
});
});
return deserialize(this, false, readFile);
}
}
module.exports = FileMiddleware;

View File

@ -0,0 +1,48 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
/** @typedef {import("./ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
/** @typedef {import("./ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
class MapObjectSerializer {
/**
* @template K, V
* @param {Map<K, V>} obj map
* @param {ObjectSerializerContext} context context
*/
serialize(obj, context) {
context.write(obj.size);
for (const key of obj.keys()) {
context.write(key);
}
for (const value of obj.values()) {
context.write(value);
}
}
/**
* @template K, V
* @param {ObjectDeserializerContext} context context
* @returns {Map<K, V>} map
*/
deserialize(context) {
/** @type {number} */
const size = context.read();
/** @type {Map<K, V>} */
const map = new Map();
/** @type {K[]} */
const keys = [];
for (let i = 0; i < size; i++) {
keys.push(context.read());
}
for (let i = 0; i < size; i++) {
map.set(keys[i], context.read());
}
return map;
}
}
module.exports = MapObjectSerializer;

View File

@ -0,0 +1,51 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
/** @typedef {import("./ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
/** @typedef {import("./ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
class NullPrototypeObjectSerializer {
/**
* @template {object} T
* @param {T} obj null object
* @param {ObjectSerializerContext} context context
*/
serialize(obj, context) {
/** @type {string[]} */
const keys = Object.keys(obj);
for (const key of keys) {
context.write(key);
}
context.write(null);
for (const key of keys) {
context.write(obj[/** @type {keyof T} */ (key)]);
}
}
/**
* @template {object} T
* @param {ObjectDeserializerContext} context context
* @returns {T} null object
*/
deserialize(context) {
/** @type {T} */
const obj = Object.create(null);
/** @type {string[]} */
const keys = [];
/** @type {string | null} */
let key = context.read();
while (key !== null) {
keys.push(key);
key = context.read();
}
for (const key of keys) {
obj[/** @type {keyof T} */ (key)] = context.read();
}
return obj;
}
}
module.exports = NullPrototypeObjectSerializer;

View File

@ -0,0 +1,839 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
const { DEFAULTS } = require("../config/defaults");
const createHash = require("../util/createHash");
const AggregateErrorSerializer = require("./AggregateErrorSerializer");
const ArraySerializer = require("./ArraySerializer");
const DateObjectSerializer = require("./DateObjectSerializer");
const ErrorObjectSerializer = require("./ErrorObjectSerializer");
const MapObjectSerializer = require("./MapObjectSerializer");
const NullPrototypeObjectSerializer = require("./NullPrototypeObjectSerializer");
const PlainObjectSerializer = require("./PlainObjectSerializer");
const RegExpObjectSerializer = require("./RegExpObjectSerializer");
const SerializerMiddleware = require("./SerializerMiddleware");
const SetObjectSerializer = require("./SetObjectSerializer");
/** @typedef {import("../logging/Logger").Logger} Logger */
/** @typedef {typeof import("../util/Hash")} Hash */
/** @typedef {import("./SerializerMiddleware").LazyOptions} LazyOptions */
/** @typedef {import("./types").ComplexSerializableType} ComplexSerializableType */
/** @typedef {import("./types").PrimitiveSerializableType} PrimitiveSerializableType */
/** @typedef {new (...params: EXPECTED_ANY[]) => EXPECTED_ANY} Constructor */
/*
Format:
File -> Section*
Section -> ObjectSection | ReferenceSection | EscapeSection | OtherSection
ObjectSection -> ESCAPE (
number:relativeOffset (number > 0) |
string:request (string|null):export
) Section:value* ESCAPE ESCAPE_END_OBJECT
ReferenceSection -> ESCAPE number:relativeOffset (number < 0)
EscapeSection -> ESCAPE ESCAPE_ESCAPE_VALUE (escaped value ESCAPE)
EscapeSection -> ESCAPE ESCAPE_UNDEFINED (escaped value ESCAPE)
OtherSection -> any (except ESCAPE)
Why using null as escape value?
Multiple null values can merged by the BinaryMiddleware, which makes it very efficient
Technically any value can be used.
*/
/**
* @typedef {object} ObjectSerializerSnapshot
* @property {number} length
* @property {number} cycleStackSize
* @property {number} referenceableSize
* @property {number} currentPos
* @property {number} objectTypeLookupSize
* @property {number} currentPosTypeLookup
*/
/** @typedef {TODO} Value */
/** @typedef {EXPECTED_OBJECT | string} ReferenceableItem */
/**
* @typedef {object} ObjectSerializerContext
* @property {(value: Value) => void} write
* @property {(value: ReferenceableItem) => void} setCircularReference
* @property {() => ObjectSerializerSnapshot} snapshot
* @property {(snapshot: ObjectSerializerSnapshot) => void} rollback
* @property {((item: Value | (() => Value)) => void)=} writeLazy
* @property {((item: (Value | (() => Value)), obj: LazyOptions | undefined) => import("./SerializerMiddleware").LazyFunction<EXPECTED_ANY, EXPECTED_ANY, EXPECTED_ANY, LazyOptions>)=} writeSeparate
*/
/**
* @typedef {object} ObjectDeserializerContext
* @property {() => Value} read
* @property {(value: ReferenceableItem) => void} setCircularReference
*/
/**
* @typedef {object} ObjectSerializer
* @property {(value: Value, context: ObjectSerializerContext) => void} serialize
* @property {(context: ObjectDeserializerContext) => Value} deserialize
*/
/**
* @template T
* @param {Set<T>} set set
* @param {number} size count of items to keep
*/
const setSetSize = (set, size) => {
let i = 0;
for (const item of set) {
if (i++ >= size) {
set.delete(item);
}
}
};
/**
* @template K, X
* @param {Map<K, X>} map map
* @param {number} size count of items to keep
*/
const setMapSize = (map, size) => {
let i = 0;
for (const item of map.keys()) {
if (i++ >= size) {
map.delete(item);
}
}
};
/**
* @param {Buffer} buffer buffer
* @param {string | Hash} hashFunction hash function to use
* @returns {string} hash
*/
const toHash = (buffer, hashFunction) => {
const hash = createHash(hashFunction);
hash.update(buffer);
return /** @type {string} */ (hash.digest("latin1"));
};
const ESCAPE = null;
const ESCAPE_ESCAPE_VALUE = null;
const ESCAPE_END_OBJECT = true;
const ESCAPE_UNDEFINED = false;
const CURRENT_VERSION = 2;
/** @typedef {{ request?: string, name?: string | number | null, serializer?: ObjectSerializer }} SerializerConfig */
/** @typedef {{ request?: string, name?: string | number | null, serializer: ObjectSerializer }} SerializerConfigWithSerializer */
/** @type {Map<Constructor, SerializerConfig>} */
const serializers = new Map();
/** @type {Map<string | number, ObjectSerializer>} */
const serializerInversed = new Map();
/** @type {Set<string>} */
const loadedRequests = new Set();
const NOT_SERIALIZABLE = {};
const jsTypes = new Map();
jsTypes.set(Object, new PlainObjectSerializer());
jsTypes.set(Array, new ArraySerializer());
jsTypes.set(null, new NullPrototypeObjectSerializer());
jsTypes.set(Map, new MapObjectSerializer());
jsTypes.set(Set, new SetObjectSerializer());
jsTypes.set(Date, new DateObjectSerializer());
jsTypes.set(RegExp, new RegExpObjectSerializer());
jsTypes.set(Error, new ErrorObjectSerializer(Error));
jsTypes.set(EvalError, new ErrorObjectSerializer(EvalError));
jsTypes.set(RangeError, new ErrorObjectSerializer(RangeError));
jsTypes.set(ReferenceError, new ErrorObjectSerializer(ReferenceError));
jsTypes.set(SyntaxError, new ErrorObjectSerializer(SyntaxError));
jsTypes.set(TypeError, new ErrorObjectSerializer(TypeError));
// @ts-expect-error ES2018 doesn't `AggregateError`, but it can be used by developers
// eslint-disable-next-line n/no-unsupported-features/es-builtins, n/no-unsupported-features/es-syntax
if (typeof AggregateError !== "undefined") {
jsTypes.set(
// @ts-expect-error ES2018 doesn't `AggregateError`, but it can be used by developers
// eslint-disable-next-line n/no-unsupported-features/es-builtins, n/no-unsupported-features/es-syntax
AggregateError,
new AggregateErrorSerializer()
);
}
// If in a sandboxed environment (e.g. jest), this escapes the sandbox and registers
// real Object and Array types to. These types may occur in the wild too, e.g. when
// using Structured Clone in postMessage.
// eslint-disable-next-line n/exports-style
if (exports.constructor !== Object) {
// eslint-disable-next-line n/exports-style
const Obj = /** @type {ObjectConstructor} */ (exports.constructor);
const Fn = /** @type {FunctionConstructor} */ (Obj.constructor);
for (const [type, config] of Array.from(jsTypes)) {
if (type) {
const Type = new Fn(`return ${type.name};`)();
jsTypes.set(Type, config);
}
}
}
{
let i = 1;
for (const [type, serializer] of jsTypes) {
serializers.set(type, {
request: "",
name: i++,
serializer
});
}
}
for (const { request, name, serializer } of serializers.values()) {
serializerInversed.set(
`${request}/${name}`,
/** @type {ObjectSerializer} */ (serializer)
);
}
/** @type {Map<RegExp, (request: string) => boolean>} */
const loaders = new Map();
/** @typedef {ComplexSerializableType[]} DeserializedType */
/** @typedef {PrimitiveSerializableType[]} SerializedType */
/** @typedef {{ logger: Logger }} Context */
/**
* @extends {SerializerMiddleware<DeserializedType, SerializedType, Context>}
*/
class ObjectMiddleware extends SerializerMiddleware {
/**
* @param {(context: ObjectSerializerContext | ObjectDeserializerContext) => void} extendContext context extensions
* @param {string | Hash} hashFunction hash function to use
*/
constructor(extendContext, hashFunction = DEFAULTS.HASH_FUNCTION) {
super();
this.extendContext = extendContext;
this._hashFunction = hashFunction;
}
/**
* @param {RegExp} regExp RegExp for which the request is tested
* @param {(request: string) => boolean} loader loader to load the request, returns true when successful
* @returns {void}
*/
static registerLoader(regExp, loader) {
loaders.set(regExp, loader);
}
/**
* @param {Constructor} Constructor the constructor
* @param {string} request the request which will be required when deserializing
* @param {string | null} name the name to make multiple serializer unique when sharing a request
* @param {ObjectSerializer} serializer the serializer
* @returns {void}
*/
static register(Constructor, request, name, serializer) {
const key = `${request}/${name}`;
if (serializers.has(Constructor)) {
throw new Error(
`ObjectMiddleware.register: serializer for ${Constructor.name} is already registered`
);
}
if (serializerInversed.has(key)) {
throw new Error(
`ObjectMiddleware.register: serializer for ${key} is already registered`
);
}
serializers.set(Constructor, {
request,
name,
serializer
});
serializerInversed.set(key, serializer);
}
/**
* @param {Constructor} Constructor the constructor
* @returns {void}
*/
static registerNotSerializable(Constructor) {
if (serializers.has(Constructor)) {
throw new Error(
`ObjectMiddleware.registerNotSerializable: serializer for ${Constructor.name} is already registered`
);
}
serializers.set(Constructor, NOT_SERIALIZABLE);
}
/**
* @param {Constructor} object for serialization
* @returns {SerializerConfigWithSerializer} Serializer config
*/
static getSerializerFor(object) {
const proto = Object.getPrototypeOf(object);
let c;
if (proto === null) {
// Object created with Object.create(null)
c = null;
} else {
c = proto.constructor;
if (!c) {
throw new Error(
"Serialization of objects with prototype without valid constructor property not possible"
);
}
}
const config = serializers.get(c);
if (!config) throw new Error(`No serializer registered for ${c.name}`);
if (config === NOT_SERIALIZABLE) throw NOT_SERIALIZABLE;
return /** @type {SerializerConfigWithSerializer} */ (config);
}
/**
* @param {string} request request
* @param {string} name name
* @returns {ObjectSerializer} serializer
*/
static getDeserializerFor(request, name) {
const key = `${request}/${name}`;
const serializer = serializerInversed.get(key);
if (serializer === undefined) {
throw new Error(`No deserializer registered for ${key}`);
}
return serializer;
}
/**
* @param {string} request request
* @param {string} name name
* @returns {ObjectSerializer | undefined} serializer
*/
static _getDeserializerForWithoutError(request, name) {
const key = `${request}/${name}`;
const serializer = serializerInversed.get(key);
return serializer;
}
/**
* @param {DeserializedType} data data
* @param {Context} context context object
* @returns {SerializedType | Promise<SerializedType> | null} serialized data
*/
serialize(data, context) {
/** @type {Value[]} */
let result = [CURRENT_VERSION];
let currentPos = 0;
/** @type {Map<ReferenceableItem, number>} */
let referenceable = new Map();
/**
* @param {ReferenceableItem} item referenceable item
*/
const addReferenceable = item => {
referenceable.set(item, currentPos++);
};
let bufferDedupeMap = new Map();
/**
* @param {Buffer} buf buffer
* @returns {Buffer} deduped buffer
*/
const dedupeBuffer = buf => {
const len = buf.length;
const entry = bufferDedupeMap.get(len);
if (entry === undefined) {
bufferDedupeMap.set(len, buf);
return buf;
}
if (Buffer.isBuffer(entry)) {
if (len < 32) {
if (buf.equals(entry)) {
return entry;
}
bufferDedupeMap.set(len, [entry, buf]);
return buf;
}
const hash = toHash(entry, this._hashFunction);
const newMap = new Map();
newMap.set(hash, entry);
bufferDedupeMap.set(len, newMap);
const hashBuf = toHash(buf, this._hashFunction);
if (hash === hashBuf) {
return entry;
}
return buf;
} else if (Array.isArray(entry)) {
if (entry.length < 16) {
for (const item of entry) {
if (buf.equals(item)) {
return item;
}
}
entry.push(buf);
return buf;
}
const newMap = new Map();
const hash = toHash(buf, this._hashFunction);
let found;
for (const item of entry) {
const itemHash = toHash(item, this._hashFunction);
newMap.set(itemHash, item);
if (found === undefined && itemHash === hash) found = item;
}
bufferDedupeMap.set(len, newMap);
if (found === undefined) {
newMap.set(hash, buf);
return buf;
}
return found;
}
const hash = toHash(buf, this._hashFunction);
const item = entry.get(hash);
if (item !== undefined) {
return item;
}
entry.set(hash, buf);
return buf;
};
let currentPosTypeLookup = 0;
let objectTypeLookup = new Map();
const cycleStack = new Set();
/**
* @param {Value} item item to stack
* @returns {string} stack
*/
const stackToString = item => {
const arr = Array.from(cycleStack);
arr.push(item);
return arr
.map(item => {
if (typeof item === "string") {
if (item.length > 100) {
return `String ${JSON.stringify(item.slice(0, 100)).slice(
0,
-1
)}..."`;
}
return `String ${JSON.stringify(item)}`;
}
try {
const { request, name } = ObjectMiddleware.getSerializerFor(item);
if (request) {
return `${request}${name ? `.${name}` : ""}`;
}
} catch (_err) {
// ignore -> fallback
}
if (typeof item === "object" && item !== null) {
if (item.constructor) {
if (item.constructor === Object)
return `Object { ${Object.keys(item).join(", ")} }`;
if (item.constructor === Map) return `Map { ${item.size} items }`;
if (item.constructor === Array)
return `Array { ${item.length} items }`;
if (item.constructor === Set) return `Set { ${item.size} items }`;
if (item.constructor === RegExp) return item.toString();
return `${item.constructor.name}`;
}
return `Object [null prototype] { ${Object.keys(item).join(
", "
)} }`;
}
if (typeof item === "bigint") {
return `BigInt ${item}n`;
}
try {
return `${item}`;
} catch (err) {
return `(${/** @type {Error} */ (err).message})`;
}
})
.join(" -> ");
};
/** @type {WeakSet<Error>} */
let hasDebugInfoAttached;
/** @type {ObjectSerializerContext} */
let ctx = {
write(value) {
try {
process(value);
} catch (err) {
if (err !== NOT_SERIALIZABLE) {
if (hasDebugInfoAttached === undefined)
hasDebugInfoAttached = new WeakSet();
if (!hasDebugInfoAttached.has(/** @type {Error} */ (err))) {
/** @type {Error} */
(err).message += `\nwhile serializing ${stackToString(value)}`;
hasDebugInfoAttached.add(/** @type {Error} */ (err));
}
}
throw err;
}
},
setCircularReference(ref) {
addReferenceable(ref);
},
snapshot() {
return {
length: result.length,
cycleStackSize: cycleStack.size,
referenceableSize: referenceable.size,
currentPos,
objectTypeLookupSize: objectTypeLookup.size,
currentPosTypeLookup
};
},
rollback(snapshot) {
result.length = snapshot.length;
setSetSize(cycleStack, snapshot.cycleStackSize);
setMapSize(referenceable, snapshot.referenceableSize);
currentPos = snapshot.currentPos;
setMapSize(objectTypeLookup, snapshot.objectTypeLookupSize);
currentPosTypeLookup = snapshot.currentPosTypeLookup;
},
...context
};
this.extendContext(ctx);
/**
* @param {Value} item item to serialize
*/
const process = item => {
if (Buffer.isBuffer(item)) {
// check if we can emit a reference
const ref = referenceable.get(item);
if (ref !== undefined) {
result.push(ESCAPE, ref - currentPos);
return;
}
const alreadyUsedBuffer = dedupeBuffer(item);
if (alreadyUsedBuffer !== item) {
const ref = referenceable.get(alreadyUsedBuffer);
if (ref !== undefined) {
referenceable.set(item, ref);
result.push(ESCAPE, ref - currentPos);
return;
}
item = alreadyUsedBuffer;
}
addReferenceable(item);
result.push(item);
} else if (item === ESCAPE) {
result.push(ESCAPE, ESCAPE_ESCAPE_VALUE);
} else if (
typeof item === "object"
// We don't have to check for null as ESCAPE is null and this has been checked before
) {
// check if we can emit a reference
const ref = referenceable.get(item);
if (ref !== undefined) {
result.push(ESCAPE, ref - currentPos);
return;
}
if (cycleStack.has(item)) {
throw new Error(
"This is a circular references. To serialize circular references use 'setCircularReference' somewhere in the circle during serialize and deserialize."
);
}
const { request, name, serializer } =
ObjectMiddleware.getSerializerFor(item);
const key = `${request}/${name}`;
const lastIndex = objectTypeLookup.get(key);
if (lastIndex === undefined) {
objectTypeLookup.set(key, currentPosTypeLookup++);
result.push(ESCAPE, request, name);
} else {
result.push(ESCAPE, currentPosTypeLookup - lastIndex);
}
cycleStack.add(item);
try {
serializer.serialize(item, ctx);
} finally {
cycleStack.delete(item);
}
result.push(ESCAPE, ESCAPE_END_OBJECT);
addReferenceable(item);
} else if (typeof item === "string") {
if (item.length > 1) {
// short strings are shorter when not emitting a reference (this saves 1 byte per empty string)
// check if we can emit a reference
const ref = referenceable.get(item);
if (ref !== undefined) {
result.push(ESCAPE, ref - currentPos);
return;
}
addReferenceable(item);
}
if (item.length > 102400 && context.logger) {
context.logger.warn(
`Serializing big strings (${Math.round(
item.length / 1024
)}kiB) impacts deserialization performance (consider using Buffer instead and decode when needed)`
);
}
result.push(item);
} else if (typeof item === "function") {
if (!SerializerMiddleware.isLazy(item))
throw new Error(`Unexpected function ${item}`);
/** @type {SerializedType | undefined} */
const serializedData =
SerializerMiddleware.getLazySerializedValue(item);
if (serializedData !== undefined) {
if (typeof serializedData === "function") {
result.push(serializedData);
} else {
throw new Error("Not implemented");
}
} else if (SerializerMiddleware.isLazy(item, this)) {
throw new Error("Not implemented");
} else {
const data = SerializerMiddleware.serializeLazy(item, data =>
this.serialize([data], context)
);
SerializerMiddleware.setLazySerializedValue(item, data);
result.push(data);
}
} else if (item === undefined) {
result.push(ESCAPE, ESCAPE_UNDEFINED);
} else {
result.push(item);
}
};
try {
for (const item of data) {
process(item);
}
return result;
} catch (err) {
if (err === NOT_SERIALIZABLE) return null;
throw err;
} finally {
// Get rid of these references to avoid leaking memory
// This happens because the optimized code v8 generates
// is optimized for our "ctx.write" method so it will reference
// it from e. g. Dependency.prototype.serialize -(IC)-> ctx.write
data =
result =
referenceable =
bufferDedupeMap =
objectTypeLookup =
ctx =
/** @type {EXPECTED_ANY} */
(undefined);
}
}
/**
* @param {SerializedType} data data
* @param {Context} context context object
* @returns {DeserializedType | Promise<DeserializedType>} deserialized data
*/
deserialize(data, context) {
let currentDataPos = 0;
const read = () => {
if (currentDataPos >= data.length)
throw new Error("Unexpected end of stream");
return data[currentDataPos++];
};
if (read() !== CURRENT_VERSION)
throw new Error("Version mismatch, serializer changed");
let currentPos = 0;
/** @type {ReferenceableItem[]} */
let referenceable = [];
/**
* @param {Value} item referenceable item
*/
const addReferenceable = item => {
referenceable.push(item);
currentPos++;
};
let currentPosTypeLookup = 0;
/** @type {ObjectSerializer[]} */
let objectTypeLookup = [];
let result = [];
/** @type {ObjectDeserializerContext} */
let ctx = {
read() {
return decodeValue();
},
setCircularReference(ref) {
addReferenceable(ref);
},
...context
};
this.extendContext(ctx);
const decodeValue = () => {
const item = read();
if (item === ESCAPE) {
const nextItem = read();
if (nextItem === ESCAPE_ESCAPE_VALUE) {
return ESCAPE;
} else if (nextItem === ESCAPE_UNDEFINED) {
// Nothing
} else if (nextItem === ESCAPE_END_OBJECT) {
throw new Error(
`Unexpected end of object at position ${currentDataPos - 1}`
);
} else {
const request = nextItem;
let serializer;
if (typeof request === "number") {
if (request < 0) {
// relative reference
return referenceable[currentPos + request];
}
serializer = objectTypeLookup[currentPosTypeLookup - request];
} else {
if (typeof request !== "string") {
throw new Error(
`Unexpected type (${typeof request}) of request ` +
`at position ${currentDataPos - 1}`
);
}
const name = /** @type {string} */ (read());
serializer = ObjectMiddleware._getDeserializerForWithoutError(
request,
name
);
if (serializer === undefined) {
if (request && !loadedRequests.has(request)) {
let loaded = false;
for (const [regExp, loader] of loaders) {
if (regExp.test(request) && loader(request)) {
loaded = true;
break;
}
}
if (!loaded) {
require(request);
}
loadedRequests.add(request);
}
serializer = ObjectMiddleware.getDeserializerFor(request, name);
}
objectTypeLookup.push(serializer);
currentPosTypeLookup++;
}
try {
const item = serializer.deserialize(ctx);
const end1 = read();
if (end1 !== ESCAPE) {
throw new Error("Expected end of object");
}
const end2 = read();
if (end2 !== ESCAPE_END_OBJECT) {
throw new Error("Expected end of object");
}
addReferenceable(item);
return item;
} catch (err) {
// As this is only for error handling, we omit creating a Map for
// faster access to this information, as this would affect performance
// in the good case
let serializerEntry;
for (const entry of serializers) {
if (entry[1].serializer === serializer) {
serializerEntry = entry;
break;
}
}
const name = !serializerEntry
? "unknown"
: !serializerEntry[1].request
? serializerEntry[0].name
: serializerEntry[1].name
? `${serializerEntry[1].request} ${serializerEntry[1].name}`
: serializerEntry[1].request;
/** @type {Error} */
(err).message += `\n(during deserialization of ${name})`;
throw err;
}
}
} else if (typeof item === "string") {
if (item.length > 1) {
addReferenceable(item);
}
return item;
} else if (Buffer.isBuffer(item)) {
addReferenceable(item);
return item;
} else if (typeof item === "function") {
return SerializerMiddleware.deserializeLazy(
item,
data =>
/** @type {[DeserializedType]} */
(this.deserialize(data, context))[0]
);
} else {
return item;
}
};
try {
while (currentDataPos < data.length) {
result.push(decodeValue());
}
return result;
} finally {
// Get rid of these references to avoid leaking memory
// This happens because the optimized code v8 generates
// is optimized for our "ctx.read" method so it will reference
// it from e. g. Dependency.prototype.deserialize -(IC)-> ctx.read
result =
referenceable =
data =
objectTypeLookup =
ctx =
/** @type {EXPECTED_ANY} */
(undefined);
}
}
}
module.exports = ObjectMiddleware;
module.exports.NOT_SERIALIZABLE = NOT_SERIALIZABLE;

View File

@ -0,0 +1,117 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
/** @typedef {import("./ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
/** @typedef {import("./ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
/** @typedef {EXPECTED_FUNCTION} CacheAssoc */
/**
* @template T
* @typedef {WeakMap<CacheAssoc, ObjectStructure<T>>}
*/
const cache = new WeakMap();
/**
* @template T
*/
class ObjectStructure {
constructor() {
this.keys = undefined;
this.children = undefined;
}
/**
* @param {keyof T[]} keys keys
* @returns {keyof T[]} keys
*/
getKeys(keys) {
if (this.keys === undefined) this.keys = keys;
return this.keys;
}
/**
* @param {keyof T} key key
* @returns {ObjectStructure<T>} object structure
*/
key(key) {
if (this.children === undefined) this.children = new Map();
const child = this.children.get(key);
if (child !== undefined) return child;
const newChild = new ObjectStructure();
this.children.set(key, newChild);
return newChild;
}
}
/**
* @template T
* @param {(keyof T)[]} keys keys
* @param {CacheAssoc} cacheAssoc cache assoc fn
* @returns {(keyof T)[]} keys
*/
const getCachedKeys = (keys, cacheAssoc) => {
let root = cache.get(cacheAssoc);
if (root === undefined) {
root = new ObjectStructure();
cache.set(cacheAssoc, root);
}
let current = root;
for (const key of keys) {
current = current.key(key);
}
return current.getKeys(keys);
};
class PlainObjectSerializer {
/**
* @template {object} T
* @param {T} obj plain object
* @param {ObjectSerializerContext} context context
*/
serialize(obj, context) {
const keys = /** @type {(keyof T)[]} */ (Object.keys(obj));
if (keys.length > 128) {
// Objects with so many keys are unlikely to share structure
// with other objects
context.write(keys);
for (const key of keys) {
context.write(obj[key]);
}
} else if (keys.length > 1) {
context.write(getCachedKeys(keys, context.write));
for (const key of keys) {
context.write(obj[key]);
}
} else if (keys.length === 1) {
const key = keys[0];
context.write(key);
context.write(obj[key]);
} else {
context.write(null);
}
}
/**
* @template {object} T
* @param {ObjectDeserializerContext} context context
* @returns {T} plain object
*/
deserialize(context) {
const keys = context.read();
const obj = /** @type {T} */ ({});
if (Array.isArray(keys)) {
for (const key of keys) {
obj[/** @type {keyof T} */ (key)] = context.read();
}
} else if (keys !== null) {
obj[/** @type {keyof T} */ (keys)] = context.read();
}
return obj;
}
}
module.exports = PlainObjectSerializer;

View File

@ -0,0 +1,29 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
/** @typedef {import("./ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
/** @typedef {import("./ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
class RegExpObjectSerializer {
/**
* @param {RegExp} obj regexp
* @param {ObjectSerializerContext} context context
*/
serialize(obj, context) {
context.write(obj.source);
context.write(obj.flags);
}
/**
* @param {ObjectDeserializerContext} context context
* @returns {RegExp} regexp
*/
deserialize(context) {
return new RegExp(context.read(), context.read());
}
}
module.exports = RegExpObjectSerializer;

View File

@ -0,0 +1,80 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
/**
* @template T, K, C
* @typedef {import("./SerializerMiddleware")<T, K, C>} SerializerMiddleware
*/
/**
* @template DeserializedValue
* @template SerializedValue
* @template Context
*/
class Serializer {
/**
* @param {SerializerMiddleware<EXPECTED_ANY, EXPECTED_ANY, EXPECTED_ANY>[]} middlewares serializer middlewares
* @param {Context=} context context
*/
constructor(middlewares, context) {
this.serializeMiddlewares = middlewares.slice();
this.deserializeMiddlewares = middlewares.slice().reverse();
this.context = context;
}
/**
* @template ExtendedContext
* @param {DeserializedValue | Promise<DeserializedValue>} obj object
* @param {Context & ExtendedContext} context context object
* @returns {Promise<SerializedValue>} result
*/
serialize(obj, context) {
const ctx = { ...context, ...this.context };
let current = obj;
for (const middleware of this.serializeMiddlewares) {
if (
current &&
typeof (/** @type {Promise<DeserializedValue>} */ (current).then) ===
"function"
) {
current =
/** @type {Promise<DeserializedValue>} */
(current).then(data => data && middleware.serialize(data, ctx));
} else if (current) {
try {
current = middleware.serialize(current, ctx);
} catch (err) {
current = Promise.reject(err);
}
} else break;
}
return /** @type {Promise<SerializedValue>} */ (current);
}
/**
* @template ExtendedContext
* @param {SerializedValue | Promise<SerializedValue>} value value
* @param {Context & ExtendedContext} context object
* @returns {Promise<DeserializedValue>} result
*/
deserialize(value, context) {
const ctx = { ...context, ...this.context };
let current = value;
for (const middleware of this.deserializeMiddlewares) {
current =
current &&
typeof (/** @type {Promise<SerializedValue>} */ (current).then) ===
"function"
? /** @type {Promise<SerializedValue>} */ (current).then(data =>
middleware.deserialize(data, ctx)
)
: middleware.deserialize(current, ctx);
}
return /** @type {Promise<DeserializedValue>} */ (current);
}
}
module.exports = Serializer;

View File

@ -0,0 +1,226 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
const memoize = require("../util/memoize");
const LAZY_TARGET = Symbol("lazy serialization target");
const LAZY_SERIALIZED_VALUE = Symbol("lazy serialization data");
/** @typedef {SerializerMiddleware<EXPECTED_ANY, EXPECTED_ANY, Record<string, EXPECTED_ANY>>} LazyTarget */
/** @typedef {Record<string, EXPECTED_ANY>} LazyOptions */
/**
* @template InputValue
* @template OutputValue
* @template {LazyTarget} InternalLazyTarget
* @template {LazyOptions | undefined} InternalLazyOptions
* @typedef {(() => InputValue | Promise<InputValue>) & Partial<{ [LAZY_TARGET]: InternalLazyTarget, options: InternalLazyOptions, [LAZY_SERIALIZED_VALUE]?: OutputValue | LazyFunction<OutputValue, InputValue, InternalLazyTarget, InternalLazyOptions> | undefined }>} LazyFunction
*/
/**
* @template DeserializedType
* @template SerializedType
* @template Context
*/
class SerializerMiddleware {
/* istanbul ignore next */
/**
* @abstract
* @param {DeserializedType} data data
* @param {Context} context context object
* @returns {SerializedType | Promise<SerializedType> | null} serialized data
*/
serialize(data, context) {
const AbstractMethodError = require("../AbstractMethodError");
throw new AbstractMethodError();
}
/* istanbul ignore next */
/**
* @abstract
* @param {SerializedType} data data
* @param {Context} context context object
* @returns {DeserializedType | Promise<DeserializedType>} deserialized data
*/
deserialize(data, context) {
const AbstractMethodError = require("../AbstractMethodError");
throw new AbstractMethodError();
}
/**
* @template TLazyInputValue
* @template TLazyOutputValue
* @template {LazyTarget} TLazyTarget
* @template {LazyOptions | undefined} TLazyOptions
* @param {TLazyInputValue | (() => TLazyInputValue)} value contained value or function to value
* @param {TLazyTarget} target target middleware
* @param {TLazyOptions=} options lazy options
* @param {TLazyOutputValue=} serializedValue serialized value
* @returns {LazyFunction<TLazyInputValue, TLazyOutputValue, TLazyTarget, TLazyOptions>} lazy function
*/
static createLazy(
value,
target,
options = /** @type {TLazyOptions} */ ({}),
serializedValue = undefined
) {
if (SerializerMiddleware.isLazy(value, target)) return value;
const fn =
/** @type {LazyFunction<TLazyInputValue, TLazyOutputValue, TLazyTarget, TLazyOptions>} */
(typeof value === "function" ? value : () => value);
fn[LAZY_TARGET] = target;
fn.options = options;
fn[LAZY_SERIALIZED_VALUE] = serializedValue;
return fn;
}
/**
* @template {LazyTarget} TLazyTarget
* @param {EXPECTED_ANY} fn lazy function
* @param {TLazyTarget=} target target middleware
* @returns {fn is LazyFunction<EXPECTED_ANY, EXPECTED_ANY, TLazyTarget, EXPECTED_ANY>} true, when fn is a lazy function (optionally of that target)
*/
static isLazy(fn, target) {
if (typeof fn !== "function") return false;
const t = fn[LAZY_TARGET];
return target ? t === target : Boolean(t);
}
/**
* @template TLazyInputValue
* @template TLazyOutputValue
* @template {LazyTarget} TLazyTarget
* @template {Record<string, EXPECTED_ANY>} TLazyOptions
* @template TLazySerializedValue
* @param {LazyFunction<TLazyInputValue, TLazyOutputValue, TLazyTarget, TLazyOptions>} fn lazy function
* @returns {LazyOptions | undefined} options
*/
static getLazyOptions(fn) {
if (typeof fn !== "function") return;
return fn.options;
}
/**
* @template TLazyInputValue
* @template TLazyOutputValue
* @template {LazyTarget} TLazyTarget
* @template {LazyOptions} TLazyOptions
* @param {LazyFunction<TLazyInputValue, TLazyOutputValue, TLazyTarget, TLazyOptions> | EXPECTED_ANY} fn lazy function
* @returns {TLazyOutputValue | undefined} serialized value
*/
static getLazySerializedValue(fn) {
if (typeof fn !== "function") return;
return fn[LAZY_SERIALIZED_VALUE];
}
/**
* @template TLazyInputValue
* @template TLazyOutputValue
* @template {LazyTarget} TLazyTarget
* @template {LazyOptions} TLazyOptions
* @param {LazyFunction<TLazyInputValue, TLazyOutputValue, LazyTarget, LazyOptions>} fn lazy function
* @param {TLazyOutputValue} value serialized value
* @returns {void}
*/
static setLazySerializedValue(fn, value) {
fn[LAZY_SERIALIZED_VALUE] = value;
}
/**
* @template TLazyInputValue DeserializedValue
* @template TLazyOutputValue SerializedValue
* @template {LazyTarget} TLazyTarget
* @template {LazyOptions | undefined} TLazyOptions
* @param {LazyFunction<TLazyInputValue, TLazyOutputValue, TLazyTarget, TLazyOptions>} lazy lazy function
* @param {(value: TLazyInputValue) => TLazyOutputValue} serialize serialize function
* @returns {LazyFunction<TLazyOutputValue, TLazyInputValue, TLazyTarget, TLazyOptions>} new lazy
*/
static serializeLazy(lazy, serialize) {
const fn =
/** @type {LazyFunction<TLazyOutputValue, TLazyInputValue, TLazyTarget, TLazyOptions>} */
(
memoize(() => {
const r = lazy();
if (
r &&
typeof (/** @type {Promise<TLazyInputValue>} */ (r).then) ===
"function"
) {
return (
/** @type {Promise<TLazyInputValue>} */
(r).then(data => data && serialize(data))
);
}
return serialize(/** @type {TLazyInputValue} */ (r));
})
);
fn[LAZY_TARGET] = lazy[LAZY_TARGET];
fn.options = lazy.options;
lazy[LAZY_SERIALIZED_VALUE] = fn;
return fn;
}
/**
* @template TLazyInputValue SerializedValue
* @template TLazyOutputValue DeserializedValue
* @template SerializedValue
* @template {LazyTarget} TLazyTarget
* @template {LazyOptions | undefined} TLazyOptions
* @param {LazyFunction<TLazyInputValue, TLazyOutputValue, TLazyTarget, TLazyOptions>} lazy lazy function
* @param {(data: TLazyInputValue) => TLazyOutputValue} deserialize deserialize function
* @returns {LazyFunction<TLazyOutputValue, TLazyInputValue, TLazyTarget, TLazyOptions>} new lazy
*/
static deserializeLazy(lazy, deserialize) {
const fn =
/** @type {LazyFunction<TLazyOutputValue, TLazyInputValue, TLazyTarget, TLazyOptions>} */ (
memoize(() => {
const r = lazy();
if (
r &&
typeof (/** @type {Promise<TLazyInputValue>} */ (r).then) ===
"function"
) {
return (
/** @type {Promise<TLazyInputValue>} */
(r).then(data => deserialize(data))
);
}
return deserialize(/** @type {TLazyInputValue} */ (r));
})
);
fn[LAZY_TARGET] = lazy[LAZY_TARGET];
fn.options = lazy.options;
fn[LAZY_SERIALIZED_VALUE] = lazy;
return fn;
}
/**
* @template TLazyInputValue
* @template TLazyOutputValue
* @template {LazyTarget} TLazyTarget
* @template {LazyOptions} TLazyOptions
* @param {LazyFunction<TLazyInputValue | TLazyOutputValue, TLazyInputValue | TLazyOutputValue, TLazyTarget, TLazyOptions> | undefined} lazy lazy function
* @returns {LazyFunction<TLazyInputValue | TLazyOutputValue, TLazyInputValue | TLazyOutputValue, TLazyTarget, TLazyOptions> | undefined} new lazy
*/
static unMemoizeLazy(lazy) {
if (!SerializerMiddleware.isLazy(lazy)) return lazy;
/** @type {LazyFunction<TLazyInputValue | TLazyOutputValue, TLazyInputValue | TLazyOutputValue, TLazyTarget, TLazyOptions>} */
const fn = () => {
throw new Error(
"A lazy value that has been unmemorized can't be called again"
);
};
fn[LAZY_SERIALIZED_VALUE] = SerializerMiddleware.unMemoizeLazy(
/** @type {LazyFunction<TLazyInputValue | TLazyOutputValue, TLazyInputValue | TLazyOutputValue, TLazyTarget, TLazyOptions>} */
(lazy[LAZY_SERIALIZED_VALUE])
);
fn[LAZY_TARGET] = lazy[LAZY_TARGET];
fn.options = lazy.options;
return fn;
}
}
module.exports = SerializerMiddleware;

View File

@ -0,0 +1,40 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
/** @typedef {import("./ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
/** @typedef {import("./ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
class SetObjectSerializer {
/**
* @template T
* @param {Set<T>} obj set
* @param {ObjectSerializerContext} context context
*/
serialize(obj, context) {
context.write(obj.size);
for (const value of obj) {
context.write(value);
}
}
/**
* @template T
* @param {ObjectDeserializerContext} context context
* @returns {Set<T>} date
*/
deserialize(context) {
/** @type {number} */
const size = context.read();
/** @type {Set<T>} */
const set = new Set();
for (let i = 0; i < size; i++) {
set.add(context.read());
}
return set;
}
}
module.exports = SetObjectSerializer;

View File

@ -0,0 +1,36 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
const SerializerMiddleware = require("./SerializerMiddleware");
/** @typedef {EXPECTED_ANY} DeserializedType */
/** @typedef {EXPECTED_ANY[]} SerializedType */
/** @typedef {{}} Context */
/**
* @extends {SerializerMiddleware<DeserializedType, SerializedType, Context>}
*/
class SingleItemMiddleware extends SerializerMiddleware {
/**
* @param {DeserializedType} data data
* @param {Context} context context object
* @returns {SerializedType | Promise<SerializedType> | null} serialized data
*/
serialize(data, context) {
return [data];
}
/**
* @param {SerializedType} data data
* @param {Context} context context object
* @returns {DeserializedType | Promise<DeserializedType>} deserialized data
*/
deserialize(data, context) {
return data[0];
}
}
module.exports = SingleItemMiddleware;

View File

@ -0,0 +1,13 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
/** @typedef {undefined | null | number | string | boolean | Buffer | EXPECTED_OBJECT | (() => ComplexSerializableType[] | Promise<ComplexSerializableType[]>)} ComplexSerializableType */
/** @typedef {undefined | null | number | bigint | string | boolean | Buffer | (() => PrimitiveSerializableType[] | Promise<PrimitiveSerializableType[]>)} PrimitiveSerializableType */
/** @typedef {Buffer | (() => BufferSerializableType[] | Promise<BufferSerializableType[]>)} BufferSerializableType */
module.exports = {};