pixl-server-storage 3.2.8 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # Overview
2
2
 
3
- This module is a component for use in [pixl-server](https://www.github.com/jhuckaby/pixl-server). It implements a simple key/value storage system that can use multiple back-ends, such as [Amazon S3](https://aws.amazon.com/s3/), [Couchbase](http://www.couchbase.com/nosql-databases/couchbase-server), [Redis](https://redis.io/), or a local filesystem. It introduces the concept of a "chunked linked list", which supports extremely fast push, pop, shift, unshift, and random reads/writes. Also provided is a fast hash table implementation with key iteration, a transaction system, and an indexing and search system.
3
+ This module is a component for use in [pixl-server](https://www.github.com/jhuckaby/pixl-server). It implements a simple key/value storage system that can use multiple back-ends, such as [Amazon S3](https://aws.amazon.com/s3/), [Redis](https://redis.io/), or a local filesystem. It introduces the concept of a "chunked linked list", which supports extremely fast push, pop, shift, unshift, and random reads/writes. Also provided is a fast hash table implementation with key iteration, a transaction system, and an indexing and search system.
4
4
 
5
5
  ## Features at a Glance
6
6
 
7
7
  * Uses very little memory in most cases.
8
8
  * Store JSON or binary (raw) data records.
9
- * Supports multiple back-ends including Amazon S3, Couchbase, Redis, and local filesystem.
9
+ * Supports multiple back-ends including Amazon S3, Redis, and local filesystem.
10
10
  * Linked lists with very fast push, pop, shift, unshift, and random reads/writes.
11
11
  * Hash tables with key iterators, and very fast reads / writes.
12
12
  * Advisory locking system with shared and exclusive locks.
@@ -54,7 +54,7 @@ Here is the table of contents for this current document:
54
54
  + [S3 Key Prefix](#s3-key-prefix)
55
55
  + [S3 Key Template](#s3-key-template)
56
56
  + [S3 Cache](#s3-cache)
57
- * [Couchbase](#couchbase)
57
+ + [S3 Compatible Services](#s3-compatible-services)
58
58
  * [Redis](#redis)
59
59
  + [RedisCluster](#rediscluster)
60
60
  * [SQLite](#sqlite)
@@ -266,7 +266,7 @@ The `debug` property is only used when using [Standalone Mode](#standalone-mode)
266
266
 
267
267
  # Engines
268
268
 
269
- The storage system can be backed by a number of different "engines", which actually perform the reads and writes. A simple local filesystem implementation is included, as well as modules for Amazon S3, Couchbase and Redis. Each one requires a bit of extra configuration.
269
+ The storage system can be backed by a number of different "engines", which actually perform the reads and writes. A simple local filesystem implementation is included, as well as modules for Amazon S3 and Redis. Each one requires a bit of extra configuration.
270
270
 
271
271
  ## Local Filesystem
272
272
 
@@ -518,44 +518,60 @@ It is recommended that you set the `maxItems` and `maxBytes` high enough to allo
518
518
 
519
519
  Note that binary records are **not** cached, as they are generally large. Only JSON records are cached, as they are usually much smaller and used in [Lists](https://github.com/jhuckaby/pixl-server-storage/blob/master/docs/Lists.md), [Hashes](https://github.com/jhuckaby/pixl-server-storage/blob/master/docs/Hashes.md), [Transactions](https://github.com/jhuckaby/pixl-server-storage/blob/master/docs/Transactions.md) and the [Indexer](https://github.com/jhuckaby/pixl-server-storage/blob/master/docs/Indexer.md).
520
520
 
521
- ## Couchbase
521
+ ### S3 Compatible Services
522
522
 
523
- Please note that as of this writing (April 2022), pixl-server-storage only supports Couchbase Client v2, so you need to force install version `2.6.12` (see instructions below). Work is underway to support the v3 API, which has many breaking changes.
523
+ To use an S3 compatible service such as [MinIO](https://github.com/minio/minio), you'll need to add a few extra parameters to the `AWS` section:
524
524
 
525
- If you want to use [Couchbase](http://www.couchbase.com/nosql-databases/couchbase-server) as a backing store, here is how to do so. First, you need to manually install the [couchbase](https://www.npmjs.com/package/couchbase) module into your app, and it **must be v2**:
526
-
527
- ```sh
528
- npm install --save couchbase@2.6.12
525
+ ```js
526
+ {
527
+ "endpoint": "http://minio:9000",
528
+ "endpointPrefix": false,
529
+ "forcePathStyle": true,
530
+ "hostPrefixEnabled": false
531
+ }
529
532
  ```
530
533
 
531
- Then configure your storage thusly:
534
+ Replace `minio:9000` with your MinIO hostname or IP address, and port number (9000 is the default).
532
535
 
533
- ```json
536
+ Here is a complete example with the new parameters added:
537
+
538
+ ```js
534
539
  {
535
- "engine": "Couchbase",
536
- "Couchbase": {
537
- "connectString": "couchbase://127.0.0.1",
538
- "bucket": "default",
539
- "username": "",
540
- "password": "",
541
- "serialize": false,
542
- "keyPrefix": ""
540
+ "Storage": {
541
+ "transactions": true,
542
+ "trans_auto_recover": true,
543
+
544
+ "engine": "S3",
545
+ "AWS": {
546
+ "endpoint": "http://minio:9000",
547
+ "endpointPrefix": false,
548
+ "forcePathStyle": true,
549
+ "hostPrefixEnabled": false,
550
+ "region": "us-west-1",
551
+ "credentials": {
552
+ "accessKeyId": "YOUR_MINIO_ACCESS_KEY",
553
+ "secretAccessKey": "YOUR_MINIO_SECRET_KEY"
554
+ }
555
+ },
556
+ "S3": {
557
+ "connectTimeout": 5000,
558
+ "socketTimeout": 5000,
559
+ "maxAttempts": 50,
560
+ "keyPrefix": "",
561
+ "fileExtensions": true,
562
+ "params": {
563
+ "Bucket": "YOUR_MINIO_BUCKET_ID"
564
+ },
565
+ "cache": {
566
+ "enabled": true,
567
+ "maxItems": 1000,
568
+ "maxBytes": 10485760
569
+ }
570
+ }
543
571
  }
544
572
  }
545
573
  ```
546
574
 
547
- Set the `connectString` for your own Couchbase server setup. You can embed a username and password into the string if they are required to connect (this is different from the bucket password), and use `couchbases://` for SSL, if desired.
548
-
549
- The `bucket` property should be set to the bucket name. If you don't know this then `default` is probably correct. The `password` property is the bucket password, and may or may not be required, depending on your Couchbase server setup.
550
-
551
- The `serialize` property, when set to `true`, will cause all object values to be serialized to JSON before storing, and they will also be parsed from JSON when fetching. When set to `false` (the default), this is left up to Couchbase to handle.
552
-
553
- The optional `keyPrefix` property works similarly to the [S3 Key Prefix](#s3-key-prefix) feature. It allows you to prefix all the Couchbase keys with a common string, to separate your application's data in a shared bucket situation.
554
-
555
- The optional `keyTemplate` property works similarly to the [S3 Key Template](#s3-key-template) feature. It allows you to specify an exact layout of MD5 hash characters, which can be prefixed, mixed in with or postfixed after the key.
556
-
557
- Note that for Couchbase Server v5.0+ (Couchbase Node SDK 2.5+), you will have to supply both a `username` and `password` for a valid user created in the Couchbase UI. Prior to v5+ you could omit the `username` and only specify a `password`, or no password at all if your bucket has no authentication.
558
-
559
575
  ## Redis
560
576
 
561
577
  If you want to use [Redis](https://redis.io/) as a backing store, here is how to do so. First, you need to manually install the [ioredis](https://www.npmjs.com/package/ioredis) module into your app:
@@ -626,10 +642,10 @@ The optional `keyTemplate` property works similarly to the [S3 Key Template](#s3
626
642
 
627
643
  ## SQLite
628
644
 
629
- If you want to use [SQLite](https://sqlite.com/) as a backing store, here is how to do so. First, you need to manually install the [sqlite3](https://www.npmjs.com/package/sqlite3) module into your app:
645
+ If you want to use [SQLite](https://sqlite.com/) as a backing store, here is how to do so. First, you need to manually install the [better-sqlite3](https://www.npmjs.com/package/better-sqlite3) module into your app:
630
646
 
631
647
  ```sh
632
- npm install --save sqlite3
648
+ npm install --save better-sqlite3
633
649
  ```
634
650
 
635
651
  Then configure your storage thusly:
@@ -797,8 +813,6 @@ storage.head( 'test1', function(err, data) {
797
813
  } );
798
814
  ```
799
815
 
800
- Note that the [Couchbase](#couchbase) engine does not support `head`, but the [Amazon S3](#amazon-s3) and [Local Filesystem](#local-filesystem) engines both do.
801
-
802
816
  You can fetch multiple records at once by calling `getMulti()` and passing in array of keys. Example:
803
817
 
804
818
  ```js
@@ -896,7 +910,7 @@ storage.getStream( 'test1.gif', function(err, readStream) {
896
910
  } );
897
911
  ```
898
912
 
899
- Please note that not all the storage engines support streams natively, so the content may actually be loaded into RAM in the background. Namely, as of this writing, the Couchbase and Redis APIs do not support streams, so they are currently simulated in those engines. Streams *are* supported natively in both the Filesystem and Amazon S3 engines.
913
+ Please note that not all the storage engines support streams natively, so the content may actually be loaded into RAM in the background. Namely, as of this writing, the Redis APIs do not support streams, so they are currently simulated in those engines. Streams *are* supported natively in both the Filesystem and Amazon S3 engines.
900
914
 
901
915
  # Expiring Data
902
916
 
@@ -1267,7 +1281,7 @@ If you install the [pixl-unit](https://www.github.com/jhuckaby/pixl-unit) module
1267
1281
  pixl-unit test/test.js --verbose
1268
1282
  ```
1269
1283
 
1270
- This also allows you to specify an alternate test configuration file via the `--configFile` option. Using this you can load your own test config, which may use a different engine (e.g. S3, Couchbase, etc.):
1284
+ This also allows you to specify an alternate test configuration file via the `--configFile` option. Using this you can load your own test config, which may use a different engine (e.g. S3, etc.):
1271
1285
 
1272
1286
  ```sh
1273
1287
  pixl-unit test/test.js --configFile /path/to/my/config.json
@@ -1277,7 +1291,7 @@ pixl-unit test/test.js --configFile /path/to/my/config.json
1277
1291
 
1278
1292
  **The MIT License (MIT)**
1279
1293
 
1280
- Copyright (c) 2015 - 2018 Joseph Huckaby.
1294
+ Copyright (c) 2015 - 2026 Joseph Huckaby.
1281
1295
 
1282
1296
  Permission is hereby granted, free of charge, to any person obtaining a copy
1283
1297
  of this software and associated documentation files (the "Software"), to deal
@@ -54,9 +54,9 @@ Choose your `trans_dir` carefully. This directory is used to store temporary fi
54
54
  |----------------|----------------|
55
55
  | Local Filesystem | Make sure your `trans_dir` is on the *same mount* as your local storage data, to ensure speed and safety. This is the default. |
56
56
  | NFS Filesystem | Make sure your `trans_dir` is **NOT** on the NFS mount, but instead points to a local fast SSD mount. |
57
- | S3 or Couchbase | Make sure your `trans_dir` points to a local fast SSD mount. |
57
+ | S3 | Make sure your `trans_dir` points to a local fast SSD mount. |
58
58
 
59
- So basically, if you plan to use the [Filesystem](../README.md#local-filesystem) engine with a local (i.e. non-network) disk, you don't need to make any adjustments. The default setting for `trans_dir` is a subdirectory just inside your `base_dir` engine property. But if you are going with a networked (i.e. NFS) filesystem, or you're going to use [S3](../README.md#amazon-s3) or [Couchbase](../README.md#couchbase), then you should set `trans_dir` to a local filesystem path on a fast disk (ideally SSD).
59
+ So basically, if you plan to use the [Filesystem](../README.md#local-filesystem) engine with a local (i.e. non-network) disk, you don't need to make any adjustments. The default setting for `trans_dir` is a subdirectory just inside your `base_dir` engine property. But if you are going with a networked (i.e. NFS) filesystem, or you're going to use [S3](../README.md#amazon-s3), then you should set `trans_dir` to a local filesystem path on a fast disk (ideally SSD).
60
60
 
61
61
  **Pro-Tip:** When using transactions with the Amazon S3 engine, consider setting the `maxAttempts` property in your `S3` configuration object, so the AWS client library makes multiple attempts before failing an operation. Network hiccups can happen.
62
62
 
@@ -180,7 +180,7 @@ storage.begin( 'some_path', function(err, trans) {
180
180
 
181
181
  It should be noted that if you receive an error from a [commit()](API.md#commit) call, it is *vital* that you call [abort()](API.md#abort) to undo whatever operations may have already executed. A commit error is typically very bad, and your storage system will be in an unknown state. Only by calling [abort()](API.md#abort) can you restore it to before the transaction started.
182
182
 
183
- If the [abort()](API.md#abort) also fails, then the database raises a fatal error and exits immediately. See [Emergency Shutdown](#emergency-shutdown) below for details about what this means, and [Recovery](#recovery) for how to get back up and running. Examples of fatal errors include your disk running completely out of space, or a major network failure when using NFS, S3 or Couchbase.
183
+ If the [abort()](API.md#abort) also fails, then the database raises a fatal error and exits immediately. See [Emergency Shutdown](#emergency-shutdown) below for details about what this means, and [Recovery](#recovery) for how to get back up and running. Examples of fatal errors include your disk running completely out of space, or a major network failure when using NFS, or S3.
184
184
 
185
185
  ## Automatic API Wrappers
186
186
 
@@ -215,7 +215,7 @@ You can still use these methods inside of your own transaction, and they will "j
215
215
 
216
216
  ## Emergency Shutdown
217
217
 
218
- If a fatal storage error is encountered in the middle of an abort (rollback) operation, the database immediately shuts itself down. This is because your data will be in an undefined state, stuck in the middle of partial transaction. This should be a very rare event, only occurring when the underlying storage completely fails to write records. Examples include a disk running out of space (local or NFS filesystem), or a hard network failure with S3 or Couchbase. When this happens, the Node.js process will exit (by default), and will need to be recovered (see [Recovery](#recovery) below).
218
+ If a fatal storage error is encountered in the middle of an abort (rollback) operation, the database immediately shuts itself down. This is because your data will be in an undefined state, stuck in the middle of partial transaction. This should be a very rare event, only occurring when the underlying storage completely fails to write records. Examples include a disk running out of space (local or NFS filesystem), or a hard network failure with S3. When this happens, the Node.js process will exit (by default), and will need to be recovered (see [Recovery](#recovery) below).
219
219
 
220
220
  Upon fatal error, your application can hook the event and provide its own emergency shutdown procedure. Simply add an event listener on the storage object for the `fatal` event. It will be passed an `Error` object describing the actual error that occurred. It is then up to your code to call `process.exit()`. Example:
221
221
 
@@ -271,7 +271,7 @@ The transaction system is implemented by temporarily "branching" the storage sys
271
271
 
272
272
  ### Temporary Directory
273
273
 
274
- A local temporary directory on disk is always used for transactions, regardless of the storage engine. Even if you are using S3 or Couchbase for primary storage, transactions still use temp files on disk before and during commit. This is to insure both speed and data safety. You can control where the temporary files live by setting the `trans_dir` configuration property.
274
+ A local temporary directory on disk is always used for transactions, regardless of the storage engine. Even if you are using S3 for primary storage, transactions still use temp files on disk before and during commit. This is to insure both speed and data safety. You can control where the temporary files live by setting the `trans_dir` configuration property.
275
275
 
276
276
  Inside the temp directory, two subdirectories are created: `data` and `logs`. The `data` directory is used to hold all modified records during a transaction (before commit). Each is named using a unique transaction ID (see below) and a hash of the original key. This ensures no files will collide with each other, even with many concurrent transactions. The `logs` directory is for rollback logs. When a transaction is committed, the original state of the mutated records is written to the log, so it can be applied in reverse during a recovery event.
277
277
 
@@ -409,7 +409,7 @@ So here is the commit process, step by step:
409
409
 
410
410
  The idea here is that no matter where a crash or sudden power loss occurs during the commit process, a full rollback can always be achieved. The actual changes on the main storage system are only made once the rollback log is fully written and flushed to disk, and the log itself is only deleted when the record changes are also made (and flushed, where possible).
411
411
 
412
- This is not a 100% guarantee of data durability, as corruption can happen during a crash or power loss. The rollback log may only be partially written, or corrupted in some way where a replay is not possible. This can happen because fsync calls are not guaranteed on all operating systems, filesystems or disk media (e.g. certain SSDs), and really all bets are off if you are using S3 or Couchbase for primary storage.
412
+ This is not a 100% guarantee of data durability, as corruption can happen during a crash or power loss. The rollback log may only be partially written, or corrupted in some way where a replay is not possible. This can happen because fsync calls are not guaranteed on all operating systems, filesystems or disk media (e.g. certain SSDs), and really all bets are off if you are using S3 for primary storage.
413
413
 
414
414
  Basically it's all just a big crap shoot, but we're trying to cover as many error cases as possible.
415
415
 
package/engines/SQLite.js CHANGED
@@ -1,16 +1,15 @@
1
1
  // SQLite Storage Plugin
2
- // Copyright (c) 2023 Joseph Huckaby
2
+ // Copyright (c) 2023 - 2026 Joseph Huckaby
3
3
  // Released under the MIT License
4
4
 
5
- // Requires the 'sqlite3' module from npm
6
- // npm install --save sqlite3
5
+ // Requires the 'better-sqlite3' module
7
6
 
8
7
  const fs = require('fs');
9
8
  const Path = require('path');
10
9
  const zlib = require('zlib');
11
10
  const Class = require("pixl-class");
12
11
  const Component = require("pixl-server/component");
13
- const SQLite3 = require('sqlite3');
12
+ const BetterSqlite3 = require('better-sqlite3');
14
13
  const Tools = require("pixl-tools");
15
14
  const Perf = require("pixl-perf");
16
15
  const Cache = require("pixl-cache");
@@ -81,36 +80,34 @@ module.exports = Class.create({
81
80
  }
82
81
 
83
82
  var db_file = Path.join( this.baseDir, sql_config.filename );
84
- this.logDebug(3, "Opening database file: " + db_file);
85
-
86
- async.series([
87
- function(callback) {
88
- self.db = new SQLite3.Database( db_file, callback );
89
- },
90
- function(callback) {
91
- // optionally set pragmas on the db
92
- if (!sql_config.pragmas) return process.nextTick(callback);
93
- async.eachSeries( Object.keys(sql_config.pragmas),
94
- function(key, callback) {
95
- var value = sql_config.pragmas[key];
96
- self.db.run(`PRAGMA ${key} = ${value};`, callback);
97
- },
98
- callback
99
- ); // eachSeries
100
- },
101
- function(callback) {
102
- // create our table if necessary
103
- self.db.run( 'CREATE TABLE IF NOT EXISTS items( key TEXT PRIMARY KEY, value BLOB, modified INTEGER )', callback );
104
- },
105
- ],
106
- function(err) {
107
- if (err) {
108
- self.logError('sqlite', "FATAL ERROR: Database setup failed: " + err);
109
- return callback(err);
83
+ this.logDebug(3, "Opening database file: " + db_file, sql_config.pragmas || {});
84
+
85
+ try {
86
+ // open sync connection
87
+ this.db = new BetterSqlite3(db_file);
88
+
89
+ // optionally set pragmas on the db
90
+ if (sql_config.pragmas) {
91
+ Object.keys(sql_config.pragmas).forEach(function(key) {
92
+ var value = sql_config.pragmas[key];
93
+ // value may be string (e.g. WAL) or number
94
+ self.db.pragma(`${key} = ${value}`);
95
+ });
110
96
  }
97
+
98
+ // create our table if necessary
99
+ this.db.prepare('CREATE TABLE IF NOT EXISTS items( key TEXT PRIMARY KEY, value BLOB, modified INTEGER )').run();
100
+ }
101
+ catch (err) {
102
+ this.logError('sqlite', "FATAL ERROR: Database setup failed: " + err);
103
+ return callback(err);
104
+ }
105
+
106
+ // behave asynchronously for API compatibility
107
+ process.nextTick(function() {
111
108
  self.logDebug(3, "Setup complete");
112
109
  callback();
113
- }); // async.series
110
+ });
114
111
  },
115
112
 
116
113
  prepKey: function(key) {
@@ -148,20 +145,22 @@ module.exports = Class.create({
148
145
  value = Buffer.from( JSON.stringify( value ) );
149
146
  }
150
147
 
151
- this.db.run( this.commands.put, { $key: key, $value: value, $now: now }, function(err) {
152
- if (err) {
148
+ process.nextTick(function() {
149
+ try {
150
+ self.db.prepare(self.commands.put).run({ key: key, value: value, now: now });
151
+ self.logDebug(9, "Store complete: " + key);
152
+ // possibly cache in LRU
153
+ if (self.cache && !is_binary) {
154
+ self.cache.set( key, value, { date: Tools.timeNow(true) } );
155
+ }
156
+ if (callback) callback(null);
157
+ }
158
+ catch (err) {
153
159
  err.message = "Failed to store object: " + key + ": " + err;
154
160
  self.logError('sqlite', '' + err);
161
+ if (callback) callback(err);
155
162
  }
156
- else self.logDebug(9, "Store complete: " + key);
157
-
158
- // possibly cache in LRU
159
- if (self.cache && !is_binary) {
160
- self.cache.set( key, value, { date: Tools.timeNow(true) } );
161
- }
162
-
163
- if (callback) callback(err);
164
- } ); // put
163
+ });
165
164
  },
166
165
 
167
166
  putStream: function(key, inp, callback) {
@@ -200,24 +199,22 @@ module.exports = Class.create({
200
199
  return;
201
200
  } // cache
202
201
 
203
- this.db.get( this.commands.head, { $key: key }, function(err, row) {
204
- if (err) {
205
- // an actual error
202
+ process.nextTick(function() {
203
+ try {
204
+ var row = self.db.prepare(self.commands.head).get({ key: key });
205
+ if (!row) {
206
+ var err = new Error("Failed to head key: " + key + ": Not found");
207
+ err.code = "NoSuchKey";
208
+ return callback(err, null);
209
+ }
210
+ callback(null, { mod: row.modified, len: row['length(value)'] });
211
+ }
212
+ catch (err) {
206
213
  err.message = "Failed to head key: " + key + ": " + err;
207
214
  self.logError('sqlite', '' + err);
208
215
  callback(err);
209
216
  }
210
- else if (!row) {
211
- // record not found
212
- // always use "NoSuchKey" in error code
213
- var err = new Error("Failed to head key: " + key + ": Not found");
214
- err.code = "NoSuchKey";
215
- callback( err, null );
216
- }
217
- else {
218
- callback( null, { mod: row.modified, len: row['length(value)'] } );
219
- }
220
- }); // head
217
+ });
221
218
  },
222
219
 
223
220
  get: function(key, callback) {
@@ -247,44 +244,36 @@ module.exports = Class.create({
247
244
 
248
245
  this.logDebug(9, "Fetching SQLite Object: " + key);
249
246
 
250
- this.db.get( this.commands.get, { $key: key }, function(err, row) {
251
- if (err) {
252
- // an actual error
253
- err.message = "Failed to fetch key: " + key + ": " + err;
254
- self.logError('sqlite', '' + err);
255
- callback(err);
256
- }
257
- else if (!row) {
258
- // record not found
259
- // always use "NoSuchKey" in error code
260
- var err = new Error("Failed to fetch key: " + key + ": Not found");
261
- err.code = "NoSuchKey";
262
- callback( err, null );
263
- }
264
- else {
265
- // success
247
+ process.nextTick(function() {
248
+ try {
249
+ var row = self.db.prepare(self.commands.get).get({ key: key });
250
+ if (!row) {
251
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
252
+ err.code = "NoSuchKey";
253
+ return callback(err, null);
254
+ }
266
255
  if (is_binary) {
267
256
  self.logDebug(9, "Binary fetch complete: " + key, '' + row.value.length + ' bytes');
268
- callback( null, row.value );
257
+ return callback(null, row.value);
258
+ }
259
+ if (self.cache) {
260
+ self.cache.set( key, row.value, { date: Tools.timeNow(true) } );
269
261
  }
270
- else {
271
- // possibly cache in LRU
272
- if (self.cache) {
273
- self.cache.set( key, row.value, { date: Tools.timeNow(true) } );
274
- }
275
-
276
- var json = null;
277
- try { json = JSON.parse( row.value.toString() ); }
278
- catch (err) {
279
- self.logError('sqlite', "Failed to parse JSON record: " + key + ": " + err);
280
- callback( err, null );
281
- return;
282
- }
283
- self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? json : null);
284
- callback( null, json );
262
+ var json = null;
263
+ try { json = JSON.parse( row.value.toString() ); }
264
+ catch (err) {
265
+ self.logError('sqlite', "Failed to parse JSON record: " + key + ": " + err);
266
+ return callback(err, null);
285
267
  }
268
+ self.logDebug(9, "JSON fetch complete: " + key, self.debugLevel(10) ? json : null);
269
+ callback(null, json);
270
+ }
271
+ catch (err) {
272
+ err.message = "Failed to fetch key: " + key + ": " + err;
273
+ self.logError('sqlite', '' + err);
274
+ callback(err);
286
275
  }
287
- }); // get
276
+ });
288
277
  },
289
278
 
290
279
  getBuffer: function(key, callback) {
@@ -294,26 +283,23 @@ module.exports = Class.create({
294
283
 
295
284
  this.logDebug(9, "Fetching SQLite Object: " + key);
296
285
 
297
- this.db.get( this.commands.get, { $key: key }, function(err, row) {
298
- if (err) {
299
- // an actual error
286
+ process.nextTick(function() {
287
+ try {
288
+ var row = self.db.prepare(self.commands.get).get({ key: key });
289
+ if (!row) {
290
+ var err = new Error("Failed to fetch key: " + key + ": Not found");
291
+ err.code = "NoSuchKey";
292
+ return callback(err, null);
293
+ }
294
+ self.logDebug(9, "Binary fetch complete: " + key, '' + row.value.length + ' bytes');
295
+ callback(null, row.value);
296
+ }
297
+ catch (err) {
300
298
  err.message = "Failed to fetch key: " + key + ": " + err;
301
299
  self.logError('sqlite', '' + err);
302
300
  callback(err);
303
301
  }
304
- else if (!row) {
305
- // record not found
306
- // always use "NoSuchKey" in error code
307
- var err = new Error("Failed to fetch key: " + key + ": Not found");
308
- err.code = "NoSuchKey";
309
- callback( err, null );
310
- }
311
- else {
312
- // success
313
- self.logDebug(9, "Binary fetch complete: " + key, '' + row.value.length + ' bytes');
314
- callback( null, row.value );
315
- }
316
- }); // get
302
+ });
317
303
  },
318
304
 
319
305
  getStream: function(key, callback) {
@@ -390,25 +376,25 @@ module.exports = Class.create({
390
376
 
391
377
  this.logDebug(9, "Deleting SQLite Object: " + key);
392
378
 
393
- this.db.run( this.commands.delete, { $key: key }, function(err) {
394
- // In sqlite3 callbacks `this` is special, and contains `changes`
395
- if (!err && !this.changes) {
396
- err = new Error("Failed to delete object: " + key + ": Not found");
397
- err.code = "NoSuchKey";
379
+ process.nextTick(function() {
380
+ try {
381
+ var info = self.db.prepare(self.commands.delete).run({ key: key });
382
+ if (!info.changes) {
383
+ var err = new Error("Failed to delete object: " + key + ": Not found");
384
+ err.code = "NoSuchKey";
385
+ return callback(err);
386
+ }
387
+ self.logDebug(9, "Delete complete: " + key);
388
+ if (self.cache && self.cache.has(key)) {
389
+ self.cache.delete(key);
390
+ }
391
+ callback();
398
392
  }
399
- if (err) {
393
+ catch (err) {
400
394
  self.logError('sqlite', "Failed to delete object: " + key + ": " + err);
401
- return callback(err);
402
- }
403
- self.logDebug(9, "Delete complete: " + key);
404
-
405
- // possibly delete from LRU cache as well
406
- if (self.cache && self.cache.has(key)) {
407
- self.cache.delete(key);
395
+ callback(err);
408
396
  }
409
-
410
- callback();
411
- }); // delete
397
+ });
412
398
  },
413
399
 
414
400
  runMaintenance: function(callback) {
@@ -528,39 +514,30 @@ module.exports = Class.create({
528
514
  } );
529
515
  }; // compress
530
516
 
531
- // Perform the backup -- as long as WAL mode is enabled, this should only acquire a shared lock on the DB
532
- // allowing reads and writes to continue (albeit slowly), and NOT be included in the backup
533
- // see: https://sqlite.org/forum/forumpost/95c0e12f4c
517
+ // Perform the backup using better-sqlite3's async backup API
518
+ // As long as WAL mode is enabled, this should only acquire a shared lock
534
519
  perf.begin('backup');
535
-
536
520
  var db = this.db;
537
- var backup = db.backup(file);
538
- backup.step(-1);
539
-
540
- var timer = setInterval( function() {
541
- if (backup.idle) {
542
- backup.step(-1);
543
- }
544
-
545
- if (backup.completed) {
546
- perf.end('backup');
547
- clearTimeout(timer);
548
- self.logDebug(6, "SQLite backup operation completed: " + file);
549
-
550
- // now compress or keep or done
551
- if (backups.compress) compress();
552
- else if (backups.keep) keep();
553
- else finish();
554
- }
555
-
556
- if (backup.failed) {
557
- perf.end('backup');
558
- clearTimeout(timer);
559
- self.logError('backup', "SQLite backup operation failed. Please check permissions and available disk space.");
560
- fs.unlink(file, noop);
561
- finish();
521
+ db.backup(file, {
522
+ progress: function(info) {
523
+ // transfer pages per tick (tuneable)
524
+ return 100;
562
525
  }
563
- }, 10 );
526
+ })
527
+ .then(function() {
528
+ perf.end('backup');
529
+ self.logDebug(6, "SQLite backup operation completed: " + file);
530
+ // now compress or keep or done
531
+ if (backups.compress) compress();
532
+ else if (backups.keep) keep();
533
+ else finish();
534
+ })
535
+ .catch(function(err) {
536
+ perf.end('backup');
537
+ self.logError('backup', "SQLite backup operation failed. Please check permissions and available disk space. " + err);
538
+ fs.unlink(file, noop);
539
+ finish();
540
+ });
564
541
  },
565
542
 
566
543
  shutdown: function(callback) {
@@ -569,15 +546,15 @@ module.exports = Class.create({
569
546
  this.logDebug(2, "Shutting down SQLite");
570
547
 
571
548
  if (this.db) {
572
- // close db
573
- self.db.close( function(err) {
574
- if (err) self.logError('sqlite', "Failed to shutdown database cleanly: " + err);
575
- else self.logDebug(3, "Shutdown complete");
576
- callback();
577
- } );
578
- self.db = null;
579
- self.commands = null;
549
+ try { this.db.close(); }
550
+ catch (err) { this.logError('sqlite', "Failed to shutdown database cleanly: " + err); }
551
+ this.db = null;
552
+ this.commands = null;
580
553
  }
554
+ process.nextTick(function() {
555
+ self.logDebug(3, "Shutdown complete");
556
+ callback();
557
+ });
581
558
  }
582
559
 
583
560
  });
@@ -83,6 +83,23 @@ module.exports = Class.create({
83
83
 
84
84
  filterWords_number: function(value) {
85
85
  // filter number queries
86
+
87
+ // support simple date-to-epoch conversion here
88
+ if (value.match(/^(\d{4})\D+(\d{2})\D+(\d{2})$/)) {
89
+ var dargs = Tools.getDateArgs( value + " 00:00:00" );
90
+ value = '' + (dargs.epoch || 0);
91
+ }
92
+ else if (value.match(/^(today)$/)) {
93
+ // normalize "today" to midnight today (server timezone)
94
+ var midnight = Tools.normalizeTime( Tools.timeNow(true), { hour:0, min:0, sec:0 } );
95
+ var dargs = Tools.getDateArgs( midnight );
96
+ value = '' + (dargs.epoch || 0);
97
+ }
98
+ else if (value.match(/^(now)$/)) {
99
+ var dargs = Tools.getDateArgs( Tools.timeNow(true) );
100
+ value = '' + (dargs.epoch || 0);
101
+ }
102
+
86
103
  value = value.replace(/\.\d+$/, '').replace(/[^\d\-]+/g, '').replace(/\-/, 'N');
87
104
  return value;
88
105
  },
package/indexer.js CHANGED
@@ -1105,6 +1105,13 @@ module.exports = Class.create({
1105
1105
  // range between two values (inclusive)
1106
1106
  var low = RegExp.$1;
1107
1107
  var high = RegExp.$2;
1108
+
1109
+ var lwords = this.getWordList(low, def, config);
1110
+ if (lwords.length) low = lwords[0];
1111
+
1112
+ var hwords = this.getWordList(high, def, config);
1113
+ if (hwords.length) high = hwords[0];
1114
+
1108
1115
  crit = {
1109
1116
  mode: 'and',
1110
1117
  criteria: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixl-server-storage",
3
- "version": "3.2.8",
3
+ "version": "4.0.0",
4
4
  "description": "A key/value/list storage component for the pixl-server framework.",
5
5
  "author": "Joseph Huckaby <jhuckaby@gmail.com>",
6
6
  "homepage": "https://github.com/jhuckaby/pixl-server-storage",
Binary file