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/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +315 -0
- package/assets/icon.jpeg +0 -0
- package/index.d.ts +288 -0
- package/lib/ruggy.dll +0 -0
- package/package.json +42 -0
- package/src/Collection.js +176 -0
- package/src/Database.js +217 -0
- package/src/Pool.js +251 -0
- package/src/bindings.js +34 -0
- package/src/config.js +124 -0
- package/src/index.js +58 -0
- package/src/utils.js +73 -0
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;
|
package/src/Database.js
ADDED
|
@@ -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;
|