修复完整历史与运行态内存放大并重建发布包

This commit is contained in:
shiyue
2026-10-14 18:13:02 +08:00
parent 69b6db0f5b
commit 2b1c91488d
28 changed files with 2344 additions and 159 deletions

305
lib/json-persistence.js Normal file
View File

@@ -0,0 +1,305 @@
'use strict';
const { StringDecoder } = require('string_decoder');
const { types } = require('util');
const BUFFER_BYTES = 64 * 1024;
// 读取时直接构造容器和正文,不先拼接整份 JSON 原文。
function createJsonReader(fs, filename) {
const fd = fs.openSync(filename, 'r');
const buffer = Buffer.allocUnsafe(BUFFER_BYTES);
let index = 0;
let length = 0;
let eof = false;
let closed = false;
function available() {
if (index < length) return true;
if (eof) return false;
length = fs.readSync(fd, buffer, 0, buffer.length, null);
index = 0;
eof = length === 0;
return !eof;
}
function whitespace() {
while (available()) {
const ch = buffer[index];
if (ch !== 32 && ch !== 9 && ch !== 10 && ch !== 13) break;
index += 1;
}
}
function peek() {
whitespace();
return available() ? buffer[index] : -1;
}
function invalid() {
throw new SyntaxError('持久化 JSON 结构无效或意外结束');
}
function consume(expected) {
if (peek() !== expected.charCodeAt(0)) invalid();
index += 1;
}
function string() {
consume('"');
const parts = [];
let pending = [];
let pendingChars = 0;
let decoder = new StringDecoder('utf8');
function append(text) {
if (!text) return;
pending.push(text);
pendingChars += text.length;
if (pendingChars >= 8192) {
parts.push(pending.join(''));
pending = [];
pendingChars = 0;
}
}
while (available()) {
const start = index;
while (index < length && buffer[index] >= 32 && buffer[index] !== 34 && buffer[index] !== 92) index += 1;
if (index > start) append(decoder.write(buffer.subarray(start, index)));
if (index === length) continue;
const ch = buffer[index++];
if (ch < 32) invalid();
// 转义字符与引号都是 ASCII,先结束前一段 UTF-8 解码。
append(decoder.end());
if (ch === 34) { parts.push(pending.join('')); return parts.join(''); }
decoder = new StringDecoder('utf8');
if (!available()) invalid();
const escaped = buffer[index++];
const simple = { 34: '"', 92: '\\', 47: '/', 98: '\b', 102: '\f', 110: '\n', 114: '\r', 116: '\t' };
if (Object.prototype.hasOwnProperty.call(simple, escaped)) append(simple[escaped]);
else if (escaped === 117) {
let hex = '';
for (let count = 0; count < 4; count += 1) {
if (!available()) invalid();
hex += String.fromCharCode(buffer[index++]);
}
if (!/^[0-9a-fA-F]{4}$/.test(hex)) invalid();
append(String.fromCharCode(Number.parseInt(hex, 16)));
} else invalid();
}
invalid();
}
function object(field) {
consume('{');
if (peek() === 125) { index += 1; return; }
while (true) {
if (peek() !== 34) invalid();
const key = string();
consume(':');
field(key);
if (peek() === 125) { index += 1; return; }
consume(',');
}
}
function array(item) {
consume('[');
if (peek() === 93) { index += 1; return; }
while (true) {
item(value());
if (peek() === 93) { index += 1; return; }
consume(',');
}
}
function value() {
const first = peek();
if (first === 34) return string();
if (first === 123) {
const result = {};
object(key => Object.defineProperty(result, key, {
value: value(), enumerable: true, writable: true, configurable: true,
}));
return result;
}
if (first === 91) {
const result = [];
array(item => result.push(item));
return result;
}
const parts = [];
while (available()) {
const start = index;
while (index < length && ![44, 125, 93, 32, 9, 10, 13].includes(buffer[index])) index += 1;
if (index > start) parts.push(buffer.toString('utf8', start, index));
if (index < length) break;
}
const token = parts.join('');
if (token === 'null') return null;
if (token === 'true') return true;
if (token === 'false') return false;
if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(token)) invalid();
return Number(token);
}
return {
value, object, array,
end() { if (peek() !== -1) invalid(); },
close() { if (!closed) { closed = true; fs.closeSync(fd); } },
};
}
function readJsonSync(fs, filename) {
const reader = createJsonReader(fs, filename);
try {
const result = reader.value();
reader.end();
return result;
} finally {
reader.close();
}
}
// 与历史持久化既有 replacer 保持一致;toJSON 在项目类型转换之前执行。
function jsonValue(value, key) {
if (value !== null && (typeof value === 'object' || typeof value === 'function' || typeof value === 'bigint')) {
const toJSON = value.toJSON;
if (typeof toJSON === 'function') value = toJSON.call(value, key);
}
if (typeof value === 'bigint') return String(value);
if (value instanceof Map) value = Object.fromEntries(value);
else if (value instanceof Set) value = [...value];
if (value && typeof value === 'object') {
if (types.isNumberObject(value)) value = Number(value);
else if (types.isStringObject(value)) value = String(value);
else if (types.isBooleanObject(value)) value = Boolean.prototype.valueOf.call(value);
else if (types.isBigIntObject(value)) throw new TypeError('JSON 不支持包装的 bigint');
}
if (typeof value === 'number' && !Number.isFinite(value)) return null;
if (value === 0) return 0;
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') return undefined;
return value;
}
function cloneJsonValue(value) {
const ancestors = new Set();
function clone(item, key) {
item = jsonValue(item, key);
if (item === null || typeof item !== 'object') return item;
if (ancestors.has(item)) throw new TypeError('JSON 不支持循环引用');
ancestors.add(item);
try {
if (Array.isArray(item)) {
const result = [];
const length = item.length;
for (let index = 0; index < length; index += 1) result.push(clone(item[index], String(index)) ?? null);
return result;
}
const result = {};
for (const name of Object.keys(item)) {
const next = clone(item[name], name);
if (next !== undefined) Object.defineProperty(result, name, {
value: next, enumerable: true, writable: true, configurable: true,
});
}
return result;
} finally {
ancestors.delete(item);
}
}
return clone(value, '');
}
function writeJsonSync(fs, fd, value) {
const ancestors = new Set();
const output = Buffer.allocUnsafe(BUFFER_BYTES);
let used = 0;
function flush() {
let offset = 0;
while (offset < used) {
const written = fs.writeSync(fd, output, offset, used - offset, null);
if (!Number.isInteger(written) || written <= 0) throw new Error('持久化 JSON 写入未取得进展');
offset += written;
}
used = 0;
}
function append(text) {
const bytes = Buffer.from(text, 'utf8');
for (let offset = 0; offset < bytes.length;) {
const count = Math.min(output.length - used, bytes.length - offset);
bytes.copy(output, used, offset, offset + count);
offset += count;
used += count;
if (used === output.length) flush();
}
}
function string(text) {
append('"');
for (let start = 0; start < text.length;) {
let end = Math.min(text.length, start + 8192);
// 不能在分段转义处拆开 UTF-16 代理对,否则会改变实际 JSON 字节。
if (end < text.length && text.charCodeAt(end - 1) >= 0xd800 && text.charCodeAt(end - 1) <= 0xdbff
&& text.charCodeAt(end) >= 0xdc00 && text.charCodeAt(end) <= 0xdfff) end -= 1;
append(JSON.stringify(text.slice(start, end)).slice(1, -1));
start = end;
}
append('"');
}
function write(item, key, arrayItem = false, prepared = false) {
if (!prepared) item = jsonValue(item, key);
if (item === undefined) {
if (arrayItem) { append('null'); return true; }
return false;
}
if (item === null) append('null');
else if (typeof item === 'string') string(item);
else if (typeof item !== 'object') append(String(item));
else {
if (ancestors.has(item)) throw new TypeError('JSON 不支持循环引用');
ancestors.add(item);
try {
if (Array.isArray(item)) {
append('[');
const length = item.length;
for (let index = 0; index < length; index += 1) {
if (index) append(',');
write(item[index], String(index), true);
}
append(']');
} else {
append('{');
let first = true;
for (const name of Object.keys(item)) {
// 每个字段只求值一次,避免 getter 或 toJSON 重复执行。
const next = jsonValue(item[name], name);
if (next === undefined) continue;
if (!first) append(',');
first = false;
string(name);
append(':');
write(next, name, false, true);
}
append('}');
}
} finally {
ancestors.delete(item);
}
}
return true;
}
if (!write(value, '')) throw new TypeError('顶层值不可持久化为 JSON');
flush();
}
// 合并输入已是规范 JSON 值;直接比较结构,避免为长正文制造序列化副本。
function jsonValueEqual(left, right) {
if (left === right) return true;
if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object') return false;
if (Array.isArray(left) !== Array.isArray(right)) return false;
if (Array.isArray(left)) return left.length === right.length && left.every((item, index) => jsonValueEqual(item, right[index]));
const keys = Object.keys(left);
return keys.length === Object.keys(right).length && keys.every(key => Object.prototype.hasOwnProperty.call(right, key)
&& jsonValueEqual(left[key], right[key]));
}
module.exports = { createJsonReader, readJsonSync, writeJsonSync, cloneJsonValue, jsonValueEqual };