22
33var fs = require ( 'fs' ) ;
44var path = require ( 'path' ) ;
5+ var originalEmitWarning = process . emitWarning ;
6+ process . emitWarning = function ( warning ) {
7+ var text = String ( warning && warning . message || warning || '' ) ;
8+ if ( text . indexOf ( 'SQLite is an experimental feature' ) !== - 1 ) return ;
9+ return originalEmitWarning . apply ( process , arguments ) ;
10+ } ;
11+ var sqlite = require ( 'node:sqlite' ) ;
12+ process . emitWarning = originalEmitWarning ;
13+ var DatabaseSync = sqlite . DatabaseSync ;
514
615function ensureDir ( dir ) {
716 fs . mkdirSync ( dir , { recursive : true } ) ;
@@ -26,67 +35,94 @@ function writeJsonAtomic(file, data) {
2635 fs . renameSync ( tmp , file ) ;
2736}
2837
29- function readJsonLines ( file ) {
30- try {
31- var text = fs . readFileSync ( file , 'utf8' ) ;
32- if ( ! text . trim ( ) ) return [ ] ;
33- return text . split ( / \n + / ) . filter ( Boolean ) . map ( function ( line ) { return JSON . parse ( line ) ; } ) ;
34- } catch ( err ) {
35- return [ ] ;
36- }
37- }
38-
39- function appendJsonLine ( file , item ) {
40- ensureDir ( path . dirname ( file ) ) ;
41- fs . appendFileSync ( file , JSON . stringify ( item ) + '\n' , 'utf8' ) ;
42- }
43-
4438function ArchiveStore ( rootDir ) {
4539 this . rootDir = rootDir || path . join ( process . cwd ( ) , 'data' , 'archive-node' ) ;
46- this . blocksDir = path . join ( this . rootDir , 'blocks' ) ;
47- this . eventsDir = path . join ( this . rootDir , 'events' ) ;
48- ensureDir ( this . blocksDir ) ;
49- ensureDir ( this . eventsDir ) ;
40+ ensureDir ( this . rootDir ) ;
41+ this . dbPath = path . join ( this . rootDir , 'archive.sqlite' ) ;
42+ this . db = new DatabaseSync ( this . dbPath ) ;
43+ this . db . exec ( 'PRAGMA journal_mode = WAL' ) ;
44+ this . db . exec ( 'PRAGMA synchronous = NORMAL' ) ;
45+ this . db . exec ( 'PRAGMA temp_store = MEMORY' ) ;
46+ this . _initSchema ( ) ;
5047}
5148
52- ArchiveStore . prototype . cursorPath = function ( ) {
53- return path . join ( this . rootDir , 'cursor.json' ) ;
54- } ;
55-
56- ArchiveStore . prototype . statusPath = function ( ) {
57- return path . join ( this . rootDir , 'status.json' ) ;
58- } ;
59-
60- ArchiveStore . prototype . blockPath = function ( blockNum ) {
61- return path . join ( this . blocksDir , String ( blockNum ) + '.json' ) ;
62- } ;
63-
64- ArchiveStore . prototype . allEventsPath = function ( ) {
65- return path . join ( this . eventsDir , 'all.jsonl' ) ;
49+ ArchiveStore . prototype . _initSchema = function ( ) {
50+ this . db . exec ( [
51+ 'CREATE TABLE IF NOT EXISTS blocks (' ,
52+ ' block_num INTEGER PRIMARY KEY,' ,
53+ ' block_id TEXT,' ,
54+ ' previous TEXT,' ,
55+ ' timestamp TEXT,' ,
56+ ' source_node TEXT,' ,
57+ ' indexed_at TEXT,' ,
58+ ' event_count INTEGER NOT NULL DEFAULT 0,' ,
59+ ' raw_json TEXT NOT NULL' ,
60+ ')' ,
61+ ';' ,
62+ 'CREATE TABLE IF NOT EXISTS events (' ,
63+ ' id TEXT PRIMARY KEY,' ,
64+ ' block_num INTEGER NOT NULL,' ,
65+ ' block_id TEXT,' ,
66+ ' previous TEXT,' ,
67+ ' timestamp TEXT,' ,
68+ ' tx_index INTEGER NOT NULL,' ,
69+ ' op_index INTEGER NOT NULL,' ,
70+ ' op_type TEXT NOT NULL,' ,
71+ ' protocol TEXT NOT NULL,' ,
72+ ' type TEXT,' ,
73+ ' sender TEXT,' ,
74+ ' account TEXT,' ,
75+ ' accounts_json TEXT,' ,
76+ ' payload_json TEXT,' ,
77+ ' raw_json TEXT NOT NULL' ,
78+ ')' ,
79+ ';' ,
80+ 'CREATE TABLE IF NOT EXISTS event_accounts (' ,
81+ ' event_id TEXT NOT NULL,' ,
82+ ' account TEXT NOT NULL,' ,
83+ ' protocol TEXT NOT NULL,' ,
84+ ' block_num INTEGER NOT NULL,' ,
85+ ' PRIMARY KEY(event_id, account)' ,
86+ ')' ,
87+ ';' ,
88+ 'CREATE TABLE IF NOT EXISTS meta (' ,
89+ ' key TEXT PRIMARY KEY,' ,
90+ ' value TEXT NOT NULL' ,
91+ ')' ,
92+ ';' ,
93+ 'CREATE INDEX IF NOT EXISTS idx_events_block ON events(block_num)' ,
94+ ';' ,
95+ 'CREATE INDEX IF NOT EXISTS idx_events_protocol_block ON events(protocol, block_num)' ,
96+ ';' ,
97+ 'CREATE INDEX IF NOT EXISTS idx_event_accounts_account_protocol ON event_accounts(account, protocol, block_num)' ,
98+ ';'
99+ ] . join ( '\n' ) ) ;
66100} ;
67101
68- ArchiveStore . prototype . protocolEventsPath = function ( protocol ) {
69- return path . join ( this . eventsDir , 'protocol-' + sanitizePart ( protocol ) + '.jsonl' ) ;
102+ ArchiveStore . prototype . _getMeta = function ( key , fallback ) {
103+ var row = this . db . prepare ( 'SELECT value FROM meta WHERE key = ?' ) . get ( key ) ;
104+ if ( ! row ) return fallback ;
105+ try { return JSON . parse ( row . value ) ; } catch ( err ) { return fallback ; }
70106} ;
71107
72- ArchiveStore . prototype . accountProtocolEventsPath = function ( account , protocol ) {
73- return path . join ( this . eventsDir , 'account-' + sanitizePart ( account ) + '-protocol-' + sanitizePart ( protocol ) + '.jsonl' ) ;
108+ ArchiveStore . prototype . _setMeta = function ( key , value ) {
109+ this . db . prepare ( 'INSERT OR REPLACE INTO meta(key, value) VALUES(?, ?)' ) . run ( key , JSON . stringify ( value ) ) ;
74110} ;
75111
76112ArchiveStore . prototype . getCursor = function ( ) {
77- return readJson ( this . cursorPath ( ) , { lastIndexedBlock : 0 , updatedAt : null } ) ;
113+ return this . _getMeta ( 'cursor' , { lastIndexedBlock : 0 , updatedAt : null } ) ;
78114} ;
79115
80116ArchiveStore . prototype . setCursor = function ( lastIndexedBlock ) {
81- writeJsonAtomic ( this . cursorPath ( ) , {
117+ this . _setMeta ( 'cursor' , {
82118 lastIndexedBlock : Number ( lastIndexedBlock ) || 0 ,
83119 updatedAt : new Date ( ) . toISOString ( )
84120 } ) ;
85121} ;
86122
87123ArchiveStore . prototype . getStatus = function ( ) {
88124 var cursor = this . getCursor ( ) ;
89- var status = readJson ( this . statusPath ( ) , { } ) ;
125+ var status = this . _getMeta ( 'status' , { } ) ;
90126 status . lastIndexedBlock = cursor . lastIndexedBlock || 0 ;
91127 status . cursorUpdatedAt = cursor . updatedAt || null ;
92128 return status ;
@@ -95,70 +131,140 @@ ArchiveStore.prototype.getStatus = function() {
95131ArchiveStore . prototype . setStatus = function ( status ) {
96132 status = status || { } ;
97133 status . updatedAt = new Date ( ) . toISOString ( ) ;
98- writeJsonAtomic ( this . statusPath ( ) , status ) ;
134+ this . _setMeta ( 'status' , status ) ;
99135} ;
100136
101137ArchiveStore . prototype . hasBlock = function ( blockNum ) {
102- return fs . existsSync ( this . blockPath ( blockNum ) ) ;
138+ return ! ! this . db . prepare ( 'SELECT 1 AS ok FROM blocks WHERE block_num = ?' ) . get ( Number ( blockNum ) ) ;
103139} ;
104140
105141ArchiveStore . prototype . getBlockRecord = function ( blockNum ) {
106- return readJson ( this . blockPath ( blockNum ) , null ) ;
142+ var row = this . db . prepare ( 'SELECT * FROM blocks WHERE block_num = ?' ) . get ( Number ( blockNum ) ) ;
143+ if ( ! row ) return null ;
144+ return {
145+ blockNum : row . block_num ,
146+ block_id : row . block_id || '' ,
147+ previous : row . previous || '' ,
148+ timestamp : row . timestamp || '' ,
149+ sourceNode : row . source_node || '' ,
150+ indexedAt : row . indexed_at || '' ,
151+ eventCount : row . event_count || 0 ,
152+ block : JSON . parse ( row . raw_json )
153+ } ;
107154} ;
108155
109156ArchiveStore . prototype . putBlock = function ( blockNum , block , sourceNode , events ) {
110- var record = {
111- blockNum : Number ( blockNum ) ,
112- block_id : block && ( block . block_id || block . id || '' ) ,
113- previous : block && ( block . previous || block . previous_block_id || '' ) ,
114- timestamp : block && block . timestamp || '' ,
115- sourceNode : sourceNode || '' ,
116- indexedAt : new Date ( ) . toISOString ( ) ,
117- eventCount : events ? events . length : 0 ,
118- block : block
119- } ;
120- writeJsonAtomic ( this . blockPath ( blockNum ) , record ) ;
157+ var now = new Date ( ) . toISOString ( ) ;
158+ var bn = Number ( blockNum ) ;
159+ var blockId = block && ( block . block_id || block . id || '' ) || '' ;
160+ var previous = block && ( block . previous || block . previous_block_id || '' ) || '' ;
161+ var timestamp = block && block . timestamp || '' ;
162+ this . db . exec ( 'BEGIN IMMEDIATE' ) ;
163+ try {
164+ this . db . prepare ( 'DELETE FROM event_accounts WHERE block_num = ?' ) . run ( bn ) ;
165+ this . db . prepare ( 'DELETE FROM events WHERE block_num = ?' ) . run ( bn ) ;
166+ this . db . prepare ( [
167+ 'INSERT OR REPLACE INTO blocks(block_num, block_id, previous, timestamp, source_node, indexed_at, event_count, raw_json)' ,
168+ 'VALUES(?, ?, ?, ?, ?, ?, ?, ?)'
169+ ] . join ( ' ' ) ) . run ( bn , blockId , previous , timestamp , sourceNode || '' , now , events ? events . length : 0 , JSON . stringify ( block || { } ) ) ;
170+ this . db . exec ( 'COMMIT' ) ;
171+ } catch ( err ) {
172+ this . db . exec ( 'ROLLBACK' ) ;
173+ throw err ;
174+ }
121175} ;
122176
123177ArchiveStore . prototype . eventKey = function ( event ) {
124178 return [ event . blockNum , event . txIndex , event . opIndex , event . protocol , event . type , event . sender || event . account || '' ] . join ( ':' ) ;
125179} ;
126180
127181ArchiveStore . prototype . putEventsForBlock = function ( events ) {
182+ var insertEvent = this . db . prepare ( [
183+ 'INSERT OR REPLACE INTO events(id, block_num, block_id, previous, timestamp, tx_index, op_index, op_type, protocol, type, sender, account, accounts_json, payload_json, raw_json)' ,
184+ 'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
185+ ] . join ( ' ' ) ) ;
186+ var insertAccount = this . db . prepare ( 'INSERT OR REPLACE INTO event_accounts(event_id, account, protocol, block_num) VALUES(?, ?, ?, ?)' ) ;
128187 var self = this ;
129- events . forEach ( function ( event ) {
130- event . id = event . id || self . eventKey ( event ) ;
131- appendJsonLine ( self . allEventsPath ( ) , event ) ;
132- if ( event . protocol ) appendJsonLine ( self . protocolEventsPath ( event . protocol ) , event ) ;
133- var accounts = event . accounts || [ ] ;
134- for ( var i = 0 ; i < accounts . length ; i += 1 ) {
135- appendJsonLine ( self . accountProtocolEventsPath ( accounts [ i ] , event . protocol || '_' ) , event ) ;
136- }
137- } ) ;
188+ this . db . exec ( 'BEGIN IMMEDIATE' ) ;
189+ try {
190+ events . forEach ( function ( event ) {
191+ event . id = event . id || self . eventKey ( event ) ;
192+ var accounts = event . accounts || [ ] ;
193+ insertEvent . run (
194+ event . id ,
195+ Number ( event . blockNum ) || 0 ,
196+ event . block_id || '' ,
197+ event . previous || '' ,
198+ event . timestamp || '' ,
199+ Number ( event . txIndex ) || 0 ,
200+ Number ( event . opIndex ) || 0 ,
201+ event . opType || '' ,
202+ event . protocol || '' ,
203+ event . type || '' ,
204+ event . sender || '' ,
205+ event . account || '' ,
206+ JSON . stringify ( accounts ) ,
207+ JSON . stringify ( event . payload || null ) ,
208+ JSON . stringify ( event . raw || null )
209+ ) ;
210+ for ( var i = 0 ; i < accounts . length ; i += 1 ) {
211+ insertAccount . run ( event . id , accounts [ i ] , event . protocol || '_' , Number ( event . blockNum ) || 0 ) ;
212+ }
213+ } ) ;
214+ this . db . exec ( 'COMMIT' ) ;
215+ } catch ( err ) {
216+ this . db . exec ( 'ROLLBACK' ) ;
217+ throw err ;
218+ }
219+ } ;
220+
221+ ArchiveStore . prototype . _eventFromRow = function ( row ) {
222+ return {
223+ blockNum : row . block_num ,
224+ block_id : row . block_id || '' ,
225+ previous : row . previous || '' ,
226+ timestamp : row . timestamp || '' ,
227+ txIndex : row . tx_index ,
228+ opIndex : row . op_index ,
229+ opType : row . op_type || '' ,
230+ protocol : row . protocol || '' ,
231+ type : row . type || '' ,
232+ sender : row . sender || '' ,
233+ account : row . account || '' ,
234+ accounts : JSON . parse ( row . accounts_json || '[]' ) ,
235+ payload : JSON . parse ( row . payload_json || 'null' ) ,
236+ raw : JSON . parse ( row . raw_json || 'null' ) ,
237+ id : row . id
238+ } ;
138239} ;
139240
140241ArchiveStore . prototype . queryEvents = function ( options ) {
141242 options = options || { } ;
142- var file ;
143- if ( options . account && options . protocol ) file = this . accountProtocolEventsPath ( options . account , options . protocol ) ;
144- else if ( options . protocol ) file = this . protocolEventsPath ( options . protocol ) ;
145- else file = this . allEventsPath ( ) ;
146243 var start = Number ( options . start || options . from || 0 ) ;
147244 var end = Number ( options . end || options . to || 2147483647 ) ;
148245 var limit = Math . max ( 1 , Math . min ( Number ( options . limit || 500 ) , 5000 ) ) ;
149- var protocols = null ;
150- if ( options . protocols && options . protocols . length ) protocols = options . protocols ;
151- var rows = readJsonLines ( file ) . filter ( function ( ev ) {
152- if ( ev . blockNum < start || ev . blockNum > end ) return false ;
153- if ( protocols && protocols . indexOf ( ev . protocol ) === - 1 ) return false ;
154- return true ;
155- } ) ;
156- rows . sort ( function ( a , b ) {
157- if ( a . blockNum !== b . blockNum ) return a . blockNum - b . blockNum ;
158- if ( a . txIndex !== b . txIndex ) return a . txIndex - b . txIndex ;
159- return a . opIndex - b . opIndex ;
160- } ) ;
161- if ( rows . length > limit ) rows = rows . slice ( rows . length - limit ) ;
246+ var params = [ ] ;
247+ var where = [ 'e.block_num >= ?' , 'e.block_num <= ?' ] ;
248+ params . push ( start , end ) ;
249+ var sql = 'SELECT e.* FROM events e' ;
250+ if ( options . account && options . protocol ) {
251+ sql += ' JOIN event_accounts ea ON ea.event_id = e.id' ;
252+ where . push ( 'ea.account = ?' ) ;
253+ where . push ( 'ea.protocol = ?' ) ;
254+ params . push ( String ( options . account ) , String ( options . protocol ) ) ;
255+ } else if ( options . protocol ) {
256+ where . push ( 'e.protocol = ?' ) ;
257+ params . push ( String ( options . protocol ) ) ;
258+ } else if ( options . protocols && options . protocols . length ) {
259+ where . push ( 'e.protocol IN (' + options . protocols . map ( function ( ) { return '?' ; } ) . join ( ',' ) + ')' ) ;
260+ options . protocols . forEach ( function ( protocol ) { params . push ( String ( protocol ) ) ; } ) ;
261+ }
262+ sql += ' WHERE ' + where . join ( ' AND ' ) ;
263+ sql += ' ORDER BY e.block_num DESC, e.tx_index DESC, e.op_index DESC LIMIT ?' ;
264+ params . push ( limit ) ;
265+ var stmt = this . db . prepare ( sql ) ;
266+ var rows = stmt . all . apply ( stmt , params ) ;
267+ rows = rows . map ( this . _eventFromRow ) . reverse ( ) ;
162268 return rows ;
163269} ;
164270
@@ -167,6 +273,5 @@ module.exports = {
167273 ensureDir : ensureDir ,
168274 readJson : readJson ,
169275 writeJsonAtomic : writeJsonAtomic ,
170- readJsonLines : readJsonLines ,
171276 sanitizePart : sanitizePart
172277} ;
0 commit comments