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.
Files changed (3) hide show
  1. package/README.md +64 -0
  2. package/package.json +30 -0
  3. 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 };