283 lines
7.1 KiB
JavaScript
283 lines
7.1 KiB
JavaScript
|
|
// grid-db/src/api/grid-api.js
|
|||
|
|
// Grid-DB · API 层
|
|||
|
|
// 统一 CRUD / Query 接口
|
|||
|
|
// PRJ-GDB-001 · Phase 0 · ZY-GDB-P0-005
|
|||
|
|
// 版权:国作登字-2026-A-00037559
|
|||
|
|
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const GridCell = require('../core/grid-cell');
|
|||
|
|
const WAL = require('../storage/wal');
|
|||
|
|
const PageManager = require('../storage/page-manager');
|
|||
|
|
const EventLog = require('../events/event-log');
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* GridAPI — Grid-DB 统一接口
|
|||
|
|
*
|
|||
|
|
* 提供 put / get / delete / scan 等操作,
|
|||
|
|
* 内部协调 WAL、PageManager、EventLog 协同工作。
|
|||
|
|
*
|
|||
|
|
* 本体论锚定:API = 笔的握持接口。
|
|||
|
|
* 无论笔内部结构如何,写字的人只需要握住笔就能写字。
|
|||
|
|
*/
|
|||
|
|
class GridAPI {
|
|||
|
|
/**
|
|||
|
|
* @param {object} config
|
|||
|
|
* @param {string} config.dataDir 数据目录
|
|||
|
|
* @param {number} [config.pageSize] 页大小
|
|||
|
|
*/
|
|||
|
|
constructor(config) {
|
|||
|
|
this._dataDir = config.dataDir;
|
|||
|
|
this._pageSize = config.pageSize || PageManager.DEFAULT_PAGE_SIZE;
|
|||
|
|
|
|||
|
|
// 核心组件
|
|||
|
|
this._wal = new WAL(`${this._dataDir}/grid.wal`);
|
|||
|
|
this._pageManager = new PageManager(`${this._dataDir}/grid.gdb`, {
|
|||
|
|
pageSize: this._pageSize
|
|||
|
|
});
|
|||
|
|
this._eventLog = new EventLog();
|
|||
|
|
|
|||
|
|
// 内存索引:key → pageId
|
|||
|
|
this._index = new Map();
|
|||
|
|
|
|||
|
|
// 从 WAL 恢复
|
|||
|
|
this._recoverFromWAL();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 写入格点数据
|
|||
|
|
* @param {string} namespace 命名空间
|
|||
|
|
* @param {GridCell|object} gridCell 格点(或 { gridX, gridY, layer })
|
|||
|
|
* @param {*} data 数据
|
|||
|
|
* @returns {object} { key, seqNo, event }
|
|||
|
|
*/
|
|||
|
|
put(namespace, gridCell, data) {
|
|||
|
|
const cell = this._resolveCell(namespace, gridCell);
|
|||
|
|
const key = cell.toKey();
|
|||
|
|
const dataBuf = Buffer.from(JSON.stringify(data));
|
|||
|
|
|
|||
|
|
// 1. 先写 WAL
|
|||
|
|
const seqNo = this._wal.appendPut(key, dataBuf);
|
|||
|
|
|
|||
|
|
// 2. 写入 PageManager
|
|||
|
|
let pageId = this._index.get(key);
|
|||
|
|
if (pageId !== undefined) {
|
|||
|
|
// 更新现有页
|
|||
|
|
this._pageManager.writePage(pageId, dataBuf);
|
|||
|
|
} else {
|
|||
|
|
// 分配新页
|
|||
|
|
pageId = this._pageManager.allocPage();
|
|||
|
|
this._pageManager.writePage(pageId, dataBuf);
|
|||
|
|
this._index.set(key, pageId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 记录事件
|
|||
|
|
const event = this._eventLog.append(namespace, 'put', key, {
|
|||
|
|
dataSize: dataBuf.length
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return { key, seqNo, event };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 读取格点数据
|
|||
|
|
* @param {string} namespace 命名空间
|
|||
|
|
* @param {GridCell|object} gridCell 格点
|
|||
|
|
* @returns {*|null} 数据,不存在返回 null
|
|||
|
|
*/
|
|||
|
|
get(namespace, gridCell) {
|
|||
|
|
const cell = this._resolveCell(namespace, gridCell);
|
|||
|
|
const key = cell.toKey();
|
|||
|
|
const pageId = this._index.get(key);
|
|||
|
|
|
|||
|
|
if (pageId === undefined) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const dataBuf = this._pageManager.readPage(pageId);
|
|||
|
|
if (!dataBuf) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(dataBuf.toString('utf-8'));
|
|||
|
|
} catch {
|
|||
|
|
return dataBuf;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 删除格点数据
|
|||
|
|
* @param {string} namespace 命名空间
|
|||
|
|
* @param {GridCell|object} gridCell 格点
|
|||
|
|
* @returns {boolean} 是否删除成功
|
|||
|
|
*/
|
|||
|
|
delete(namespace, gridCell) {
|
|||
|
|
const cell = this._resolveCell(namespace, gridCell);
|
|||
|
|
const key = cell.toKey();
|
|||
|
|
const pageId = this._index.get(key);
|
|||
|
|
|
|||
|
|
if (pageId === undefined) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 1. 先写 WAL
|
|||
|
|
this._wal.appendDelete(key);
|
|||
|
|
|
|||
|
|
// 2. 释放页
|
|||
|
|
this._pageManager.freePage(pageId);
|
|||
|
|
this._index.delete(key);
|
|||
|
|
|
|||
|
|
// 3. 记录事件
|
|||
|
|
this._eventLog.append(namespace, 'delete', key);
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 范围扫描(Phase 0 简版:基于内存索引)
|
|||
|
|
* @param {string} namespace
|
|||
|
|
* @param {object} range { xRange: [min, max], yRange: [min, max], layer }
|
|||
|
|
* @returns {Array<{ cell: GridCell, data: * }>}
|
|||
|
|
*/
|
|||
|
|
scan(namespace, range = {}) {
|
|||
|
|
const results = [];
|
|||
|
|
|
|||
|
|
for (const [key, pageId] of this._index) {
|
|||
|
|
try {
|
|||
|
|
const cell = GridCell.fromKey(key);
|
|||
|
|
|
|||
|
|
if (cell.namespace !== namespace) continue;
|
|||
|
|
if (range.layer && cell.layer !== range.layer) continue;
|
|||
|
|
|
|||
|
|
if (range.xRange) {
|
|||
|
|
if (cell.gridX < range.xRange[0] || cell.gridX > range.xRange[1]) continue;
|
|||
|
|
}
|
|||
|
|
if (range.yRange) {
|
|||
|
|
if (cell.gridY < range.yRange[0] || cell.gridY > range.yRange[1]) continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const dataBuf = this._pageManager.readPage(pageId);
|
|||
|
|
if (dataBuf) {
|
|||
|
|
let data;
|
|||
|
|
try { data = JSON.parse(dataBuf.toString('utf-8')); } catch { data = dataBuf; }
|
|||
|
|
results.push({ cell, data });
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
// 跳过无效键
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this._eventLog.append(namespace, 'scan', `scan:${namespace}`, {
|
|||
|
|
resultCount: results.length
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return results;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取命名空间统计
|
|||
|
|
* @param {string} [namespace] 不传则统计全部
|
|||
|
|
* @returns {object}
|
|||
|
|
*/
|
|||
|
|
stats(namespace) {
|
|||
|
|
let count = 0;
|
|||
|
|
for (const key of this._index.keys()) {
|
|||
|
|
if (!namespace || key.startsWith(namespace + ':')) {
|
|||
|
|
count++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
namespace: namespace || '*',
|
|||
|
|
cellCount: count,
|
|||
|
|
wal: this._wal.getStatus(),
|
|||
|
|
pageManager: this._pageManager.getStatus(),
|
|||
|
|
eventLog: this._eventLog.getStatus()
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 订阅事件流
|
|||
|
|
* @param {string} subscriberId
|
|||
|
|
* @param {Function} handler
|
|||
|
|
* @returns {Function} 取消订阅函数
|
|||
|
|
*/
|
|||
|
|
subscribe(subscriberId, handler) {
|
|||
|
|
return this._eventLog.subscribe(subscriberId, handler);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 执行 checkpoint:截断 WAL
|
|||
|
|
*/
|
|||
|
|
checkpoint() {
|
|||
|
|
this._wal.truncate();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 关闭数据库
|
|||
|
|
*/
|
|||
|
|
close() {
|
|||
|
|
this._wal.close();
|
|||
|
|
this._pageManager.close();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 内部方法 ──
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 解析格点参数
|
|||
|
|
* @param {string} namespace
|
|||
|
|
* @param {GridCell|object} input
|
|||
|
|
* @returns {GridCell}
|
|||
|
|
*/
|
|||
|
|
_resolveCell(namespace, input) {
|
|||
|
|
if (input instanceof GridCell) {
|
|||
|
|
return input;
|
|||
|
|
}
|
|||
|
|
return new GridCell(
|
|||
|
|
namespace,
|
|||
|
|
input.gridX ?? input.grid_x ?? 0,
|
|||
|
|
input.gridY ?? input.grid_y ?? 0,
|
|||
|
|
input.layer || 'raw'
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 从 WAL 恢复数据到内存索引
|
|||
|
|
*/
|
|||
|
|
_recoverFromWAL() {
|
|||
|
|
const entries = this._wal.recover();
|
|||
|
|
|
|||
|
|
for (const entry of entries) {
|
|||
|
|
if (entry.op === WAL.OP_PUT) {
|
|||
|
|
// 检查是否已有页
|
|||
|
|
let pageId = this._index.get(entry.key);
|
|||
|
|
if (pageId !== undefined) {
|
|||
|
|
// 页已存在,更新
|
|||
|
|
try {
|
|||
|
|
this._pageManager.writePage(pageId, entry.data);
|
|||
|
|
} catch {
|
|||
|
|
// 页可能已损坏,分配新页
|
|||
|
|
pageId = this._pageManager.allocPage();
|
|||
|
|
this._pageManager.writePage(pageId, entry.data);
|
|||
|
|
this._index.set(entry.key, pageId);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 分配新页并写入
|
|||
|
|
pageId = this._pageManager.allocPage();
|
|||
|
|
this._pageManager.writePage(pageId, entry.data);
|
|||
|
|
this._index.set(entry.key, pageId);
|
|||
|
|
}
|
|||
|
|
} else if (entry.op === WAL.OP_DELETE) {
|
|||
|
|
const pageId = this._index.get(entry.key);
|
|||
|
|
if (pageId !== undefined) {
|
|||
|
|
this._pageManager.freePage(pageId);
|
|||
|
|
this._index.delete(entry.key);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = GridAPI;
|