pixl-server-storage 4.1.0 → 4.2.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 +35 -4
- package/docs/API.md +58 -0
- package/docs/Indexer.md +62 -0
- package/docs/Transactions.md +102 -60
- package/engines/Filesystem.js +59 -27
- package/engines/Hybrid.js +104 -2
- package/engines/Postgres.js +291 -23
- package/engines/Redis.js +262 -28
- package/engines/RedisCluster.js +152 -28
- package/engines/S3.js +53 -25
- package/engines/SQLite.js +253 -21
- package/package.json +1 -1
- package/storage.js +63 -1
- package/test/config.json +4 -0
- package/test/data/backups/backup-2026-06-14-15-49-56.db.gz +0 -0
- package/test/data/backups/backup-2026-06-14-15-58-00.db.gz +0 -0
- package/test/data/backups/backup-2026-06-14-16-29-30.db.gz +0 -0
- package/test/data/backups/backup-2026-06-14-17-29-36.db.gz +0 -0
- package/test/data/backups/backup-2026-06-14-17-31-49.db.gz +0 -0
- package/test/data/backups/backup-2026-06-14-18-02-10.db.gz +0 -0
- package/test/data/backups/backup-2026-06-14-18-02-55.db.gz +0 -0
- package/test/test-main.js +17 -0
- package/test/test.js +59 -38
- package/transaction.js +105 -3
- package/test/data/backups/backup-2026-02-15-18-10-33.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-10-45.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-10-58.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-11-10.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-11-23.db.gz +0 -0
- package/test/data/backups/backup-2026-02-15-18-11-35.db.gz +0 -0
- package/test/data/backups/backup-2026-04-05-20-23-56.db.gz +0 -0
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ Here is the table of contents for this current document:
|
|
|
58
58
|
* [Redis](#redis)
|
|
59
59
|
+ [RedisCluster](#rediscluster)
|
|
60
60
|
* [SQLite](#sqlite)
|
|
61
|
+
* [Postgres](#postgres)
|
|
61
62
|
* [Hybrid](#hybrid)
|
|
62
63
|
- [Key Normalization](#key-normalization)
|
|
63
64
|
- [Basic Functions](#basic-functions)
|
|
@@ -66,6 +67,7 @@ Here is the table of contents for this current document:
|
|
|
66
67
|
* [Copying Records](#copying-records)
|
|
67
68
|
* [Renaming Records](#renaming-records)
|
|
68
69
|
* [Deleting Records](#deleting-records)
|
|
70
|
+
* [Optimizing Storage](#optimizing-storage)
|
|
69
71
|
- [Storing Binary Blobs](#storing-binary-blobs)
|
|
70
72
|
- [Using Streams](#using-streams)
|
|
71
73
|
- [Expiring Data](#expiring-data)
|
|
@@ -589,7 +591,12 @@ Then configure your storage thusly:
|
|
|
589
591
|
"host": "127.0.0.1",
|
|
590
592
|
"port": 6379,
|
|
591
593
|
"keyPrefix": "",
|
|
592
|
-
"keyTemplate": ""
|
|
594
|
+
"keyTemplate": "",
|
|
595
|
+
"cache": {
|
|
596
|
+
"enabled": true,
|
|
597
|
+
"maxItems": 100000,
|
|
598
|
+
"maxBytes": 104857600
|
|
599
|
+
}
|
|
593
600
|
}
|
|
594
601
|
}
|
|
595
602
|
```
|
|
@@ -600,6 +607,8 @@ The optional `keyPrefix` property works similarly to the [S3 Key Prefix](#s3-key
|
|
|
600
607
|
|
|
601
608
|
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.
|
|
602
609
|
|
|
610
|
+
See [S3 Cache](#s3-cache) for details on the `cache` section, as it works in the same way.
|
|
611
|
+
|
|
603
612
|
### RedisCluster
|
|
604
613
|
|
|
605
614
|
If you want to use a Redis cluster (e.g. [AWS ElastiCache](https://aws.amazon.com/elasticache/)), then here is how to do that. First, you will need to manually install the following module into your app:
|
|
@@ -625,7 +634,12 @@ Then configure your storage thusly:
|
|
|
625
634
|
}
|
|
626
635
|
},
|
|
627
636
|
"keyPrefix": "",
|
|
628
|
-
"keyTemplate": ""
|
|
637
|
+
"keyTemplate": "",
|
|
638
|
+
"cache": {
|
|
639
|
+
"enabled": true,
|
|
640
|
+
"maxItems": 100000,
|
|
641
|
+
"maxBytes": 104857600
|
|
642
|
+
}
|
|
629
643
|
}
|
|
630
644
|
}
|
|
631
645
|
```
|
|
@@ -640,6 +654,8 @@ The optional `keyPrefix` property works similarly to the [S3 Key Prefix](#s3-key
|
|
|
640
654
|
|
|
641
655
|
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.
|
|
642
656
|
|
|
657
|
+
See [S3 Cache](#s3-cache) for details on the `cache` section, as it works in the same way.
|
|
658
|
+
|
|
643
659
|
## SQLite
|
|
644
660
|
|
|
645
661
|
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:
|
|
@@ -782,6 +798,8 @@ The `Hybrid` engine only has two properties, `docEngine` and `binaryEngine`. Th
|
|
|
782
798
|
|
|
783
799
|
Note that all of the engine configuration objects are on the same level as the `Hybrid` object.
|
|
784
800
|
|
|
801
|
+
When transactions are enabled, the `Hybrid` engine routes transaction commits through the configured `docEngine`, because pixl-server-storage transactions only apply to JSON records. If the `docEngine` supports native transactions, such as Postgres, Redis or SQLite, `Hybrid` will use that native commit path automatically. The `binaryEngine` is not involved in transactions, because binary keys are passed straight through.
|
|
802
|
+
|
|
785
803
|
# Key Normalization
|
|
786
804
|
|
|
787
805
|
In order to maintain compatibility with all the various engines, keys are "normalized" on all entry points. Specifically, they undergo the following transformations before being passed along to the engine:
|
|
@@ -914,6 +932,19 @@ storage.delete( 'test1', function(err) {
|
|
|
914
932
|
} );
|
|
915
933
|
```
|
|
916
934
|
|
|
935
|
+
## Optimizing Storage
|
|
936
|
+
|
|
937
|
+
To ask the current engine to run any engine-specific optimization routine, call the [optimize()](https://github.com/jhuckaby/pixl-server-storage/blob/master/docs/API.md#optimize) method. This waits for the storage queue and advisory locks to drain, then runs whatever is appropriate for the backing store. SQLite runs `VACUUM`, performs a WAL checkpoint when applicable, and runs an integrity check. Postgres runs `VACUUM` on the configured storage table. Redis and engines without an optimization routine return a no-op report.
|
|
938
|
+
|
|
939
|
+
```js
|
|
940
|
+
storage.optimize( function(err, report) {
|
|
941
|
+
if (err) throw err;
|
|
942
|
+
console.log( report );
|
|
943
|
+
} );
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
The returned report is JSON-friendly and suitable for rendering into logs, Markdown or an admin UI. It includes a `perf` object from [pixl-perf](https://github.com/jhuckaby/pixl-perf) with named timings for the optimization steps. See the [API Reference](https://github.com/jhuckaby/pixl-server-storage/blob/master/docs/API.md#optimize) for details.
|
|
947
|
+
|
|
917
948
|
# Storing Binary Blobs
|
|
918
949
|
|
|
919
950
|
To store a binary value, pass a filled `Buffer` object as the value, and specify a key ending in a "file extension", e.g. `.gif`. The latter requirement is so the engine can detect which records are binary and which are JSON, just by looking at the key. Example:
|
|
@@ -1109,7 +1140,7 @@ Other examples of errors include transaction commit failures and transaction rol
|
|
|
1109
1140
|
Transactions (well, more specifically, all storage actions) are logged with the `category` column set to `transaction`. The `code` column will be one of the following constants, denoting which action took place:
|
|
1110
1141
|
|
|
1111
1142
|
```
|
|
1112
|
-
get, put, head, delete, expire_set, perf_sec, perf_min, commit, index, unindex, search, sort, maint
|
|
1143
|
+
get, put, head, delete, expire_set, perf_sec, perf_min, commit, index, unindex, search, sort, maint, optimize
|
|
1113
1144
|
```
|
|
1114
1145
|
|
|
1115
1146
|
You can control which of these event types are logged, by including a `log_event_types` object in your storage configuration. Include keys with true values for any log event types you want to see logged. Example:
|
|
@@ -1117,7 +1148,7 @@ You can control which of these event types are logged, by including a `log_event
|
|
|
1117
1148
|
```js
|
|
1118
1149
|
log_event_types: {
|
|
1119
1150
|
get:0, put:1, head:0, delete:1, expire_set:1, perf_sec:1, perf_min:1,
|
|
1120
|
-
commit:1, index:1, unindex:1, search:0, sort:0, maint:1
|
|
1151
|
+
commit:1, index:1, unindex:1, search:0, sort:0, maint:1, optimize:1
|
|
1121
1152
|
}
|
|
1122
1153
|
```
|
|
1123
1154
|
|
package/docs/API.md
CHANGED
|
@@ -32,6 +32,7 @@ let storage = server.Storage;
|
|
|
32
32
|
* [shareUnlock](#shareunlock)
|
|
33
33
|
* [expire](#expire)
|
|
34
34
|
* [addRecordType](#addrecordtype)
|
|
35
|
+
* [optimize](#optimize)
|
|
35
36
|
* [getStats](#getstats)
|
|
36
37
|
- [List Methods](#list-methods)
|
|
37
38
|
* [listCreate](#listcreate)
|
|
@@ -430,6 +431,63 @@ storage.addRecordType( 'my_custom_type', {
|
|
|
430
431
|
|
|
431
432
|
See [Custom Record Types](../README.md#custom-record-types) for more details.
|
|
432
433
|
|
|
434
|
+
## optimize
|
|
435
|
+
|
|
436
|
+
```js
|
|
437
|
+
storage.optimize( CALLBACK );
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
The `optimize()` method asks the current storage engine to run any engine-specific compaction or optimization routine, and returns a JSON-friendly report. This is mostly useful for database engines. SQLite runs `VACUUM`, performs a WAL checkpoint when applicable, and runs an integrity check. Postgres runs `VACUUM` on the configured storage table. Redis and engines without an optimization routine return a no-op report.
|
|
441
|
+
|
|
442
|
+
The method waits for the storage queue and advisory locks to drain before starting. Your callback is passed an error if optimization fails, or a report object as the second argument. The `perf` property is the direct result of a [pixl-perf](https://github.com/jhuckaby/pixl-perf) `metrics()` call using second-scale timing. Example:
|
|
443
|
+
|
|
444
|
+
```js
|
|
445
|
+
storage.optimize( function(err, report) {
|
|
446
|
+
if (err) throw err;
|
|
447
|
+
console.log( report );
|
|
448
|
+
} );
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Example SQLite report:
|
|
452
|
+
|
|
453
|
+
```json
|
|
454
|
+
{
|
|
455
|
+
"engine": "SQLite",
|
|
456
|
+
"optimized": true,
|
|
457
|
+
"file": "/let/data/sqlite.db",
|
|
458
|
+
"size_before": 1048576,
|
|
459
|
+
"size_after": 786432,
|
|
460
|
+
"bytes_reclaimed": 262144,
|
|
461
|
+
"integrity_ok": true,
|
|
462
|
+
"integrity_check": "ok",
|
|
463
|
+
"perf": {
|
|
464
|
+
"scale": 1,
|
|
465
|
+
"perf": {
|
|
466
|
+
"total": 0.421,
|
|
467
|
+
"vacuum": 0.31,
|
|
468
|
+
"checkpoint": 0.001,
|
|
469
|
+
"integrity": 0.11
|
|
470
|
+
},
|
|
471
|
+
"counters": {}
|
|
472
|
+
},
|
|
473
|
+
"operations": [
|
|
474
|
+
{
|
|
475
|
+
"name": "vacuum",
|
|
476
|
+
"ok": true,
|
|
477
|
+
"size_before": 1048576,
|
|
478
|
+
"size_after": 786432,
|
|
479
|
+
"bytes_reclaimed": 262144,
|
|
480
|
+
"reclaimed": "256 KB"
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
"name": "integrity_check",
|
|
484
|
+
"ok": true,
|
|
485
|
+
"result": "ok"
|
|
486
|
+
}
|
|
487
|
+
]
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
433
491
|
## getStats
|
|
434
492
|
|
|
435
493
|
```js
|
package/docs/Indexer.md
CHANGED
|
@@ -56,6 +56,7 @@ let storage = server.Storage;
|
|
|
56
56
|
* [Changing Fields](#changing-fields)
|
|
57
57
|
- [Performance Tips](#performance-tips)
|
|
58
58
|
- [Indexer Internals](#indexer-internals)
|
|
59
|
+
* [Sorters](#sorters)
|
|
59
60
|
* [Date and Number Fields](#date-and-number-fields)
|
|
60
61
|
* [Special Metadata](#special-metadata)
|
|
61
62
|
|
|
@@ -1006,6 +1007,67 @@ The `tags` field also has the [master_list](#master-list) property set, so a spe
|
|
|
1006
1007
|
}
|
|
1007
1008
|
```
|
|
1008
1009
|
|
|
1010
|
+
### Sorters
|
|
1011
|
+
|
|
1012
|
+
Sorters are stored a little differently than searchable field indexes. A field index is inverted by word, meaning each unique word gets its own hash of matching record IDs. A sorter is a single paginated [Hash](Hashes.md) containing every indexed record ID for that sorter, with the sorter value stored as the hash value.
|
|
1013
|
+
|
|
1014
|
+
For example, a sorter with an ID of `username` is stored here:
|
|
1015
|
+
|
|
1016
|
+
```
|
|
1017
|
+
index/
|
|
1018
|
+
└ myapp/
|
|
1019
|
+
└ username/
|
|
1020
|
+
├ sort/
|
|
1021
|
+
│ └ data.json
|
|
1022
|
+
└ sort.json
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
Internally, the hash path is:
|
|
1026
|
+
|
|
1027
|
+
```
|
|
1028
|
+
index/myapp/username/sort
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
The hash keys are your primary record IDs, and the hash values are the values that will be used for sorting:
|
|
1032
|
+
|
|
1033
|
+
```js
|
|
1034
|
+
{
|
|
1035
|
+
"TICKET0001": "alice",
|
|
1036
|
+
"TICKET0002": "bob",
|
|
1037
|
+
"TICKET0003": "carol"
|
|
1038
|
+
}
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
Number sorters use the same layout, but store numeric values instead:
|
|
1042
|
+
|
|
1043
|
+
```js
|
|
1044
|
+
{
|
|
1045
|
+
"TICKET0001": 3,
|
|
1046
|
+
"TICKET0002": 12,
|
|
1047
|
+
"TICKET0003": 0
|
|
1048
|
+
}
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
The sorter hash uses `sorter_page_size` from your index configuration, or `1000` if that is not set. This is separate from the standard index hash page size, because sorter hashes can be very large and are commonly scanned page by page.
|
|
1052
|
+
|
|
1053
|
+
When [indexRecord()](API.md#indexrecord) runs, sorter updates happen after the searchable fields have been indexed. For each sorter, the indexer reads the configured `source` path from the source record, applies `default_value` if needed, and writes the value using [hashPut()](API.md#hashput). The record's `_data` record also gets a `_sorters` entry with the last indexed sorter value. This allows the indexer to skip writing a sorter again if the value has not changed.
|
|
1054
|
+
|
|
1055
|
+
When a record is unindexed, or when a sorter has `delete` set, the record ID is removed from the sorter hash using [hashDelete()](API.md#hashdelete).
|
|
1056
|
+
|
|
1057
|
+
The important thing to understand is how sorting actually happens. When you call [sortRecords()](API.md#sortrecords), the indexer does not fetch only the matching IDs from the sorter hash. Instead, it enumerates the entire sorter hash, one page at a time, using [hashEachPage()](API.md#hasheachpage). As each page is loaded, the indexer checks each hash key against the current search result hash. If the ID is present in your result set, the sorter value is plucked out and added to an in-memory list of pairs:
|
|
1058
|
+
|
|
1059
|
+
```js
|
|
1060
|
+
[
|
|
1061
|
+
[ "TICKET0001", "alice" ],
|
|
1062
|
+
[ "TICKET0002", "bob" ],
|
|
1063
|
+
[ "TICKET0003", "carol" ]
|
|
1064
|
+
]
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
Once the full sorter hash has been scanned, only those matching pairs remain. That final list is sorted in memory, using either a numeric comparison for `type: "number"`, or `localeCompare()` for string sorting. The sorted record IDs are then returned as a simple array.
|
|
1068
|
+
|
|
1069
|
+
This design keeps the persistent storage model simple and compact, but it also means sorting cost is based on the size of the entire sorter hash, not just the size of the current search results. For example, sorting 50 matching records still requires scanning the whole sorter hash if the sorter contains 100,000 total records. This is why the [Performance Tips](#performance-tips) recommend only adding sorters for fields you truly need.
|
|
1070
|
+
|
|
1009
1071
|
### Date and Number Fields
|
|
1010
1072
|
|
|
1011
1073
|
Date and number fields are still technically indexed as "words", but some internal trickery is applied to allow for range searches. Essentially numbers and dates are placed into several range buckets. Along with a [Master List](#master-list) which is automatically enabled for these fields, the system can search across a range of values.
|