diff --git a/CHANGELOG.md b/CHANGELOG.md index 85b8898..c18004e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,12 @@ All notable changes to MetonaSqlark will be documented in this file. ### Added +- **AriaEngine 可选自研 KVStore 后端**(`storageBackend: 'kv'` / `diskEngine: 'kv'`): + - aria 完全跑在自研存储栈上(KVStore 日志+快照),不再依赖浏览器 OPFS API(Node/测试用 SharedMemory 介质) + - KVStore 新增 APPEND 日志操作类型:aria WAL 分片追加 O(chunk) 高效,恢复按序拼接,checkpoint 快照含最终值 + - writeMany/deleteMany 经 KVStore 单日志记录真原子(此前 OPFS 逐文件写) + - aria 崩溃恢复 = KVStore 双层恢复 + WAL 重放;二级索引/页面化兼容(kv 后端 SSTable 整 value 存储) + - 10 个 aria+kv 集成测试(后端单元/CRUD 持久化/WAL 追加恢复/checkpoint 混合/索引/大数据崩溃/高层 API 事务级联) - 27 个生产异常场景测试(`tests/production-abnormal.test.ts`): - KVStore 边界:空 value/空 key、1MB 大 value、中文/emoji/冒号/超长 key、未 open、 重复 open/close 幂等、clear 后写、1 万小 key、写失败后继续可用 diff --git a/README.md b/README.md index 36f16d1..32f720f 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@
-
-
+
+
o+4){m=!1;break}const e=u.getUint8(f);f+=1;const t=u.getUint32(f,!1);if(f+=4,f+t+4>o+4){m=!1;break}const s=a.decode(l.subarray(f,f+t));f+=t;const n=u.getUint32(f,!1);if(f+=4,f+n>o+4){m=!1;break}const i=l.slice(f,f+n).buffer;f+=n,y.push({op:e,key:s,value:i})}if(!m){if(s&&!s(h))break;break}t({seq:d,entries:y,raw:l}),i++,n=c}return i}(e,e=>{e.seq<=s||(this.applyRecord(e.entries),this.seq=e.seq)},e=>(i.push(e),!0))>0||i.length>0)&&(this.logBytes=e.byteLength),i.length>0&&await this.truncateLog()}this.opened=!0}async reload(){if(this.opened){try{await this.opQueue}catch{}this.index=new Map,this.seq=0,this.logBytes=0,this.opened=!1,await this.open(this.dbName)}}async close(){if(this.opened){try{await this.opQueue}catch{}await this.medium.close(),this.index.clear(),this.seq=0,this.logBytes=0,this.opened=!1}}async get(e){return this.index.get(e)??null}async getAll(){return Array.from(this.index.entries())}async listKeys(){return Array.from(this.index.keys())}async exists(e){return this.index.has(e)}size(){return this.index.size}async put(e,t){await this.enqueue(async()=>{await this.appendRecord({[e]:t},[])})}async putMany(e){0!==Object.keys(e).length&&await this.enqueue(async()=>{await this.appendRecord(e,[])})}async delete(e){await this.enqueue(async()=>{await this.appendRecord({},[e])})}async deleteMany(e){0!==e.length&&await this.enqueue(async()=>{await this.appendRecord({},e)})}async checkpoint(){await this.enqueue(async()=>{if(null!==this.lastBackgroundError){const e=this.lastBackgroundError;throw this.lastBackgroundError=null,new n("KVStore background write failed","KV_BACKGROUND_ERROR",e)}if(0===this.logBytes&&0===this.index.size)return;const e=k(this.seq,this.index);await this.medium.write(I,e.buffer);const t={seq:this.seq};await this.medium.write(A,(new TextEncoder).encode(JSON.stringify(t)).buffer),await this.truncateLog()})}async clear(){await this.enqueue(async()=>{await this.medium.clear(),this.index.clear(),this.seq=0,this.logBytes=0,await this.medium.write(A,(new TextEncoder).encode(JSON.stringify({seq:0})).buffer)})}async repair(){return this.enqueue(async()=>{let e=0;const t=await this.medium.read(I);t&&!x(new Uint8Array(t))&&(await this.medium.delete(I),e++);const s=await this.medium.read(S);if(s&&s.byteLength>0){const t=new Uint8Array(s),n=this.findValidLogLength(t);if(n o+4){m=!1;break}const e=u.getUint8(f);f+=1;const t=u.getUint32(f,!1);if(f+=4,f+t+4>o+4){m=!1;break}const s=a.decode(l.subarray(f,f+t));f+=t;const n=u.getUint32(f,!1);if(f+=4,f+n>o+4){m=!1;break}const i=l.slice(f,f+n).buffer;f+=n,y.push({op:e,key:s,value:i})}if(!m){if(s&&!s(h))break;break}t({seq:d,entries:y,raw:l}),i++,n=c}return i}(e,e=>{e.seq<=s||(this.applyRecord(e.entries),this.seq=e.seq)},e=>(i.push(e),!0))>0||i.length>0)&&(this.logBytes=e.byteLength),i.length>0&&await this.truncateLog()}this.opened=!0}async reload(){if(this.opened){try{await this.opQueue}catch{}this.index=new Map,this.seq=0,this.logBytes=0,this.opened=!1,await this.open(this.dbName)}}async close(){if(this.opened){try{await this.opQueue}catch{}await this.medium.close(),this.index.clear(),this.seq=0,this.logBytes=0,this.opened=!1}}async get(e){return this.index.get(e)??null}async getAll(){return Array.from(this.index.entries())}async listKeys(){return Array.from(this.index.keys())}async exists(e){return this.index.has(e)}size(){return this.index.size}async put(e,t){await this.enqueue(async()=>{await this.appendRecord({[e]:t},[])})}async putMany(e){0!==Object.keys(e).length&&await this.enqueue(async()=>{await this.appendRecord(e,[])})}async delete(e){await this.enqueue(async()=>{await this.appendRecord({},[e])})}async deleteMany(e){0!==e.length&&await this.enqueue(async()=>{await this.appendRecord({},e)})}async appendValue(e,t){0!==t.byteLength&&await this.enqueue(async()=>{await this.appendRecord({},[],{[e]:t})})}async checkpoint(){await this.enqueue(async()=>{if(null!==this.lastBackgroundError){const e=this.lastBackgroundError;throw this.lastBackgroundError=null,new n("KVStore background write failed","KV_BACKGROUND_ERROR",e)}if(0===this.logBytes&&0===this.index.size)return;const e=k(this.seq,this.index);await this.medium.write(I,e.buffer);const t={seq:this.seq};await this.medium.write(A,(new TextEncoder).encode(JSON.stringify(t)).buffer),await this.truncateLog()})}async clear(){await this.enqueue(async()=>{await this.medium.clear(),this.index.clear(),this.seq=0,this.logBytes=0,await this.medium.write(A,(new TextEncoder).encode(JSON.stringify({seq:0})).buffer)})}async repair(){return this.enqueue(async()=>{let e=0;const t=await this.medium.read(I);t&&!x(new Uint8Array(t))&&(await this.medium.delete(I),e++);const s=await this.medium.read(S);if(s&&s.byteLength>0){const t=new Uint8Array(s),n=this.findValidLogLength(t);if(n=this.maxSize}getAllEntries(){return this.tree.getAllEntries()}rangeScan(e,t){const s=[];return this.tree.rangeScan(e,t,(e,t)=>s.push([e,t])),s}getEntryCount(){return this.tree.size}getEstimatedSize(){return this._estimatedSize}clear(){this.tree.clear(),this._estimatedSize=0}estimateEntrySize(e,t){if(!t)return 0;let s=2*e.length;for(const e of Object.entries(t)){s+=2*e[0].length;const t=e[1];s+="string"==typeof t?2*t.length:"number"==typeof t?8:"boolean"==typeof t||null==t?1:16}return s}}class V{constructor(e,t=10){this._inserted=0;const s=Math.max(64,e*t),n=Math.ceil(s/8);this.bits=new Uint8Array(n),this.numHashes=Math.max(1,Math.floor(.69*t))}static fromData(e,t){const s=new V(1);return s.bits=e,s.numHashes=t,s}insert(e){const t=this.getHashes(e);for(const e of t){const t=Math.floor(e/8),s=e%8;this.bits[t]|=1<>>0;return t}murmurSimple(e){let t=0;for(let s=0;sthis.data.byteLength);t++){const t=this.readLen(e);if((e+=n)+t+8>this.data.byteLength)break;const s=(new TextDecoder).decode(this.data.slice(e,e+t));e+=t;const i=this.view.getUint32(e,!1);e+=4;const r=this.view.getUint32(e,!1);e+=4,0===r||i+r>this.data.byteLength||this.indexEntries.push({key:s,blockOffset:i,blockSize:r})}}getBlockData(e){return e.blockSize<=0||e.blockOffset<0||e.blockOffset+e.blockSize>this.data.byteLength?null:new Uint8Array(this.data.buffer,this.data.byteOffset+e.blockOffset,e.blockSize)}locateBlock(e){let t=0,s=this.indexEntries.length-1;for(;t<=s;){const n=Math.floor((t+s)/2);if(e<=this.indexEntries[n].key){if(e>(0===n?"":this.indexEntries[n-1].key))return n;s=n-1}else t=n+1}return-1}locateBlockGE(e){let t=0,s=this.indexEntries.length;for(;t>1;this.indexEntries[n].keye+t.byteLength,0),r=new Uint8Array(Math.min(i,s));let a=0;for(const e of n){const t=Math.min(e.byteLength,r.byteLength-a);if(t<=0)break;r.set(e.subarray(0,t),a),a+=t}return this.pageIds.delete(e),r}async delete(e,t){for(const e of t){this.bufferPool.removePage(e);try{await this.fileManager.freePageId(e)}catch{}}this.pageIds.delete(e)}}class me{constructor(e){this.nextPageId=0,this.metaLoaded=!1,this.dbName="",this.backend=e}async init(e){this.dbName=e;const t=await this.backend.read("__aria_meta");if(t&&t instanceof ArrayBuffer&&t.byteLength>=4){const e=new DataView(t);this.nextPageId=e.getUint32(0,!1)}else this.nextPageId=1,await this.saveMeta();this.metaLoaded=!0}async readPage(e){const t=`pg_${e}`,s=await this.backend.read(t);if(!s)return null;if(s.byteLength<_){const e=new ArrayBuffer(_);return new Uint8Array(e).set(new Uint8Array(s)),e}return s}async writePage(e,t){const s=`pg_${e}`;await this.backend.write(s,t)}async allocatePageId(){const e=this.nextPageId++;return await this.saveMeta(),e}async allocatePageIds(e){if(e<=0)return[];const t=[],s=this.nextPageId;this.nextPageId+=e;for(let n=0;nt.name===e);-1!==t&&(this.plugins[t].destroy(),this.plugins.splice(t,1))}getPlugins(){return[...this.plugins]}on(e,t){const s=this.hooks.get(e)??[];s.push(t),this.hooks.set(e,s)}off(e,t){const s=this.hooks.get(e);if(s){const e=s.indexOf(t);-1!==e&&s.splice(e,1)}}async trigger(e,...t){const s=this.hooks.get(e);if(s)for(const e of s)await e(...t)}destroy(){for(const e of this.plugins)try{e.destroy()}catch(e){}this.plugins=[],this.hooks.clear()}}class Fe{get version(){return this._version}get maxRowsPerQuery(){return this.config.maxRowsPerQuery??0}get debug(){return this.config.debug??!1}constructor(e){this.ready=!1,this.tableCache=new Map,this.channel=null,this.listeners=new Map,this.migrations=new Map,this.config=e,this.name=e.name??s.name,this.mode=e.mode??s.mode,this._version=e.version??s.version,this.pluginManager=new ze,e.multiTabSync&&"undefined"!=typeof BroadcastChannel&&(this.channel=new BroadcastChannel(`metona-sqlark:${this.name}`),this.channel.onmessage=e=>{const t=e.data;t&&"change"===t.type&&(this.emit(t.table??"",{type:"external",table:t.table??""}),this.engine instanceof xe&&this.engine.reloadMemoryFromDisk().catch(()=>{}))})}async init(){if(this.engine=this.createEngine(),await this.engine.open(this.name,this.version),"function"==typeof this.engine.getMeta)try{const e=await this.engine.getMeta("__metona_version");null!=e&&Number(e)>=1&&(this._version=Math.max(this._version,Math.floor(Number(e))))}catch{}if(this.executor=new Be(this.engine,this.maxRowsPerQuery),this.transactionManager=new Ke(this.engine),this.config.plugins)for(const e of this.config.plugins)this.pluginManager.register(e,this);this.ready=!0,this.config.onReady&&this.config.onReady(this)}isReady(){return this.ready}async defineTable(e,t){this.ensureReady();const s=$(e,t);try{await this.pluginManager.trigger("beforeCreateTable",s),await this.engine.createTable(s),await this.pluginManager.trigger("afterCreateTable",s)}catch(e){throw this._onError(e),e}this.tableCache.delete(e)}table(e){this.ensureReady();let t=this.tableCache.get(e);return t||(t=new Re(this.engine,e,this.executor,e=>this.broadcastChange(e),(e,t)=>this.pluginManager.trigger(e,...t)),this.tableCache.set(e,t)),t}async dropTable(e){this.ensureReady();try{await this.pluginManager.trigger("beforeDropTable",e),await this.engine.dropTable(e),await this.pluginManager.trigger("afterDropTable",e)}catch(e){throw this._onError(e),e}this.tableCache.delete(e)}async getTableNames(){return this.ensureReady(),this.engine.getTableNames()}async query(e){this.ensureReady();const t=this.debug?Date.now():0;let s;await this.pluginManager.trigger("beforeQuery",e);try{const t=$e(e);for(const e of t){await this.triggerStatementHooks(e,"before"),s=await this.executor.execute(e),await this.triggerStatementHooks(e,"after",s);const t=this.writeStatementTable(e);t&&this.broadcastChange(t)}}catch(e){throw this._onError(e),e}if(await this.pluginManager.trigger("afterQuery",e,s),this.debug){const n=Date.now()-t,i=Array.isArray(s)?s.length:0;this._debug(`query [${n}ms] ${i} rows: ${e.slice(0,100)}`)}return s}async queryStream(e,t){this.ensureReady();const s=$e(e)[0];if(!s||"SELECT"!==s.type)throw new n("queryStream only supports SELECT statements","NOT_SUPPORTED");const i=s,r=i.columns.some(e=>/^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(e));if(!(i.joins||i.groupBy||i.having||i.distinct||r||i.orderBy&&i.orderBy.length>0||i.where&&void 0!==i.where.$exists)&&"function"==typeof this.engine.findStream&&"AsyncFunction"!==t.constructor?.name){const e=this.normalizeWhereForStream(i),s=i.columns.filter(e=>!/\s+AS\s+\w+$/i.test(e));return this.engine.findStream(i.from,{table:i.from,columns:s.length>0&&"*"!==s[0]?s:["*"],where:e&&Object.keys(e).length>0?e:void 0,limit:i.limit,offset:i.offset},t)}const a=await this.query(e);if(Array.isArray(a)){for(const e of a)await t(e);return a.length}return 0}normalizeWhereForStream(e){const t=[e.alias??e.from].filter(Boolean),s=e=>{for(const s of t)if(e.startsWith(`${s}.`))return e.slice(s.length+1);return e},n=e=>{const t={};for(const[i,r]of Object.entries(e))"$and"===i||"$or"===i?t[i]=r.map(n):"$not"===i&&"object"==typeof r&&null!==r?t.$not=n(r):t[s(i)]=r;return t};return n(e.where??{})}async transaction(e){this.ensureReady(),await this.pluginManager.trigger("beforeTransaction");try{const t=await this.transactionManager.execute(e);return await this.pluginManager.trigger("afterTransaction"),t}catch(e){throw this._onError(e),e}}async exportTable(e){return this.ensureReady(),this.engine.find(e,{table:e})}async importTable(e,t){this.ensureReady();try{return await this.engine.insert(e,t)}catch(e){throw this._onError(e),e}}async exportAll(){this.ensureReady();const e={},t=await this.engine.getTableNames();for(const s of t)e[s]=await this.engine.find(s,{table:s});return e}async backup(){return this.ensureReady(),"function"==typeof this.engine.backup?this.engine.backup():this.exportAll()}subscribe(e,t){const s=`change:${e}`;return this.listeners.has(s)||this.listeners.set(s,new Set),this.listeners.get(s).add(t),()=>this.listeners.get(s)?.delete(t)}emit(e,t){const s=`change:${e}`;this.listeners.get(s)?.forEach(e=>e(t))}broadcastChange(e){if(this.channel)try{this.channel.postMessage({type:"change",table:e})}catch{}}writeStatementTable(e){switch(e.type){case"INSERT":return e.into;case"UPDATE":case"CREATE_INDEX":case"DROP_INDEX":return e.table;case"DELETE":return e.from;case"CREATE_TABLE":case"DROP_TABLE":case"TRUNCATE_TABLE":case"ALTER_TABLE":return e.name;default:return null}}async triggerStatementHooks(e,t,s){switch(e.type){case"INSERT":{const n=(e.values??[]).map(t=>{const s={},n=e.columns??[];for(let e=0;e{for(const{column:n,direction:i,nulls:r}of t){const t=null===e[n]||void 0===e[n],a=null===s[n]||void 0===s[n];if(r&&(t||a)){if(t&&a)continue;return"first"===r?t?-1:1:t?1:-1}const o=l(e[n],s[n]);if(0!==o)return"desc"===i?-o:o}return 0})}function l(e,t){return e===t?0:null==e?1:null==t?-1:"string"==typeof e&&"string"==typeof t?e.localeCompare(t):"number"==typeof e&&"number"==typeof t?e-t:String(e).localeCompare(String(t))}function u(e,t){const s={};for(const n of t)if(n in e)s[n]=e[n];else for(const t of Object.keys(e))if(t.endsWith(`.${n}`)||t===n){s[n]=e[t];break}return s}class f{constructor(){this.name="memory",this.tables=new Map,this.schemas=new Map,this.indexes=new Map,this.opened=!1,this.metaStore=new Map,this.snapshot=null}async open(e,t){this.opened||(this.opened=!0)}async close(){this.tables.clear(),this.schemas.clear(),this.indexes.clear(),this.metaStore.clear(),this.opened=!1}isOpen(){return this.opened}async repair(){}async clearAll(){const e=Array.from(this.schemas.keys());for(const t of e)await this.dropTable(t);this.metaStore.clear()}async getMeta(e){return this.metaStore.get(e)??null}async setMeta(e,t){this.metaStore.set(e,t)}async createTable(e){if(this.schemas.has(e.name))throw new n(`Table "${e.name}" already exists`,"TABLE_EXISTS");const t={name:e.name,columns:{}};for(const[s,n]of Object.entries(e.columns))t.columns[s]={...n};this.schemas.set(e.name,t),this.tables.set(e.name,new Map);const s=new Map;for(const[e,n]of Object.entries(t.columns))(n.index||n.unique)&&s.set(e,new Map);this.indexes.set(e.name,s)}async dropTable(e){this.ensureTable(e),this.schemas.delete(e),this.tables.delete(e),this.indexes.delete(e)}async hasTable(e){return this.schemas.has(e)}async getTableNames(){return Array.from(this.schemas.keys())}async getTableSchema(e){return this.schemas.get(e)??null}async alterTable(e,t,s){this.ensureTable(e);const i=this.schemas.get(e);if("ADD"===t){if(i.columns[s.name])throw new n(`Column "${s.name}" already exists in table "${e}"`,"COLUMN_EXISTS");return void(i.columns[s.name]=s)}if(!i.columns[s.name])throw new n(`Column "${s.name}" does not exist in table "${e}"`,"COLUMN_NOT_FOUND");delete i.columns[s.name];const r=this.tables.get(e);for(const e of r.values())s.name in e&&delete e[s.name]}async insert(e,t){this.ensureTable(e);const s=this.schemas.get(e),i=this.tables.get(e),r=this.getPrimaryKey(s),a=[];for(const o of t){const t=this.validateRow(s,o),h=String(t[r]);if(i.has(h))throw new n(`Duplicate primary key "${h}" in table "${e}"`,"DUPLICATE_KEY");this.checkUniqueness(s,t),i.set(h,t),this.updateIndexes(e,t,h),a.push(h)}return a}async find(e,t){this.ensureTable(e);const s=this.tables.get(e);let n=this.tryIndexLookup(e,s,t);t.where&&Object.keys(t.where).length>0&&(n=n.filter(e=>a(e,t.where))),t.orderBy&&t.orderBy.length>0&&(n=c(n,t.orderBy));const i=t.offset??0,r=t.limit??n.length;return n=n.slice(i,i+r),t.columns&&t.columns.length>0&&"*"!==t.columns[0]&&(n=n.map(e=>u(e,t.columns))),n}async findStream(e,t,s){this.ensureTable(e);const n=this.tables.get(e),i=!!(t.where&&Object.keys(t.where).length>0),r=t.limit??1/0,o=t.offset??0,h=t.columns&&t.columns.length>0&&"*"!==t.columns[0]?e=>u(e,t.columns):null;let c=0,l=0;for(const e of n.values())if(!i||a(e,t.where))if(l=this.maxSize}getAllEntries(){return this.tree.getAllEntries()}rangeScan(e,t){const s=[];return this.tree.rangeScan(e,t,(e,t)=>s.push([e,t])),s}getEntryCount(){return this.tree.size}getEstimatedSize(){return this._estimatedSize}clear(){this.tree.clear(),this._estimatedSize=0}estimateEntrySize(e,t){if(!t)return 0;let s=2*e.length;for(const e of Object.entries(t)){s+=2*e[0].length;const t=e[1];s+="string"==typeof t?2*t.length:"number"==typeof t?8:"boolean"==typeof t||null==t?1:16}return s}}class V{constructor(e,t=10){this._inserted=0;const s=Math.max(64,e*t),n=Math.ceil(s/8);this.bits=new Uint8Array(n),this.numHashes=Math.max(1,Math.floor(.69*t))}static fromData(e,t){const s=new V(1);return s.bits=e,s.numHashes=t,s}insert(e){const t=this.getHashes(e);for(const e of t){const t=Math.floor(e/8),s=e%8;this.bits[t]|=1<>>0;return t}murmurSimple(e){let t=0;for(let s=0;sthis.data.byteLength);t++){const t=this.readLen(e);if((e+=n)+t+8>this.data.byteLength)break;const s=(new TextDecoder).decode(this.data.slice(e,e+t));e+=t;const i=this.view.getUint32(e,!1);e+=4;const r=this.view.getUint32(e,!1);e+=4,0===r||i+r>this.data.byteLength||this.indexEntries.push({key:s,blockOffset:i,blockSize:r})}}getBlockData(e){return e.blockSize<=0||e.blockOffset<0||e.blockOffset+e.blockSize>this.data.byteLength?null:new Uint8Array(this.data.buffer,this.data.byteOffset+e.blockOffset,e.blockSize)}locateBlock(e){let t=0,s=this.indexEntries.length-1;for(;t<=s;){const n=Math.floor((t+s)/2);if(e<=this.indexEntries[n].key){if(e>(0===n?"":this.indexEntries[n-1].key))return n;s=n-1}else t=n+1}return-1}locateBlockGE(e){let t=0,s=this.indexEntries.length;for(;t>1;this.indexEntries[n].keye+t.byteLength,0),r=new Uint8Array(Math.min(i,s));let a=0;for(const e of n){const t=Math.min(e.byteLength,r.byteLength-a);if(t<=0)break;r.set(e.subarray(0,t),a),a+=t}return this.pageIds.delete(e),r}async delete(e,t){for(const e of t){this.bufferPool.removePage(e);try{await this.fileManager.freePageId(e)}catch{}}this.pageIds.delete(e)}}class ge{constructor(e){this.nextPageId=0,this.metaLoaded=!1,this.dbName="",this.backend=e}async init(e){this.dbName=e;const t=await this.backend.read("__aria_meta");if(t&&t instanceof ArrayBuffer&&t.byteLength>=4){const e=new DataView(t);this.nextPageId=e.getUint32(0,!1)}else this.nextPageId=1,await this.saveMeta();this.metaLoaded=!0}async readPage(e){const t=`pg_${e}`,s=await this.backend.read(t);if(!s)return null;if(s.byteLengtht.txnId!==e);0===n.length?this.versionStore.delete(t):this.versionStore.set(t,n)}this.activeTxns.delete(e),this.txnWriteKeys.delete(e)}writeVersion(e,t,s,n){const i=`${e}.${t}`,r=this.versionStore.get(i)??[],a={txnId:n,data:s,prevVersion:r.length>0?r[r.length-1]:null,committed:!1};r.push(a),this.versionStore.set(i,r),this.txnWriteKeys.get(n)?.add(i)}deleteVersion(e,t,s){this.writeVersion(e,t,{__mvcc_tombstone:!0},s)}discardVersions(e){const t=this.txnWriteKeys.get(e);if(t)for(const s of t){const t=this.versionStore.get(s);if(!t)continue;const n=t.filter(t=>t.txnId!==e);0===n.length?this.versionStore.delete(s):this.versionStore.set(s,n)}}gc(e=100){for(const[t,s]of this.versionStore){if(s.length<=e)continue;const n=s.slice(s.length-e);this.versionStore.set(t,n)}}getGlobalLSN(){return this.globalCommitLsn}}function be(e,t){const s=new ArrayBuffer(U);return function(e,t,s){const n=new DataView(e);n.setUint32(0,t,!1),n.setUint8(4,s),n.setUint16(5,16,!1),n.setUint16(7,e.byteLength,!1),n.setUint16(9,0,!1),n.setUint32(11,0,!1),n.setUint8(15,0)}(s,e,t),{pageId:e,type:t,data:s,dirty:!0,pins:0,prev:null,next:null,lastAccess:Date.now()}}class Te{constructor(){this.head=null,this.tail=null,this._size=0}get size(){return this._size}moveToHead(e){this.head!==e&&(null!==e.prev||null!==e.next||this.head===e||this.tail===e?this.detach(e):this._size++,e.prev=null,e.next=this.head,this.head&&(this.head.prev=e),this.head=e,this.tail||(this.tail=e))}remove(e){(null!==e.prev||null!==e.next||this.head===e||this.tail===e)&&(this.detach(e),this._size=Math.max(0,this._size-1))}detach(e){e.prev?e.prev.next=e.next:this.head===e&&(this.head=e.next),e.next?e.next.prev=e.prev:this.tail===e&&(this.tail=e.prev),e.prev=null,e.next=null}clear(){this.head=null,this.tail=null,this._size=0}getLRU(){return this.tail}}class Ee{constructor(e,t){this.lru=new Te,this.capacity=e,this.onEvict=t}access(e){e.lastAccess=Date.now(),this.lru.moveToHead(e)}add(e){this.access(e)}remove(e){this.lru.remove(e)}async evictIfNeeded(e){let t=0;for(;this.lru.size+e>this.capacity&&this.lru.size>0;){const e=this.findEvictionCandidate();if(!e)break;e.dirty&&(await this.onEvict(e),e.dirty=!1),this.lru.remove(e),t++}return t}findEvictionCandidate(){let e=this.lru.getLRU();for(;e;){if(0===e.pins&&!e.dirty)return e;e=e.prev}for(e=this.lru.getLRU();e;){if(0===e.pins)return e;e=e.prev}return null}clear(){this.lru.clear()}}class ke{constructor(e,t=256){this.pages=new Map,this.nextPageId=0,this.pageIO=e,this.eviction=new Ee(t,async e=>{e.dirty&&(await this.pageIO.writePage(e.pageId,e.data),e.dirty=!1)})}async getPage(e){let t=this.pages.get(e);if(t)return this.eviction.access(t),t.pins++,t;const s=await this.pageIO.readPage(e);return s?(await this.eviction.evictIfNeeded(1),t={pageId:e,type:new DataView(s).getUint8(4),data:s,dirty:!1,pins:1,prev:null,next:null,lastAccess:Date.now()},this.pages.set(e,t),this.eviction.add(t),t):null}async newPages(e,t=_.DATA){if(e<=0)return[];let s;if("function"==typeof this.pageIO.allocatePageIds)s=await this.pageIO.allocatePageIds(e);else{s=[];for(let t=0;tt.name===e);-1!==t&&(this.plugins[t].destroy(),this.plugins.splice(t,1))}getPlugins(){return[...this.plugins]}on(e,t){const s=this.hooks.get(e)??[];s.push(t),this.hooks.set(e,s)}off(e,t){const s=this.hooks.get(e);if(s){const e=s.indexOf(t);-1!==e&&s.splice(e,1)}}async trigger(e,...t){const s=this.hooks.get(e);if(s)for(const e of s)await e(...t)}destroy(){for(const e of this.plugins)try{e.destroy()}catch(e){}this.plugins=[],this.hooks.clear()}}class je{get version(){return this._version}get maxRowsPerQuery(){return this.config.maxRowsPerQuery??0}get debug(){return this.config.debug??!1}constructor(e){this.ready=!1,this.tableCache=new Map,this.channel=null,this.listeners=new Map,this.migrations=new Map,this.config=e,this.name=e.name??s.name,this.mode=e.mode??s.mode,this._version=e.version??s.version,this.pluginManager=new Fe,e.multiTabSync&&"undefined"!=typeof BroadcastChannel&&(this.channel=new BroadcastChannel(`metona-sqlark:${this.name}`),this.channel.onmessage=e=>{const t=e.data;t&&"change"===t.type&&(this.emit(t.table??"",{type:"external",table:t.table??""}),this.engine instanceof Se&&this.engine.reloadMemoryFromDisk().catch(()=>{}))})}async init(){if(this.engine=this.createEngine(),await this.engine.open(this.name,this.version),"function"==typeof this.engine.getMeta)try{const e=await this.engine.getMeta("__metona_version");null!=e&&Number(e)>=1&&(this._version=Math.max(this._version,Math.floor(Number(e))))}catch{}if(this.executor=new Be(this.engine,this.maxRowsPerQuery),this.transactionManager=new ze(this.engine),this.config.plugins)for(const e of this.config.plugins)this.pluginManager.register(e,this);this.ready=!0,this.config.onReady&&this.config.onReady(this)}isReady(){return this.ready}async defineTable(e,t){this.ensureReady();const s=$(e,t);try{await this.pluginManager.trigger("beforeCreateTable",s),await this.engine.createTable(s),await this.pluginManager.trigger("afterCreateTable",s)}catch(e){throw this._onError(e),e}this.tableCache.delete(e)}table(e){this.ensureReady();let t=this.tableCache.get(e);return t||(t=new Ce(this.engine,e,this.executor,e=>this.broadcastChange(e),(e,t)=>this.pluginManager.trigger(e,...t)),this.tableCache.set(e,t)),t}async dropTable(e){this.ensureReady();try{await this.pluginManager.trigger("beforeDropTable",e),await this.engine.dropTable(e),await this.pluginManager.trigger("afterDropTable",e)}catch(e){throw this._onError(e),e}this.tableCache.delete(e)}async getTableNames(){return this.ensureReady(),this.engine.getTableNames()}async query(e){this.ensureReady();const t=this.debug?Date.now():0;let s;await this.pluginManager.trigger("beforeQuery",e);try{const t=De(e);for(const e of t){await this.triggerStatementHooks(e,"before"),s=await this.executor.execute(e),await this.triggerStatementHooks(e,"after",s);const t=this.writeStatementTable(e);t&&this.broadcastChange(t)}}catch(e){throw this._onError(e),e}if(await this.pluginManager.trigger("afterQuery",e,s),this.debug){const n=Date.now()-t,i=Array.isArray(s)?s.length:0;this._debug(`query [${n}ms] ${i} rows: ${e.slice(0,100)}`)}return s}async queryStream(e,t){this.ensureReady();const s=De(e)[0];if(!s||"SELECT"!==s.type)throw new n("queryStream only supports SELECT statements","NOT_SUPPORTED");const i=s,r=i.columns.some(e=>/^(COUNT|SUM|AVG|MIN|MAX)\(/i.test(e));if(!(i.joins||i.groupBy||i.having||i.distinct||r||i.orderBy&&i.orderBy.length>0||i.where&&void 0!==i.where.$exists)&&"function"==typeof this.engine.findStream&&"AsyncFunction"!==t.constructor?.name){const e=this.normalizeWhereForStream(i),s=i.columns.filter(e=>!/\s+AS\s+\w+$/i.test(e));return this.engine.findStream(i.from,{table:i.from,columns:s.length>0&&"*"!==s[0]?s:["*"],where:e&&Object.keys(e).length>0?e:void 0,limit:i.limit,offset:i.offset},t)}const a=await this.query(e);if(Array.isArray(a)){for(const e of a)await t(e);return a.length}return 0}normalizeWhereForStream(e){const t=[e.alias??e.from].filter(Boolean),s=e=>{for(const s of t)if(e.startsWith(`${s}.`))return e.slice(s.length+1);return e},n=e=>{const t={};for(const[i,r]of Object.entries(e))"$and"===i||"$or"===i?t[i]=r.map(n):"$not"===i&&"object"==typeof r&&null!==r?t.$not=n(r):t[s(i)]=r;return t};return n(e.where??{})}async transaction(e){this.ensureReady(),await this.pluginManager.trigger("beforeTransaction");try{const t=await this.transactionManager.execute(e);return await this.pluginManager.trigger("afterTransaction"),t}catch(e){throw this._onError(e),e}}async exportTable(e){return this.ensureReady(),this.engine.find(e,{table:e})}async importTable(e,t){this.ensureReady();try{return await this.engine.insert(e,t)}catch(e){throw this._onError(e),e}}async exportAll(){this.ensureReady();const e={},t=await this.engine.getTableNames();for(const s of t)e[s]=await this.engine.find(s,{table:s});return e}async backup(){return this.ensureReady(),"function"==typeof this.engine.backup?this.engine.backup():this.exportAll()}subscribe(e,t){const s=`change:${e}`;return this.listeners.has(s)||this.listeners.set(s,new Set),this.listeners.get(s).add(t),()=>this.listeners.get(s)?.delete(t)}emit(e,t){const s=`change:${e}`;this.listeners.get(s)?.forEach(e=>e(t))}broadcastChange(e){if(this.channel)try{this.channel.postMessage({type:"change",table:e})}catch{}}writeStatementTable(e){switch(e.type){case"INSERT":return e.into;case"UPDATE":case"CREATE_INDEX":case"DROP_INDEX":return e.table;case"DELETE":return e.from;case"CREATE_TABLE":case"DROP_TABLE":case"TRUNCATE_TABLE":case"ALTER_TABLE":return e.name;default:return null}}async triggerStatementHooks(e,t,s){switch(e.type){case"INSERT":{const n=(e.values??[]).map(t=>{const s={},n=e.columns??[];for(let e=0;e
v0.5.1 深度审查修复 — 14 个生命周期钩子全部真实接线(此前 6 个 CRUD 钩子从未触发)· EXPLAIN / ANALYZE / REINDEX / VACUUM / SAVEPOINT SQL 入口补齐(此前仅有引擎方法无法触发)· db.backup() 公共方法 · 删除全部死代码(utils.ts 整文件 / MVCC 读侧 / estimateQueryCost 未接线优化器 / 40+ 统计辅助方法)。
v0.6.0 完全移除 IndexedDB — 自研 KVStore 事务存储引擎(多 key 原子写 = 单日志记录原子追加 · 快照 checkpoint + 两阶段崩溃恢复 · CRC-32 自愈)· disk 模式切换 KVStoreEngine(替代 IndexedDBEngine + OPFSEngine)· 事务内 DDL / 外键级联 / 二级索引完整持久化 · migrateFromIndexedDB() 旧库一键迁移 · 10 万 key 压力验证 · e2e 崩溃注入 + KVStoreEngine 真实环境(12 用例)。
-v0.6.1 生产可用性深度审查 — MemoryEngine 级联环(A→B→A)无限递归修复(visited 保护,与 Aria 对齐)· KVStore 快照损坏水位 bug 修复(metaSeq 误跳日志)· 多表事务 / 级联写入合并单条日志记录真原子(崩溃无部分提交)· 未 open 防护统一 · 27 个异常场景测试(空值/大 value/特殊字符 key/写失败/事务故障/级联环/批量删除等)· 1049 测试 65 套件 · 89.3% 行覆盖率。
checkpointInterval | number | 1000 | Checkpoint 触发间隔(操作数) |
walSizeThreshold | number | 16777216 | WAL 大小阈值(字节),超阈值触发 checkpoint ✅ v0.2.5 |
compression | boolean | false | 是否启用页面压缩 |
storageBackend | 'opfs'|'memory' | 'opfs' | 存储后端类型(v0.6.0: IndexedDB 已移除) |
storageBackend | 'opfs'|'memory'|'kv' | 'opfs' | 存储后端类型('kv' = 自研 KVStore 后端 🆕 v0.6.1) |
encryption | { password: string } | - | 全库 AES-256-GCM 透明加密(PBKDF2 派生 + salt 持久化 + 密码验证)🆕 v0.5.0 |
pageStorage | boolean | OPFS 自动启用 | SSTable 4KB 页面化物理存储(BufferPool 缓存)🆕 v0.5.0 |
TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。
零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。
MetonaSqlark 的核心指标