starddb 1.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 +64 -0
- package/package.json +30 -0
- package/stardb.js +212 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# StarDDB (Star Document-database)
|
|
2
|
+
|
|
3
|
+
> "Shoot for the moon. Even if you miss, you'll land among the stars." - Norman Vincent Peale
|
|
4
|
+
|
|
5
|
+
StarDDB is a simple to use, lightweight (single file implementation) DB for efficient JSON-like storage management.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Concurrency support via field-level operation queuing
|
|
10
|
+
- Easy-to-use API
|
|
11
|
+
- Automatic background persistence
|
|
12
|
+
- Nested document support
|
|
13
|
+
- File locking for multi-process safety
|
|
14
|
+
- Path traversal protection
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install starddb
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
const { StarDDBField, StarDDB } = require("starddb");
|
|
26
|
+
|
|
27
|
+
// Create a field and queue operations
|
|
28
|
+
const field = new StarDDBField(0);
|
|
29
|
+
field.update("set", 1);
|
|
30
|
+
field.update("mult", 5);
|
|
31
|
+
field.update("div", 0.5);
|
|
32
|
+
await field.flush();
|
|
33
|
+
console.log(field.value); // 10
|
|
34
|
+
|
|
35
|
+
// Use with a database file
|
|
36
|
+
const db = new StarDDB("./data.json", 5);
|
|
37
|
+
const hook = db.db();
|
|
38
|
+
hook.health.update("sub", 30);
|
|
39
|
+
hook.mana.update("mult", 2);
|
|
40
|
+
await db.close();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## API
|
|
44
|
+
|
|
45
|
+
### StarDDBField(value, maxQueueSize)
|
|
46
|
+
|
|
47
|
+
- `value` — Initial value (any JSON-serializable type)
|
|
48
|
+
- `maxQueueSize` — Maximum queued operations (default: 10000)
|
|
49
|
+
|
|
50
|
+
**Methods:**
|
|
51
|
+
- `update(method, value)` — Queue an operation. Methods: `set`, `add`, `sub`, `mult`, `div`
|
|
52
|
+
- `flush()` — Returns a promise that resolves when all queued operations are processed
|
|
53
|
+
|
|
54
|
+
### StarDDB(database, saveTime, databaseHook, options)
|
|
55
|
+
|
|
56
|
+
- `database` — Path to the JSON database file
|
|
57
|
+
- `saveTime` — Seconds between automatic saves
|
|
58
|
+
- `databaseHook` — Optional pre-loaded object (skips file read)
|
|
59
|
+
- `options.safeRoot` — Optional root directory to restrict path traversal
|
|
60
|
+
|
|
61
|
+
**Methods:**
|
|
62
|
+
- `db()` — Get the database hook (object of StarDDBField instances)
|
|
63
|
+
- `flush()` — Wait for all field operations to complete
|
|
64
|
+
- `close()` — Flush, save, and stop the background save interval
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "starddb",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight JSON document database with field-level operation queuing and concurrency support",
|
|
5
|
+
"main": "stardb.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"stardb.js"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node tests/single_field.js && node tests/multi_field.js && node tests/test_crawl.js && node tests/test_errors.js"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/lucidityai/stardb.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"json",
|
|
18
|
+
"database",
|
|
19
|
+
"document-db",
|
|
20
|
+
"queue",
|
|
21
|
+
"concurrency",
|
|
22
|
+
"lightweight",
|
|
23
|
+
"embedded"
|
|
24
|
+
],
|
|
25
|
+
"author": "lucidityai",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"proper-lockfile": "^4.1.2"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/stardb.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const lockfile = require('proper-lockfile');
|
|
4
|
+
|
|
5
|
+
const VALID_METHODS = new Set(['set', 'add', 'sub', 'mult', 'div']);
|
|
6
|
+
const MAX_QUEUE_SIZE = 10000;
|
|
7
|
+
const MAX_RECURSION_DEPTH = 100;
|
|
8
|
+
|
|
9
|
+
class StarDDBField {
|
|
10
|
+
constructor(value = null, maxQueueSize = MAX_QUEUE_SIZE) {
|
|
11
|
+
this.value = value;
|
|
12
|
+
this.queue = [];
|
|
13
|
+
this._running = false;
|
|
14
|
+
this._processingDone = Promise.resolve();
|
|
15
|
+
this._maxQueueSize = maxQueueSize;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
update(method, value) {
|
|
19
|
+
if (!VALID_METHODS.has(method)) {
|
|
20
|
+
throw new Error(`Invalid operation "${method}". Valid operations: ${[...VALID_METHODS].join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
if (method === 'div' && value === 0) {
|
|
23
|
+
throw new Error('Division by zero is not allowed');
|
|
24
|
+
}
|
|
25
|
+
this.queue.push({ method, value });
|
|
26
|
+
if (this.queue.length > this._maxQueueSize) {
|
|
27
|
+
this.queue.pop(); // Remove the item we just pushed
|
|
28
|
+
throw new Error(`Queue is full (max size: ${this._maxQueueSize}). Wait for pending operations to complete.`);
|
|
29
|
+
}
|
|
30
|
+
if (!this._running) {
|
|
31
|
+
this._running = true;
|
|
32
|
+
this._processingDone = this._runList();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async _runList() {
|
|
37
|
+
while (true) {
|
|
38
|
+
const item = this.queue.shift();
|
|
39
|
+
if (!item) {
|
|
40
|
+
this._running = false;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
switch (item.method) {
|
|
45
|
+
case 'set':
|
|
46
|
+
this.value = item.value;
|
|
47
|
+
break;
|
|
48
|
+
case 'add':
|
|
49
|
+
this.value += item.value;
|
|
50
|
+
break;
|
|
51
|
+
case 'sub':
|
|
52
|
+
this.value -= item.value;
|
|
53
|
+
break;
|
|
54
|
+
case 'mult':
|
|
55
|
+
this.value *= item.value;
|
|
56
|
+
break;
|
|
57
|
+
case 'div':
|
|
58
|
+
if (item.value === 0) {
|
|
59
|
+
console.error('[StarDDB] Division by zero skipped');
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
this.value /= item.value;
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
console.error(`[StarDDB] Unknown method "${item.method}" dropped`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns a promise that resolves when all queued operations are processed.
|
|
74
|
+
*/
|
|
75
|
+
async flush() {
|
|
76
|
+
await this._processingDone;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _crawlDb(hook, depth = 0) {
|
|
81
|
+
if (depth > MAX_RECURSION_DEPTH) {
|
|
82
|
+
throw new Error(`Max recursion depth (${MAX_RECURSION_DEPTH}) exceeded. Nested objects are too deep.`);
|
|
83
|
+
}
|
|
84
|
+
for (const key in hook) {
|
|
85
|
+
if (typeof hook[key] === 'object' && hook[key] !== null && !Array.isArray(hook[key])) {
|
|
86
|
+
_crawlDb(hook[key], depth + 1);
|
|
87
|
+
} else {
|
|
88
|
+
hook[key] = new StarDDBField(hook[key]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return hook;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _serializeDb(hook, depth = 0) {
|
|
95
|
+
if (depth > MAX_RECURSION_DEPTH) {
|
|
96
|
+
throw new Error(`Max recursion depth (${MAX_RECURSION_DEPTH}) exceeded during serialization.`);
|
|
97
|
+
}
|
|
98
|
+
const result = {};
|
|
99
|
+
for (const key in hook) {
|
|
100
|
+
if (hook[key] instanceof StarDDBField) {
|
|
101
|
+
result[key] = hook[key].value;
|
|
102
|
+
} else if (typeof hook[key] === 'object' && hook[key] !== null) {
|
|
103
|
+
result[key] = _serializeDb(hook[key], depth + 1);
|
|
104
|
+
} else {
|
|
105
|
+
result[key] = hook[key];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function _safeSave(dbPath, databaseHook) {
|
|
112
|
+
let release = null;
|
|
113
|
+
try {
|
|
114
|
+
release = await lockfile.lock(dbPath, { stale: 10000 });
|
|
115
|
+
const data = _serializeDb(databaseHook);
|
|
116
|
+
fs.writeFileSync(dbPath, JSON.stringify(data), 'utf8');
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error(`[StarDDB] Failed to save database to ${dbPath}:`, err.message);
|
|
119
|
+
throw err;
|
|
120
|
+
} finally {
|
|
121
|
+
if (release) {
|
|
122
|
+
try {
|
|
123
|
+
await release();
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error(`[StarDDB] Failed to release lock on ${dbPath}:`, err.message);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Sanitize and validate a database path.
|
|
133
|
+
* If safeRoot is provided, ensures the resolved path stays within it.
|
|
134
|
+
*/
|
|
135
|
+
function _sanitizePath(dbPath, safeRoot = null) {
|
|
136
|
+
const resolved = path.resolve(dbPath);
|
|
137
|
+
if (safeRoot !== null) {
|
|
138
|
+
const root = path.resolve(safeRoot);
|
|
139
|
+
if (!resolved.startsWith(root + path.sep) && resolved !== root) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Database path escapes safe root "${root}". Resolved path: "${resolved}"`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return resolved;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
class StarDDB {
|
|
149
|
+
constructor(database, saveTime, databaseHook = null, options = {}) {
|
|
150
|
+
this.database = _sanitizePath(database, options.safeRoot || null);
|
|
151
|
+
|
|
152
|
+
if (databaseHook === null) {
|
|
153
|
+
if (!fs.existsSync(this.database)) {
|
|
154
|
+
throw new Error(`Database file does not exist: ${this.database}`);
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const data = fs.readFileSync(this.database, 'utf8');
|
|
158
|
+
databaseHook = JSON.parse(data);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error(`[StarDDB] Failed to read database file ${this.database}:`, err.message);
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.databaseHook = _crawlDb(databaseHook);
|
|
166
|
+
|
|
167
|
+
this._saveInterval = setInterval(async () => {
|
|
168
|
+
try {
|
|
169
|
+
await _safeSave(this.database, this.databaseHook);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
// Error already logged in _safeSave
|
|
172
|
+
}
|
|
173
|
+
}, saveTime * 1000);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
db() {
|
|
177
|
+
return this.databaseHook;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Wait for all pending field operations to complete.
|
|
182
|
+
*/
|
|
183
|
+
async flush() {
|
|
184
|
+
const promises = [];
|
|
185
|
+
for (const key in this.databaseHook) {
|
|
186
|
+
if (this.databaseHook[key] instanceof StarDDBField) {
|
|
187
|
+
promises.push(this.databaseHook[key].flush());
|
|
188
|
+
} else if (typeof this.databaseHook[key] === 'object' && this.databaseHook[key] !== null) {
|
|
189
|
+
this._collectFlush(this.databaseHook[key], promises);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
await Promise.all(promises);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_collectFlush(node, promises) {
|
|
196
|
+
for (const key in node) {
|
|
197
|
+
if (node[key] instanceof StarDDBField) {
|
|
198
|
+
promises.push(node[key].flush());
|
|
199
|
+
} else if (typeof node[key] === 'object' && node[key] !== null) {
|
|
200
|
+
this._collectFlush(node[key], promises);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async close() {
|
|
206
|
+
clearInterval(this._saveInterval);
|
|
207
|
+
await this.flush();
|
|
208
|
+
await _safeSave(this.database, this.databaseHook);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { StarDDBField, StarDDB };
|