ruggy 0.1.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/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "ruggy",
3
+ "version": "0.1.0",
4
+ "description": "A simple, fast embedded database for Node.js backed by Rust",
5
+ "main": "src/index.js",
6
+ "types": "index.d.ts",
7
+ "keywords": [
8
+ "database",
9
+ "embedded",
10
+ "rust",
11
+ "ffi",
12
+ "nosql",
13
+ "document-store"
14
+ ],
15
+ "files": [
16
+ "src/",
17
+ "lib/",
18
+ "assets/",
19
+ "index.d.ts",
20
+ "README.md",
21
+ "LICENSE",
22
+ "CHANGELOG.md"
23
+ ],
24
+ "engines": {
25
+ "node": ">=14.0.0"
26
+ },
27
+ "scripts": {
28
+ "test": "node test/run-all.js",
29
+ "bench": "node benchmark/run.js"
30
+ },
31
+ "dependencies": {
32
+ "koffi": "^2.8.0",
33
+ "js-yaml": "^4.1.0"
34
+ },
35
+ "devDependencies": {},
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/Mub1522/ruggy"
39
+ },
40
+ "author": "Andres Diaz",
41
+ "license": "MIT"
42
+ }
@@ -0,0 +1,176 @@
1
+ const {
2
+ ruggy_insert,
3
+ ruggy_find_all,
4
+ ruggy_find,
5
+ ruggy_col_free,
6
+ ruggy_str_free
7
+ } = require('./bindings');
8
+ const { readCString, toCString, isValidPointer } = require('./utils');
9
+
10
+ /**
11
+ * Represents a collection in the Ruggy database
12
+ */
13
+ class RuggyCollection {
14
+ /**
15
+ * @private
16
+ * @type {*} Native pointer to the Rust Collection
17
+ */
18
+ #colPtr = null;
19
+
20
+ /**
21
+ * @private
22
+ * @type {string} Collection name
23
+ */
24
+ #name = null;
25
+
26
+ /**
27
+ * @private
28
+ * @param {*} colPtr - Native pointer from Rust
29
+ * @param {string} name - Collection name
30
+ */
31
+ constructor(colPtr, name) {
32
+ if (!isValidPointer(colPtr)) {
33
+ throw new Error(`Invalid collection pointer for '${name}'`);
34
+ }
35
+ this.#colPtr = colPtr;
36
+ this.#name = name;
37
+ }
38
+
39
+ /**
40
+ * Gets the collection name
41
+ * @returns {string}
42
+ */
43
+ get name() {
44
+ return this.#name;
45
+ }
46
+
47
+ /**
48
+ * Checks if the collection is open
49
+ * @returns {boolean}
50
+ */
51
+ get isOpen() {
52
+ return this.#colPtr !== null;
53
+ }
54
+
55
+ /**
56
+ * Inserts a document into the collection
57
+ * @param {Object} data - Document to insert
58
+ * @returns {string|null} - Generated document ID or null on error
59
+ * @throws {Error} If collection is closed or data is invalid
60
+ */
61
+ insert(data) {
62
+ this.#ensureOpen();
63
+
64
+ if (typeof data !== 'object' || data === null) {
65
+ throw new Error('Data must be a non-null object');
66
+ }
67
+
68
+ const json = JSON.stringify(data);
69
+ const buf = toCString(json);
70
+
71
+ const resPtr = ruggy_insert(this.#colPtr, buf);
72
+
73
+ if (!isValidPointer(resPtr)) {
74
+ return null;
75
+ }
76
+
77
+ const id = readCString(resPtr);
78
+ ruggy_str_free(resPtr);
79
+ return id;
80
+ }
81
+
82
+ /**
83
+ * Finds all documents in the collection
84
+ * @returns {Array<Object>} - Array of documents
85
+ * @throws {Error} If collection is closed
86
+ */
87
+ findAll() {
88
+ this.#ensureOpen();
89
+
90
+ const resPtr = ruggy_find_all(this.#colPtr);
91
+ if (!isValidPointer(resPtr)) {
92
+ return [];
93
+ }
94
+
95
+ const json = readCString(resPtr);
96
+ ruggy_str_free(resPtr);
97
+
98
+ try {
99
+ return JSON.parse(json);
100
+ } catch (error) {
101
+ console.error('Failed to parse JSON from Rust:', error);
102
+ return [];
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Finds documents matching a field-value pair
108
+ * @param {string} field - Field name to search
109
+ * @param {*} value - Value to match (converted to string)
110
+ * @returns {Array<Object>} - Array of matching documents
111
+ * @throws {Error} If collection is closed
112
+ */
113
+ find(field, value) {
114
+ this.#ensureOpen();
115
+
116
+ if (typeof field !== 'string' || !field) {
117
+ throw new Error('Field must be a non-empty string');
118
+ }
119
+
120
+ const fieldBuf = toCString(field);
121
+ const valueBuf = toCString(String(value));
122
+
123
+ const resPtr = ruggy_find(this.#colPtr, fieldBuf, valueBuf);
124
+ if (!isValidPointer(resPtr)) {
125
+ return [];
126
+ }
127
+
128
+ const json = readCString(resPtr);
129
+ ruggy_str_free(resPtr);
130
+
131
+ try {
132
+ return JSON.parse(json);
133
+ } catch (error) {
134
+ console.error('Failed to parse JSON from Rust:', error);
135
+ return [];
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Closes the collection and frees native resources
141
+ * Safe to call multiple times
142
+ */
143
+ close() {
144
+ if (this.#colPtr) {
145
+ ruggy_col_free(this.#colPtr);
146
+ this.#colPtr = null;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * @private
152
+ * Ensures the collection is open
153
+ * @throws {Error} If collection is closed
154
+ */
155
+ #ensureOpen() {
156
+ if (!this.#colPtr) {
157
+ throw new Error(`Collection '${this.#name}' is closed`);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Symbol.dispose implementation for "using" syntax (Node.js 20+)
163
+ */
164
+ [Symbol.dispose]() {
165
+ this.close();
166
+ }
167
+
168
+ /**
169
+ * Symbol.asyncDispose implementation for "await using" syntax
170
+ */
171
+ async [Symbol.asyncDispose]() {
172
+ this.close();
173
+ }
174
+ }
175
+
176
+ module.exports = RuggyCollection;
@@ -0,0 +1,217 @@
1
+ const {
2
+ ruggy_open,
3
+ ruggy_get_collection,
4
+ ruggy_db_free
5
+ } = require('./bindings');
6
+ const { toCString, isValidPointer } = require('./utils');
7
+ const RuggyCollection = require('./Collection');
8
+
9
+ /**
10
+ * Ruggy Database - A simple, fast embedded database
11
+ */
12
+ class RuggyDatabase {
13
+ /**
14
+ * @private
15
+ * @type {*} Native pointer to the Rust Database
16
+ */
17
+ #dbPtr = null;
18
+
19
+ /**
20
+ * @private
21
+ * @type {string} Database path
22
+ */
23
+ #path = null;
24
+
25
+ /**
26
+ * @private
27
+ * @type {Map<string, RuggyCollection>} Cache of open collections
28
+ */
29
+ #collectionCache = new Map();
30
+
31
+ /**
32
+ * Creates a new database connection
33
+ * @param {string} dbPath - Path to the database directory
34
+ * @throws {Error} If the database cannot be opened
35
+ */
36
+ constructor(dbPath) {
37
+ if (typeof dbPath !== 'string' || !dbPath) {
38
+ throw new Error('Database path must be a non-empty string');
39
+ }
40
+
41
+ const buf = toCString(dbPath);
42
+ this.#dbPtr = ruggy_open(buf);
43
+
44
+ if (!isValidPointer(this.#dbPtr)) {
45
+ throw new Error(
46
+ `Failed to open database at '${dbPath}'. ` +
47
+ `Possible causes: invalid path, insufficient permissions, or disk full.`
48
+ );
49
+ }
50
+
51
+ this.#path = dbPath;
52
+ }
53
+
54
+ /**
55
+ * Gets the database path
56
+ * @returns {string}
57
+ */
58
+ get path() {
59
+ return this.#path;
60
+ }
61
+
62
+ /**
63
+ * Checks if the database is open
64
+ * @returns {boolean}
65
+ */
66
+ get isOpen() {
67
+ return this.#dbPtr !== null;
68
+ }
69
+
70
+ /**
71
+ * Gets a collection (creates if doesn't exist)
72
+ * @param {string} name - Collection name
73
+ * @returns {RuggyCollection}
74
+ * @throws {Error} If database is closed or collection cannot be opened
75
+ */
76
+ collection(name) {
77
+ this.#ensureOpen();
78
+
79
+ if (typeof name !== 'string' || !name) {
80
+ throw new Error('Collection name must be a non-empty string');
81
+ }
82
+
83
+ // Return cached collection if available
84
+ if (this.#collectionCache.has(name)) {
85
+ const col = this.#collectionCache.get(name);
86
+ if (col.isOpen) {
87
+ return col;
88
+ }
89
+ // Remove stale reference
90
+ this.#collectionCache.delete(name);
91
+ }
92
+
93
+ // Get or create collection from Rust
94
+ const buf = toCString(name);
95
+ const colPtr = ruggy_get_collection(this.#dbPtr, buf);
96
+
97
+ if (!isValidPointer(colPtr)) {
98
+ throw new Error(`Failed to get collection '${name}'`);
99
+ }
100
+
101
+ const collection = new RuggyCollection(colPtr, name);
102
+ this.#collectionCache.set(name, collection);
103
+ return collection;
104
+ }
105
+
106
+ /**
107
+ * Alias for collection() for semantic clarity
108
+ * @param {string} name - Collection name
109
+ * @returns {RuggyCollection}
110
+ */
111
+ createCollection(name) {
112
+ return this.collection(name);
113
+ }
114
+
115
+ /**
116
+ * Closes all cached collections
117
+ * @private
118
+ */
119
+ #closeAllCollections() {
120
+ for (const col of this.#collectionCache.values()) {
121
+ col.close();
122
+ }
123
+ this.#collectionCache.clear();
124
+ }
125
+
126
+ /**
127
+ * Closes the database and frees native resources
128
+ * Also closes all open collections
129
+ * Safe to call multiple times
130
+ */
131
+ close() {
132
+ if (this.#dbPtr) {
133
+ this.#closeAllCollections();
134
+ ruggy_db_free(this.#dbPtr);
135
+ this.#dbPtr = null;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Helper: Automatically manages collection lifecycle
141
+ * @param {string} name - Collection name
142
+ * @param {Function} callback - Async function that receives the collection
143
+ * @returns {Promise<*>} - Result of callback
144
+ * @throws {Error} If database is closed
145
+ */
146
+ async withCollection(name, callback) {
147
+ const col = this.collection(name);
148
+ try {
149
+ return await callback(col);
150
+ } finally {
151
+ col.close();
152
+ this.#collectionCache.delete(name);
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Static helper: Automatically manages database lifecycle
158
+ * @param {string} path - Database path
159
+ * @param {Function} callback - Async function that receives the database
160
+ * @returns {Promise<*>} - Result of callback
161
+ */
162
+ static async withDatabase(path, callback) {
163
+ const db = new RuggyDatabase(path);
164
+ try {
165
+ return await callback(db);
166
+ } finally {
167
+ db.close();
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Static factory: Creates a database instance using ruggy.yaml configuration
173
+ * Searches for ruggy.yaml from current directory up to root
174
+ * @param {Object} options - Configuration options
175
+ * @param {boolean} options.reload - Force reload configuration (bypass cache)
176
+ * @param {string} options.searchFrom - Directory to start searching from
177
+ * @returns {RuggyDatabase} - Database instance configured from YAML
178
+ * @throws {Error} If database cannot be opened with configured path
179
+ * @example
180
+ * // With ruggy.yaml in project root containing: dataPath: ./my-data
181
+ * const db = RuggyDatabase.fromConfig();
182
+ * const col = db.collection('users');
183
+ */
184
+ static fromConfig(options = {}) {
185
+ const { loadConfig } = require('./config');
186
+ const config = loadConfig(options);
187
+ return new RuggyDatabase(config.dataPath);
188
+ }
189
+
190
+
191
+ /**
192
+ * @private
193
+ * Ensures the database is open
194
+ * @throws {Error} If database is closed
195
+ */
196
+ #ensureOpen() {
197
+ if (!this.#dbPtr) {
198
+ throw new Error(`Database at '${this.#path}' is closed`);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Symbol.dispose implementation for "using" syntax (Node.js 20+)
204
+ */
205
+ [Symbol.dispose]() {
206
+ this.close();
207
+ }
208
+
209
+ /**
210
+ * Symbol.asyncDispose implementation for "await using" syntax
211
+ */
212
+ async [Symbol.asyncDispose]() {
213
+ this.close();
214
+ }
215
+ }
216
+
217
+ module.exports = RuggyDatabase;
package/src/Pool.js ADDED
@@ -0,0 +1,251 @@
1
+ const RuggyDatabase = require('./Database');
2
+
3
+ /**
4
+ * Connection pool for Ruggy database
5
+ * Reuses a single database connection across multiple operations
6
+ * Ideal for long-running applications (servers, background jobs)
7
+ */
8
+ class RuggyPool {
9
+ /**
10
+ * @private
11
+ * @type {RuggyDatabase|null} Pooled database instance
12
+ */
13
+ #db = null;
14
+
15
+ /**
16
+ * @private
17
+ * @type {string} Database path
18
+ */
19
+ #path = null;
20
+
21
+ /**
22
+ * @private
23
+ * @type {boolean} Whether the pool is closed
24
+ */
25
+ #closed = false;
26
+
27
+ /**
28
+ * @private
29
+ * @type {number} Number of active operations
30
+ */
31
+ #activeOperations = 0;
32
+
33
+ /**
34
+ * @private
35
+ * @type {number} Total operations performed
36
+ */
37
+ #totalOperations = 0;
38
+
39
+ /**
40
+ * Creates a new connection pool
41
+ * @param {string} path - Database path
42
+ * @param {Object} options - Pool options
43
+ * @param {boolean} options.lazyConnect - If true, don't open DB until first use (default: true)
44
+ */
45
+ constructor(path, options = {}) {
46
+ if (typeof path !== 'string' || !path) {
47
+ throw new Error('Database path must be a non-empty string');
48
+ }
49
+
50
+ this.#path = path;
51
+
52
+ // Eager connection if requested
53
+ if (options.lazyConnect === false) {
54
+ this.#ensureConnection();
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Static factory: Creates a pool instance using ruggy.yaml configuration
60
+ * Searches for ruggy.yaml from current directory up to root
61
+ * @param {Object} options - Combined configuration and pool options
62
+ * @param {boolean} options.reload - Force reload configuration (bypass cache)
63
+ * @param {string} options.searchFrom - Directory to start searching from
64
+ * @param {boolean} options.lazyConnect - If true, don't open DB until first use (default: true)
65
+ * @returns {RuggyPool} - Pool instance configured from YAML
66
+ * @throws {Error} If database cannot be opened with configured path
67
+ * @example
68
+ * // With ruggy.yaml in project root containing: dataPath: ./my-data
69
+ * const pool = RuggyPool.fromConfig();
70
+ * await pool.withCollection('users', async (col) => { ... });
71
+ */
72
+ static fromConfig(options = {}) {
73
+ const { loadConfig } = require('./config');
74
+ const { reload, searchFrom, ...poolOptions } = options;
75
+ const config = loadConfig({ reload, searchFrom });
76
+ return new RuggyPool(config.dataPath, poolOptions);
77
+ }
78
+
79
+
80
+ /**
81
+ * Gets the database path
82
+ * @returns {string}
83
+ */
84
+ get path() {
85
+ return this.#path;
86
+ }
87
+
88
+ /**
89
+ * Checks if the pool is closed
90
+ * @returns {boolean}
91
+ */
92
+ get isClosed() {
93
+ return this.#closed;
94
+ }
95
+
96
+ /**
97
+ * Checks if there's an active connection
98
+ * @returns {boolean}
99
+ */
100
+ get isConnected() {
101
+ return this.#db !== null && this.#db.isOpen;
102
+ }
103
+
104
+ /**
105
+ * Gets the number of active operations
106
+ * @returns {number}
107
+ */
108
+ get activeOperations() {
109
+ return this.#activeOperations;
110
+ }
111
+
112
+ /**
113
+ * Gets the total number of operations performed
114
+ * @returns {number}
115
+ */
116
+ get totalOperations() {
117
+ return this.#totalOperations;
118
+ }
119
+
120
+ /**
121
+ * Gets pool statistics
122
+ * @returns {Object}
123
+ */
124
+ getStats() {
125
+ return {
126
+ path: this.#path,
127
+ connected: this.isConnected,
128
+ closed: this.#closed,
129
+ activeOperations: this.#activeOperations,
130
+ totalOperations: this.#totalOperations
131
+ };
132
+ }
133
+
134
+ /**
135
+ * @private
136
+ * Ensures a connection exists, creating one if needed
137
+ * @throws {Error} If pool is closed
138
+ */
139
+ #ensureConnection() {
140
+ if (this.#closed) {
141
+ throw new Error('Pool is closed');
142
+ }
143
+
144
+ if (!this.#db || !this.#db.isOpen) {
145
+ this.#db = new RuggyDatabase(this.#path);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Executes a callback with a database connection
151
+ * The connection is reused across calls (not closed after each operation)
152
+ * @param {Function} callback - Async function that receives the database
153
+ * @returns {Promise<*>} - Result of callback
154
+ * @throws {Error} If pool is closed or callback fails
155
+ */
156
+ async withDatabase(callback) {
157
+ if (typeof callback !== 'function') {
158
+ throw new Error('Callback must be a function');
159
+ }
160
+
161
+ this.#ensureConnection();
162
+ this.#activeOperations++;
163
+ this.#totalOperations++;
164
+
165
+ try {
166
+ return await callback(this.#db);
167
+ } catch (error) {
168
+ // Log error but don't close connection (it might be a user error)
169
+ throw error;
170
+ } finally {
171
+ this.#activeOperations--;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Alias for withDatabase for convenience
177
+ * @param {Function} callback - Async function that receives the database
178
+ * @returns {Promise<*>}
179
+ */
180
+ async withDB(callback) {
181
+ return this.withDatabase(callback);
182
+ }
183
+
184
+ /**
185
+ * Helper: Executes a callback with a collection from the pooled database
186
+ * @param {string} collectionName - Collection name
187
+ * @param {Function} callback - Async function that receives the collection
188
+ * @returns {Promise<*>} - Result of callback
189
+ */
190
+ async withCollection(collectionName, callback) {
191
+ return this.withDatabase(async (db) => {
192
+ return db.withCollection(collectionName, callback);
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Closes the pooled connection and releases resources
198
+ * All active operations should complete before calling this
199
+ * Safe to call multiple times
200
+ */
201
+ close() {
202
+ if (this.#closed) {
203
+ return;
204
+ }
205
+
206
+ this.#closed = true;
207
+
208
+ if (this.#db) {
209
+ this.#db.close();
210
+ this.#db = null;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Waits for all active operations to complete, then closes the pool
216
+ * @param {number} timeoutMs - Maximum time to wait in milliseconds (default: 5000)
217
+ * @returns {Promise<void>}
218
+ * @throws {Error} If timeout is reached
219
+ */
220
+ async closeGracefully(timeoutMs = 5000) {
221
+ const startTime = Date.now();
222
+
223
+ while (this.#activeOperations > 0) {
224
+ if (Date.now() - startTime > timeoutMs) {
225
+ throw new Error(
226
+ `Graceful shutdown timeout: ${this.#activeOperations} operations still active`
227
+ );
228
+ }
229
+ await new Promise(resolve => setTimeout(resolve, 10));
230
+ }
231
+
232
+ this.close();
233
+ }
234
+
235
+ /**
236
+ * Symbol.dispose implementation for "using" syntax (Node.js 20+)
237
+ */
238
+ [Symbol.dispose]() {
239
+ this.close();
240
+ }
241
+
242
+ /**
243
+ * Symbol.asyncDispose implementation for "await using" syntax
244
+ * Uses graceful shutdown
245
+ */
246
+ async [Symbol.asyncDispose]() {
247
+ await this.closeGracefully();
248
+ }
249
+ }
250
+
251
+ module.exports = RuggyPool;