crondb-driver 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 +16 -0
- package/crondb.js +159 -0
- package/package.json +19 -0
- package/test_sdk.js +79 -0
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# CronDB Node.js SDK
|
|
2
|
+
|
|
3
|
+
The official asynchronous ORM driver for **CronDB**, a high-performance, temporal C++ database engine.
|
|
4
|
+
|
|
5
|
+
Traditional architectures force you to build "split-brain" systems to handle time-based events: storing the data in a database while pushing the timers to an external task queue.
|
|
6
|
+
|
|
7
|
+
**CronDB unifies state and time.** You inject finite-state-machine (FSM) timers directly into your database rows. When a state naturally expires, the C++ engine's asynchronous threads instantly push a webhook to your Node.js server.
|
|
8
|
+
|
|
9
|
+
No polling. No external queues. Just one unified temporal engine.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 📦 Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install crondb
|
package/crondb.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
|
|
3
|
+
class CronDB {
|
|
4
|
+
/**
|
|
5
|
+
* Initialize the CronDB Client
|
|
6
|
+
* @param {string} host - The IP of the C++ Engine
|
|
7
|
+
* @param {number} port - The port the engine is listening on
|
|
8
|
+
*/
|
|
9
|
+
constructor(host = '127.0.0.1', port = 8080) {
|
|
10
|
+
this.host = host;
|
|
11
|
+
this.port = port;
|
|
12
|
+
// Keep-Alive Agent to prevent TCP exhaustion during heavy FSM loads
|
|
13
|
+
this.agent = new http.Agent({ keepAlive: true, maxSockets: 100 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Send a raw SQL command to the engine
|
|
18
|
+
* @param {string} sqlString - The CronDB SQL command
|
|
19
|
+
* @returns {Promise<Object|string>} - The JSON or String response
|
|
20
|
+
*/
|
|
21
|
+
async query(sqlString) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const options = {
|
|
24
|
+
hostname: this.host,
|
|
25
|
+
port: this.port,
|
|
26
|
+
path: '/query',
|
|
27
|
+
method: 'POST',
|
|
28
|
+
agent: this.agent, // Uses our connection pool
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'text/plain',
|
|
31
|
+
'Content-Length': Buffer.byteLength(sqlString)
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const req = http.request(options, (res) => {
|
|
36
|
+
let data = '';
|
|
37
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
38
|
+
res.on('end', () => {
|
|
39
|
+
try {
|
|
40
|
+
resolve(JSON.parse(data)); // Try to parse pure JSON
|
|
41
|
+
} catch (e) {
|
|
42
|
+
resolve(data); // Fallback for raw strings like "success!"
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
req.on('error', (e) => reject(`[CronDB Connection Error]: ${e.message}`));
|
|
48
|
+
req.write(sqlString);
|
|
49
|
+
req.end();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ==========================================================
|
|
54
|
+
// ORM LAYER (Object-Relational Mapping)
|
|
55
|
+
// ==========================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Insert a native JavaScript object into the database
|
|
59
|
+
* @param {string} tableName
|
|
60
|
+
* @param {Object} dataObj - e.g., { name: "Lancelot", hp: 100 }
|
|
61
|
+
*/
|
|
62
|
+
async insert(tableName, dataObj) {
|
|
63
|
+
let sql = `INSERT INTO ${tableName}`;
|
|
64
|
+
|
|
65
|
+
for (const [key, value] of Object.entries(dataObj)) {
|
|
66
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
67
|
+
// Numbers and booleans stay raw
|
|
68
|
+
sql += ` ${key}=${value}`;
|
|
69
|
+
} else if (typeof value === 'string') {
|
|
70
|
+
const trimmed = value.trim();
|
|
71
|
+
// Smart check: Is it a CronDB Temporal FSM? e.g. "( 'Alive' --> 'Dead' @ 5 s )"
|
|
72
|
+
if (trimmed.startsWith('(') && trimmed.endsWith(')')) {
|
|
73
|
+
sql += ` ${key}=${trimmed}`; // Don't quote FSMs
|
|
74
|
+
} else {
|
|
75
|
+
sql += ` ${key}='${value}'`; // Wrap normal strings in quotes
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return await this.query(sql);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Attach a Webhook Listener to a Temporal Column
|
|
84
|
+
* @param {string} tableName - The target table
|
|
85
|
+
* @param {string} columnName - The column with the time-state
|
|
86
|
+
* @param {string} webhookUrl - Where the C++ engine should shoot the HTTP POST
|
|
87
|
+
* @param {string} [whereClause=null] - Optional condition (e.g., "pid = 5")
|
|
88
|
+
*/
|
|
89
|
+
async listen(tableName, columnName, webhookUrl, whereClause = null) {
|
|
90
|
+
let sql = `LISTEN ${tableName} . ${columnName} --> '${webhookUrl}'`;
|
|
91
|
+
|
|
92
|
+
// If the developer wants to listen to a specific row, append it!
|
|
93
|
+
if (whereClause) {
|
|
94
|
+
sql += ` WHERE ${whereClause}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return await this.query(sql);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* READ: Select rows from a table
|
|
102
|
+
* @param {string} tableName
|
|
103
|
+
* @param {string} [whereClause=null] - Optional (e.g., "energy < 50")
|
|
104
|
+
*/
|
|
105
|
+
async select(tableName, whereClause = null) {
|
|
106
|
+
let sql = `SELECT * FROM ${tableName}`;
|
|
107
|
+
if (whereClause) {
|
|
108
|
+
sql += ` WHERE ${whereClause}`;
|
|
109
|
+
}
|
|
110
|
+
return await this.query(sql);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* UPDATE: Modify existing rows using a native JS object
|
|
115
|
+
* @param {string} tableName
|
|
116
|
+
* @param {Object} dataObj - The fields to update (e.g., { status: "Dead", hp: 0 })
|
|
117
|
+
* @param {string} whereClause - Required for safety (e.g., "pid = 5")
|
|
118
|
+
*/
|
|
119
|
+
async update(tableName, dataObj, whereClause) {
|
|
120
|
+
if (!whereClause) throw new Error("CronDB ORM: Update requires a WHERE clause to prevent accidental table wipes.");
|
|
121
|
+
|
|
122
|
+
let sql = `UPDATE ${tableName} SET`;
|
|
123
|
+
let isFirst = true;
|
|
124
|
+
|
|
125
|
+
for (const [key, value] of Object.entries(dataObj)) {
|
|
126
|
+
if (!isFirst) sql += ","; // Optional: depending on if your C++ parser uses commas for UPDATE
|
|
127
|
+
isFirst = false;
|
|
128
|
+
|
|
129
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
130
|
+
sql += ` ${key}=${value}`;
|
|
131
|
+
} else if (typeof value === 'string') {
|
|
132
|
+
const trimmed = value.trim();
|
|
133
|
+
// Smart check for Temporal FSMs
|
|
134
|
+
if (trimmed.startsWith('(') && trimmed.endsWith(')')) {
|
|
135
|
+
sql += ` ${key}=${trimmed}`;
|
|
136
|
+
} else {
|
|
137
|
+
sql += ` ${key}='${value}'`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
sql += ` WHERE ${whereClause}`;
|
|
143
|
+
return await this.query(sql);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* DELETE: Remove rows from a table
|
|
148
|
+
* @param {string} tableName
|
|
149
|
+
* @param {string} whereClause - Required for safety
|
|
150
|
+
*/
|
|
151
|
+
async delete(tableName, whereClause) {
|
|
152
|
+
if (!whereClause) throw new Error("CronDB ORM: Delete requires a WHERE clause.");
|
|
153
|
+
|
|
154
|
+
let sql = `DELETE FROM ${tableName} WHERE ${whereClause}`;
|
|
155
|
+
return await this.query(sql);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = CronDB;
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "crondb-driver",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The official Node.js ORM and connection driver for the CronDB temporal database engine.",
|
|
5
|
+
"main": "crondb.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node test_sdk.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"database",
|
|
11
|
+
"temporal",
|
|
12
|
+
"fsm",
|
|
13
|
+
"orm",
|
|
14
|
+
"crondb",
|
|
15
|
+
"events"
|
|
16
|
+
],
|
|
17
|
+
"author": "Michael Tal",
|
|
18
|
+
"license": "ISC"
|
|
19
|
+
}
|
package/test_sdk.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const CronDB = require('./crondb');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
|
|
4
|
+
// 1. Spin up a tiny temporary web server to catch the temporal webhooks
|
|
5
|
+
const catcher = http.createServer((req, res) => {
|
|
6
|
+
let body = '';
|
|
7
|
+
req.on('data', chunk => body += chunk.toString());
|
|
8
|
+
req.on('end', () => {
|
|
9
|
+
console.log(`\n🎯 [WEBHOOK CAUGHT] Node.js received: ${body}`);
|
|
10
|
+
res.writeHead(200);
|
|
11
|
+
res.end();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
catcher.listen(3000);
|
|
15
|
+
|
|
16
|
+
// 2. Initialize the SDK
|
|
17
|
+
const db = new CronDB('127.0.0.1', 8080);
|
|
18
|
+
|
|
19
|
+
// Helper function to pause our Node.js script so we can watch time pass
|
|
20
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
21
|
+
|
|
22
|
+
async function runIntegrationTest() {
|
|
23
|
+
console.log("=== STARTING CRONDB SDK INTEGRATION TEST ===\n");
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
console.log("[Phase 1] Forging the schema (Raw Query)...");
|
|
27
|
+
let res = await db.query("CREATE TABLE warriors wid ApexInt name string hp int status string 4");
|
|
28
|
+
console.log("Engine Reply:", res);
|
|
29
|
+
|
|
30
|
+
console.log("\n[Phase 2] Arming the Webhook Listener...");
|
|
31
|
+
res = await db.listen('warriors', 'status', 'http://127.0.0.1:3000');
|
|
32
|
+
console.log("Engine Reply:", res);
|
|
33
|
+
|
|
34
|
+
console.log("\n[Phase 3] CREATE: Using ORM to Insert Data...");
|
|
35
|
+
// Normal JS Object
|
|
36
|
+
await db.insert('warriors', {
|
|
37
|
+
name: 'Tank',
|
|
38
|
+
hp: 200,
|
|
39
|
+
status: 'Defending'
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Temporal FSM Object (Fires in 3 seconds!)
|
|
43
|
+
await db.insert('warriors', {
|
|
44
|
+
name: 'Assassin',
|
|
45
|
+
hp: 80,
|
|
46
|
+
status: "('Hidden' --> 'Revealed' @ 3 s)"
|
|
47
|
+
});
|
|
48
|
+
console.log("Engine Reply: Data successfully planted.");
|
|
49
|
+
|
|
50
|
+
console.log("\n[Phase 4] READ: Using ORM to view Data...");
|
|
51
|
+
let rows = await db.select('warriors');
|
|
52
|
+
console.log("Current State:", rows);
|
|
53
|
+
|
|
54
|
+
console.log("\n[Phase 5] UPDATE: Using ORM to modify Data...");
|
|
55
|
+
await db.update('warriors', { hp: 150 }, "name = 'Tank'");
|
|
56
|
+
console.log("Engine Reply: Tank HP updated to 150.");
|
|
57
|
+
|
|
58
|
+
console.log("\n[Phase 6] DELETE: Using ORM to drop Data...");
|
|
59
|
+
await db.delete('warriors', "name = 'Tank'");
|
|
60
|
+
console.log("Engine Reply: Tank deleted.");
|
|
61
|
+
|
|
62
|
+
console.log("\n[Phase 7] Waiting for Temporal Detonation (3 seconds)...");
|
|
63
|
+
console.log("The clock is ticking...");
|
|
64
|
+
await sleep(4000); // Wait 4 seconds to ensure the 3-second C++ timer pops
|
|
65
|
+
|
|
66
|
+
console.log("\n[Phase 8] Final Read to verify state changes...");
|
|
67
|
+
rows = await db.select('warriors');
|
|
68
|
+
console.log("Final State (Assassin should be Revealed):", rows);
|
|
69
|
+
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error("\n❌ Test Failed:", err);
|
|
72
|
+
} finally {
|
|
73
|
+
console.log("\n=== TEST COMPLETE. SHUTTING DOWN ===");
|
|
74
|
+
catcher.close();
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
runIntegrationTest();
|