修复完整历史与运行态内存放大并重建发布包
This commit is contained in:
305
lib/json-persistence.js
Normal file
305
lib/json-persistence.js
Normal 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 };
|
||||
Reference in New Issue
Block a user