commit 4a771ae1d41ecbccea7b4fd0f2d672702a3b9ce3 Author: obvtiger Date: Sat May 10 14:51:07 2025 +0200 add types diff --git a/README.md b/README.md new file mode 100644 index 0000000..d03c59c --- /dev/null +++ b/README.md @@ -0,0 +1,177 @@ +# WireKVS-JS + +A Node.js client for the WireKVS database service. This client provides a simple interface to interact with WireKVS databases, including real-time updates via WebSocket connections. + +## Installation + +```bash +npm install wirekvs-js +``` + +## Usage + +There are two ways to use the WireKVS client: + +### 1. Direct Database Connection + +If you already have a database ID and access key, you can connect directly to the database: + +```javascript +const WireKVS = require('wirekvs-js'); + +// Connect directly to a database +const db = WireKVS.connect('your-database-id', 'your-access-key'); + +// Set up real-time listeners with method chaining +db.on('connected', () => console.log('Connected')) + .on('post', event => console.log('New value:', event)) + .on('error', error => console.error('Error:', error)); + +// Use the database +await db.set('key', 'value'); +const value = await db.get('key'); + +// Disconnect when done +db.disconnect(); +``` + +### 2. Using the Client for Database Management + +If you need to create, list, or manage databases: + +```javascript +const WireKVS = require('wirekvs-js'); + +// Initialize with your auth token +const client = new WireKVS('your-auth-token'); + +// Create a new database +const newDb = await client.createDatabase({ + name: 'My Database', + allowPublicWrites: false, + allowPublicReads: true, + allowPublicModifications: false, + allowSpecificPublicReads: false +}); + +// Connect to the new database using its access key +const db = client.database(newDb.kvsId, newDb.accessKey); + +// Use the database... +await db.set('key', 'value'); + +// Disconnect when done +db.disconnect(); +``` + +## Database Operations + +### Basic Operations + +```javascript +// Get all entries +const entries = await db.getAllEntries(); + +// Get a specific value +const value = await db.get('key'); + +// Set a value +await db.set('key', 'value'); + +// Update a value +await db.update('key', 'new value'); + +// Delete a value +await db.delete('key'); + +// Search for entries +const results = await db.search('searchTerm'); +``` + +### Database Management + +```javascript +// List all databases +const databases = await client.listDatabases(); + +// Update database settings +await client.updateDatabase(databaseId, { + name: 'New Name', + allowPublicReads: false +}); + +// Truncate a database +await client.truncateDatabase(databaseId); + +// Delete a database +await client.deleteDatabase(databaseId); +``` + +## Real-time Updates + +The database instance extends EventEmitter and provides real-time updates for all changes: + +### Available Events + +```javascript +// Connection events +db.on('connected', () => console.log('Connected')); +db.on('disconnected', () => console.log('Disconnected')); +db.on('error', error => console.error('Error:', error)); + +// Data events +db.on('post', event => console.log('New value set:', event.key, event.value)); +db.on('patch', event => console.log('Value updated:', event.key, event.value)); +db.on('delete', event => console.log('Value deleted:', event.key)); +db.on('truncate', () => console.log('Database truncated')); +``` + +### Event Handling + +```javascript +// Add event listeners with method chaining +db.on('event1', handler1) + .on('event2', handler2); + +// Remove specific event listener +db.off('event1', handler1); + +// Remove all listeners for an event +db.off('event1'); + +// Disconnect and cleanup +db.disconnect(); // Removes all listeners and closes WebSocket +``` + +## Error Handling + +All async methods will throw errors if the operation fails. It's recommended to use try-catch blocks: + +```javascript +try { + await db.set('key', 'value'); +} catch (error) { + if (error.response) { + // Server responded with error + console.error('Server error:', error.response.data); + } else if (error.request) { + // No response received + console.error('No response:', error.request); + } else { + // Request setup error + console.error('Error:', error.message); + } +} +``` + +## Automatic Reconnection + +The WebSocket connection automatically attempts to reconnect if disconnected unexpectedly. This ensures your real-time updates continue even after temporary network issues. + +## Examples + +Check out the `examples/demo.js` file for a complete working example of all features. + +## License + +MIT diff --git a/examples/demo.js b/examples/demo.js new file mode 100644 index 0000000..20a3669 --- /dev/null +++ b/examples/demo.js @@ -0,0 +1,138 @@ +const WireKVS = require("../src/index"); + +const AUTH_TOKEN = "your-token-from-cookies-here"; + +async function demoDirectConnect() { + try { + console.log("๐Ÿš€ Starting WireKVS Direct Connect Demo"); + + // Connect directly to a database if you already have the ID and access key + const db = WireKVS.connect("your-database-id", "your-access-key"); + console.log("โœ… Connected directly to database"); + + // Set up real-time listeners + db.on("connected", () => console.log("๐ŸŸข WebSocket connected")) + .on("disconnected", () => console.log("๐Ÿ”ด WebSocket disconnected")) + .on("post", (event) => console.log("๐Ÿ“ New value set:", event)) + .on("patch", (event) => console.log("โœ๏ธ Value updated:", event)) + .on("delete", (event) => console.log("๐Ÿ—‘๏ธ Value deleted:", event)) + .on("truncate", () => console.log("๐Ÿงน Database truncated")) + .on("error", (error) => console.error("โŒ WebSocket error:", error)); + + // Wait for WebSocket connection + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Perform operations... + await db.set("greeting", "Hello from direct connect!"); + const value = await db.get("greeting"); + console.log("Value:", value); + + // Disconnect when done + db.disconnect(); + console.log("โœ… Direct connect demo completed successfully!"); + } catch (error) { + console.error( + "โŒ Error during direct connect demo:", + error.response?.data || error.message + ); + } +} + +async function demoWithClient() { + try { + console.log("๐Ÿš€ Starting WireKVS Client Demo"); + + // Initialize the client + const client = new WireKVS(AUTH_TOKEN); + console.log("โœ… Client initialized"); + + // Create a new database + console.log("\n๐Ÿ†• Creating new database..."); + const newDb = await client.createDatabase({ + name: "Demo Database", + allowPublicReads: true, + allowPublicWrites: false, + allowPublicModifications: false, + allowSpecificPublicReads: true, + }); + console.log("Created database:", newDb); + + // Get database instance using the access key from the created database + const db = client.database(newDb.kvsId, newDb.accessKey); + console.log("\n๐Ÿ”Œ Connected to database:", newDb.kvsId); + + // Set up real-time listeners with chaining + db.on("connected", () => console.log("๐ŸŸข WebSocket connected")) + .on("disconnected", () => console.log("๐Ÿ”ด WebSocket disconnected")) + .on("post", (event) => console.log("๐Ÿ“ New value set:", event)) + .on("patch", (event) => console.log("โœ๏ธ Value updated:", event)) + .on("delete", (event) => console.log("๐Ÿ—‘๏ธ Value deleted:", event)) + .on("truncate", () => console.log("๐Ÿงน Database truncated")) + .on("error", (error) => console.error("โŒ WebSocket error:", error)); + + // Wait for WebSocket connection + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Set some values + console.log("\n๐Ÿ’พ Setting values..."); + await db.set("greeting", "Hello, World!"); + await db.set("number", "42"); + await db.set("json", JSON.stringify({ foo: "bar" })); + + // Get all entries + console.log("\n๐Ÿ“– Reading all entries..."); + const entries = await db.getAllEntries(); + console.log("All entries:", entries); + + // Get specific value + console.log("\n๐Ÿ” Reading specific value..."); + const greeting = await db.get("greeting"); + console.log("Greeting:", greeting); + + // Update a value + console.log("\nโœ๏ธ Updating value..."); + await db.update("greeting", "Hello, Updated World!"); + const updatedGreeting = await db.get("greeting"); + console.log("Updated greeting:", updatedGreeting); + + // Search for entries + console.log("\n๐Ÿ”Ž Searching entries..."); + const searchResults = await db.search("Hello"); + console.log("Search results:", searchResults); + + // Delete a value + console.log("\n๐Ÿ—‘๏ธ Deleting value..."); + await db.delete("number"); + + // Truncate database + console.log("\n๐Ÿงน Truncating database..."); + await client.truncateDatabase(newDb.kvsId); + + // Update database settings + console.log("\nโš™๏ธ Updating database settings..."); + await client.updateDatabase(newDb.kvsId, { + name: "Updated Demo Database", + allowPublicReads: false, + }); + + // Clean up + console.log("\n๐Ÿงน Cleaning up..."); + await client.deleteDatabase(newDb.kvsId); + db.disconnect(); + + console.log("\nโœ… Client demo completed successfully!"); + } catch (error) { + console.error( + "\nโŒ Error during client demo:", + error.response?.data || error.message + ); + } +} + +// Run the demos +console.log("Starting demos in 1 second..."); +setTimeout(async () => { + await demoDirectConnect(); + console.log("\n-------------------\n"); + await demoWithClient(); +}, 1000); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..95bba95 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,331 @@ +{ + "name": "wirekvs-js", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "wirekvs-js": "^1.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/wirekvs-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wirekvs-js/-/wirekvs-js-1.0.0.tgz", + "integrity": "sha512-98pgJD+jpc+X/W+7D+zxd6Py7unv6ZGqoRRcLS9jRULYoA1HcpuRxbnABrnOWs5dsejL/8jLM53Hwr0EpByDXQ==", + "license": "MIT", + "dependencies": { + "axios": "^1.6.7", + "events": "^3.3.0", + "ws": "^8.16.0" + } + }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9a59bd3 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "wirekvs-js", + "version": "1.0.0", + "description": "A Node.js client for the WireKVS database service", + "main": "src/index.js", + "types": "src/index.d.ts", + "scripts": { + "test": "jest", + "demo": "node examples/demo.js" + }, + "keywords": [ + "kvs", + "database", + "wireway", + "realtime" + ], + "author": "", + "license": "MIT", + "dependencies": { + "axios": "^1.6.7", + "ws": "^8.16.0", + "events": "^3.3.0" + }, + "devDependencies": { + "jest": "^29.7.0" + } +} \ No newline at end of file diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..0a80e14 --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,74 @@ +import { EventEmitter } from 'events'; +import { AxiosResponse } from 'axios'; + +export interface DatabaseConfig { + name: string; + allowPublicWrites?: boolean; + allowPublicReads?: boolean; + allowPublicModifications?: boolean; + allowSpecificPublicReads?: boolean; +} + +export interface WebSocketMessage { + type: string; + message?: string; + [key: string]: any; +} + +export interface DatabaseEntry { + key: string; + value: any; +} + +export interface SearchQuery { + [key: string]: any; +} + +export interface DatabaseInfo { + id: string; + name: string; + allowPublicWrites: boolean; + allowPublicReads: boolean; + allowPublicModifications: boolean; + allowSpecificPublicReads: boolean; +} + +export class WireKVSDatabase extends EventEmitter { + constructor(id: string, accessKey: string); + + id: string; + accessKey: string; + ws: WebSocket | null; + isConnected: boolean; + + on(event: 'connected' | 'disconnected' | 'error', listener: (...args: any[]) => void): this; + on(event: string, listener: (...args: any[]) => void): this; + + off(event: string, handler?: (...args: any[]) => void): this; + + disconnect(): void; + + getAllEntries(): Promise; + get(key: string): Promise; + set(key: string, value: any): Promise; + update(key: string, value: any): Promise; + delete(key: string): Promise; + search(query: SearchQuery): Promise; +} + +export class WireKVS { + constructor(token: string); + + token: string; + + static connect(id: string, accessKey: string): WireKVSDatabase; + + listDatabases(): Promise; + createDatabase(config: DatabaseConfig): Promise; + updateDatabase(id: string, config: Partial): Promise; + deleteDatabase(id: string): Promise; + truncateDatabase(id: string): Promise; + database(id: string, accessKey: string): WireKVSDatabase; +} + +export default WireKVS; \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..204d0b6 --- /dev/null +++ b/src/index.js @@ -0,0 +1,245 @@ +const axios = require("axios"); +const WebSocket = require("ws"); +const EventEmitter = require("events"); + +const API_BASE_URL = "https://kvs.wireway.ch/v2"; + +class WireKVSDatabase extends EventEmitter { + constructor(id, accessKey) { + super(); + this.id = id; + this.accessKey = accessKey; + this.ws = null; + this.isConnected = false; + this.keepAliveInterval = null; + this._eventHandlers = new Map(); + this._setupWebSocket(); + } + + _setupWebSocket() { + const wsUrl = `wss://kvs.wireway.ch/events/${ + this.id + }?accessKey=${encodeURIComponent(this.accessKey)}`; + this.ws = new WebSocket(wsUrl); + + this.ws.on("open", () => { + this.isConnected = true; + this.emit("connected"); + }); + + this.ws.on("close", () => { + this.isConnected = false; + this.emit("disconnected"); + if (this.keepAliveInterval) { + clearInterval(this.keepAliveInterval); + this.keepAliveInterval = null; + } + + if (!this._disconnecting) { + setTimeout(() => this._setupWebSocket(), 5000); + } + }); + + this.ws.on("message", (data) => { + try { + const message = JSON.parse(data); + if (message.type === "ping") { + return; + } + if (message.type === "error") { + console.error("WebSocket error from server:", message.message); + this.emit("error", new Error(message.message)); + if ( + message.message === "KVSDB not found" || + message.message === "Forbidden" + ) { + this.disconnect(); + return; + } + } + this.emit(message.type, message); + } catch (error) { + console.error("Error parsing WebSocket message:", error); + } + }); + + this.ws.on("error", (error) => { + console.error("WebSocket error:", error); + this.emit("error", error); + }); + } + + on(event, handler) { + super.on(event, handler); + if (!this._eventHandlers.has(event)) { + this._eventHandlers.set(event, new Set()); + } + this._eventHandlers.get(event).add(handler); + return this; + } + + off(event, handler) { + if (handler) { + super.removeListener(event, handler); + const handlers = this._eventHandlers.get(event); + if (handlers) { + handlers.delete(handler); + } + } else { + const handlers = this._eventHandlers.get(event); + if (handlers) { + handlers.forEach((h) => super.removeListener(event, h)); + handlers.clear(); + } + } + return this; + } + + disconnect() { + this._disconnecting = true; + this._eventHandlers.forEach((handlers, event) => { + handlers.forEach((handler) => super.removeListener(event, handler)); + }); + this._eventHandlers.clear(); + + if (this.ws) { + if (this.keepAliveInterval) { + clearInterval(this.keepAliveInterval); + this.keepAliveInterval = null; + } + this.ws.close(); + this.ws = null; + } + this.isConnected = false; + } + + async getAllEntries() { + const response = await axios.get(`${API_BASE_URL}/database/${this.id}`, { + headers: { Authorization: this.accessKey }, + }); + return response.data; + } + + async get(key) { + const response = await axios.get( + `${API_BASE_URL}/database/${this.id}/${key}`, + { + headers: { Authorization: this.accessKey }, + } + ); + return response.data; + } + + async set(key, value) { + await axios.post(`${API_BASE_URL}/database/${this.id}/${key}`, value, { + headers: { Authorization: this.accessKey }, + }); + } + + async update(key, value) { + await axios.patch(`${API_BASE_URL}/database/${this.id}/${key}`, value, { + headers: { Authorization: this.accessKey }, + }); + } + + async delete(key) { + await axios.delete(`${API_BASE_URL}/database/${this.id}/${key}`, { + headers: { Authorization: this.accessKey }, + }); + } + + async search(query) { + const response = await axios.post( + `${API_BASE_URL}/databaseSearch/${this.id}`, + query, + { + headers: { Authorization: this.accessKey }, + } + ); + return response.data; + } +} + +class WireKVS { + constructor(token) { + this.token = token; + } + + static connect(id, accessKey) { + return new WireKVSDatabase(id, accessKey); + } + + async listDatabases() { + const response = await axios.get(`${API_BASE_URL}/databases`, { + headers: { Authorization: this.token }, + }); + return response.data; + } + + async createDatabase({ + name, + allowPublicWrites = false, + allowPublicReads = false, + allowPublicModifications = false, + allowSpecificPublicReads = false, + }) { + const response = await axios.post( + `${API_BASE_URL}/database`, + { + name, + allowPublicWrites, + allowPublicReads, + allowPublicModifications, + allowSpecificPublicReads, + }, + { + headers: { Authorization: this.token }, + } + ); + return response.data; + } + + async updateDatabase( + id, + { + name, + allowPublicWrites, + allowPublicReads, + allowPublicModifications, + allowSpecificPublicReads, + } + ) { + const response = await axios.patch( + `${API_BASE_URL}/database/${id}`, + { + name, + allowPublicWrites, + allowPublicReads, + allowPublicModifications, + allowSpecificPublicReads, + }, + { + headers: { Authorization: this.token }, + } + ); + return response.data; + } + + async deleteDatabase(id) { + await axios.delete(`${API_BASE_URL}/database/${id}`, { + headers: { Authorization: this.token }, + }); + } + + async truncateDatabase(id) { + await axios.delete(`${API_BASE_URL}/databaseTruncate/${id}`, { + headers: { Authorization: this.token }, + }); + } + + database(id, accessKey) { + return new WireKVSDatabase(id, accessKey); + } +} + +module.exports = WireKVS; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}