From 15b3bb382ec8cddb8d80186359654a0880180b33 Mon Sep 17 00:00:00 2001 From: anonpenguin23 Date: Tue, 20 Jan 2026 10:41:28 +0200 Subject: [PATCH] a lot of fixing --- LICENSE | 2 +- README.md | 1 + examples/basic-usage.ts | 100 +++++++++++ examples/database-crud.ts | 170 +++++++++++++++++++ examples/pubsub-chat.ts | 140 +++++++++++++++ package.json | 29 +++- pnpm-lock.yaml | 206 +++++++++++++++++++++++ src/auth/index.ts | 3 + src/cache/index.ts | 14 ++ src/core/http.ts | 10 +- src/core/index.ts | 10 ++ src/core/interfaces/IAuthStrategy.ts | 28 +++ src/core/interfaces/IHttpTransport.ts | 73 ++++++++ src/core/interfaces/IRetryPolicy.ts | 20 +++ src/core/interfaces/IWebSocketClient.ts | 60 +++++++ src/core/interfaces/index.ts | 4 + src/core/transport/AuthHeaderStrategy.ts | 108 ++++++++++++ src/core/transport/RequestLogger.ts | 116 +++++++++++++ src/core/transport/RequestRetryPolicy.ts | 53 ++++++ src/core/transport/TLSConfiguration.ts | 53 ++++++ src/core/transport/index.ts | 4 + src/db/index.ts | 13 ++ src/functions/index.ts | 2 + src/index.ts | 1 + src/network/index.ts | 7 + src/pubsub/index.ts | 12 ++ src/storage/index.ts | 7 + src/utils/codec.ts | 68 ++++++++ src/utils/index.ts | 3 + src/utils/platform.ts | 44 +++++ src/utils/retry.ts | 58 +++++++ tests/e2e/storage.test.ts | 2 +- 32 files changed, 1414 insertions(+), 7 deletions(-) create mode 100644 examples/basic-usage.ts create mode 100644 examples/database-crud.ts create mode 100644 examples/pubsub-chat.ts create mode 100644 src/auth/index.ts create mode 100644 src/cache/index.ts create mode 100644 src/core/index.ts create mode 100644 src/core/interfaces/IAuthStrategy.ts create mode 100644 src/core/interfaces/IHttpTransport.ts create mode 100644 src/core/interfaces/IRetryPolicy.ts create mode 100644 src/core/interfaces/IWebSocketClient.ts create mode 100644 src/core/interfaces/index.ts create mode 100644 src/core/transport/AuthHeaderStrategy.ts create mode 100644 src/core/transport/RequestLogger.ts create mode 100644 src/core/transport/RequestRetryPolicy.ts create mode 100644 src/core/transport/TLSConfiguration.ts create mode 100644 src/core/transport/index.ts create mode 100644 src/db/index.ts create mode 100644 src/functions/index.ts create mode 100644 src/network/index.ts create mode 100644 src/pubsub/index.ts create mode 100644 src/storage/index.ts create mode 100644 src/utils/codec.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/platform.ts create mode 100644 src/utils/retry.ts diff --git a/LICENSE b/LICENSE index 63b4b68..26da2a2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) [year] [fullname] +Copyright (c) 2026 DeBrosOfficial Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d35b1d4..8399735 100644 --- a/README.md +++ b/README.md @@ -367,6 +367,7 @@ interface ClientConfig { timeout?: number; // Request timeout in ms (default: 30000) maxRetries?: number; // Max retry attempts (default: 3) retryDelayMs?: number; // Delay between retries (default: 1000) + debug?: boolean; // Enable debug logging with full SQL queries (default: false) storage?: StorageAdapter; // For persisting JWT/API key (default: MemoryStorage) wsConfig?: Partial; // WebSocket configuration fetch?: typeof fetch; // Custom fetch implementation diff --git a/examples/basic-usage.ts b/examples/basic-usage.ts new file mode 100644 index 0000000..100480b --- /dev/null +++ b/examples/basic-usage.ts @@ -0,0 +1,100 @@ +/** + * Basic Usage Example + * + * This example demonstrates the fundamental usage of the DeBros Network SDK. + * It covers client initialization, database operations, pub/sub, and caching. + */ + +import { createClient } from '../src/index'; + +async function main() { + // 1. Create client + const client = createClient({ + baseURL: 'http://localhost:6001', + apiKey: 'ak_your_key:default', + debug: true, // Enable debug logging + }); + + console.log('✓ Client created'); + + // 2. Database operations + console.log('\n--- Database Operations ---'); + + // Create table + await client.db.createTable( + `CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + created_at INTEGER DEFAULT (strftime('%s', 'now')) + )` + ); + console.log('✓ Table created'); + + // Insert data + const result = await client.db.exec( + 'INSERT INTO users (name, email) VALUES (?, ?)', + ['Alice Johnson', 'alice@example.com'] + ); + console.log(`✓ Inserted user with ID: ${result.last_insert_id}`); + + // Query data + const users = await client.db.query( + 'SELECT * FROM users WHERE email = ?', + ['alice@example.com'] + ); + console.log('✓ Found users:', users); + + // 3. Pub/Sub messaging + console.log('\n--- Pub/Sub Messaging ---'); + + const subscription = await client.pubsub.subscribe('demo-topic', { + onMessage: (msg) => { + console.log(`✓ Received message: "${msg.data}" at ${new Date(msg.timestamp).toISOString()}`); + }, + onError: (err) => console.error('Subscription error:', err), + }); + console.log('✓ Subscribed to demo-topic'); + + // Publish a message + await client.pubsub.publish('demo-topic', 'Hello from the SDK!'); + console.log('✓ Published message'); + + // Wait a bit for message delivery + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Close subscription + subscription.close(); + console.log('✓ Subscription closed'); + + // 4. Cache operations + console.log('\n--- Cache Operations ---'); + + // Put value with 1-hour TTL + await client.cache.put('default', 'user:alice', { + id: result.last_insert_id, + name: 'Alice Johnson', + email: 'alice@example.com', + }, '1h'); + console.log('✓ Cached user data'); + + // Get value + const cached = await client.cache.get('default', 'user:alice'); + if (cached) { + console.log('✓ Retrieved from cache:', cached.value); + } + + // 5. Network health check + console.log('\n--- Network Operations ---'); + + const healthy = await client.network.health(); + console.log(`✓ Gateway health: ${healthy ? 'OK' : 'FAIL'}`); + + const status = await client.network.status(); + console.log(`✓ Network status: ${status.peer_count} peers connected`); + + console.log('\n--- Example completed successfully ---'); +} + +// Run example +main().catch(console.error); diff --git a/examples/database-crud.ts b/examples/database-crud.ts new file mode 100644 index 0000000..9b7ff3e --- /dev/null +++ b/examples/database-crud.ts @@ -0,0 +1,170 @@ +/** + * Database CRUD Operations Example + * + * Demonstrates comprehensive database operations including: + * - Table creation and schema management + * - Insert, Update, Delete operations + * - QueryBuilder fluent API + * - Repository pattern (ORM-style) + * - Transactions + */ + +import { createClient } from '../src/index'; + +interface User { + id?: number; + name: string; + email: string; + age: number; + active?: number; + created_at?: number; +} + +async function main() { + const client = createClient({ + baseURL: 'http://localhost:6001', + apiKey: 'ak_your_key:default', + }); + + // 1. Create table + console.log('Creating users table...'); + await client.db.createTable( + `CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + age INTEGER, + active INTEGER DEFAULT 1, + created_at INTEGER DEFAULT (strftime('%s', 'now')) + )` + ); + + // 2. Raw SQL INSERT + console.log('\n--- Raw SQL Operations ---'); + const insertResult = await client.db.exec( + 'INSERT INTO users (name, email, age) VALUES (?, ?, ?)', + ['Bob Smith', 'bob@example.com', 30] + ); + console.log('Inserted ID:', insertResult.last_insert_id); + + // 3. Raw SQL UPDATE + await client.db.exec( + 'UPDATE users SET age = ? WHERE id = ?', + [31, insertResult.last_insert_id] + ); + console.log('Updated user age'); + + // 4. Raw SQL SELECT + const users = await client.db.query( + 'SELECT * FROM users WHERE email = ?', + ['bob@example.com'] + ); + console.log('Found users:', users); + + // 5. QueryBuilder + console.log('\n--- QueryBuilder Operations ---'); + + // Insert multiple users for querying + await client.db.exec("INSERT INTO users (name, email, age) VALUES (?, ?, ?)", ["Charlie", "charlie@example.com", 25]); + await client.db.exec("INSERT INTO users (name, email, age) VALUES (?, ?, ?)", ["Diana", "diana@example.com", 35]); + await client.db.exec("INSERT INTO users (name, email, age) VALUES (?, ?, ?)", ["Eve", "eve@example.com", 28]); + + // Complex query with QueryBuilder + const activeUsers = await client.db + .createQueryBuilder('users') + .select('id', 'name', 'email', 'age') + .where('active = ?', [1]) + .andWhere('age > ?', [25]) + .orderBy('age DESC') + .limit(10) + .getMany(); + + console.log('Active users over 25:', activeUsers); + + // Get single user + const singleUser = await client.db + .createQueryBuilder('users') + .where('email = ?', ['charlie@example.com']) + .getOne(); + + console.log('Single user:', singleUser); + + // Count users + const count = await client.db + .createQueryBuilder('users') + .where('age > ?', [25]) + .count(); + + console.log('Users over 25:', count); + + // 6. Repository Pattern (ORM) + console.log('\n--- Repository Pattern ---'); + + const userRepo = client.db.repository('users'); + + // Find all + const allUsers = await userRepo.find({}); + console.log('All users:', allUsers.length); + + // Find with criteria + const youngUsers = await userRepo.find({ age: 25 }); + console.log('Users aged 25:', youngUsers); + + // Find one + const diana = await userRepo.findOne({ email: 'diana@example.com' }); + console.log('Found Diana:', diana); + + // Save (insert new) + const newUser: User = { + name: 'Frank', + email: 'frank@example.com', + age: 40, + }; + await userRepo.save(newUser); + console.log('Saved new user:', newUser); + + // Save (update existing) + if (diana) { + diana.age = 36; + await userRepo.save(diana); + console.log('Updated Diana:', diana); + } + + // Remove + if (newUser.id) { + await userRepo.remove(newUser); + console.log('Deleted Frank'); + } + + // 7. Transactions + console.log('\n--- Transaction Operations ---'); + + const txResults = await client.db.transaction([ + { + kind: 'exec', + sql: 'INSERT INTO users (name, email, age) VALUES (?, ?, ?)', + args: ['Grace', 'grace@example.com', 27], + }, + { + kind: 'exec', + sql: 'UPDATE users SET active = ? WHERE age < ?', + args: [0, 26], + }, + { + kind: 'query', + sql: 'SELECT COUNT(*) as count FROM users WHERE active = ?', + args: [1], + }, + ]); + + console.log('Transaction results:', txResults); + + // 8. Get schema + console.log('\n--- Schema Information ---'); + const schema = await client.db.getSchema(); + console.log('Database schema:', schema); + + console.log('\n--- CRUD operations completed successfully ---'); +} + +main().catch(console.error); diff --git a/examples/pubsub-chat.ts b/examples/pubsub-chat.ts new file mode 100644 index 0000000..58a09dd --- /dev/null +++ b/examples/pubsub-chat.ts @@ -0,0 +1,140 @@ +/** + * Pub/Sub Chat Example + * + * Demonstrates a simple chat application using pub/sub with presence tracking. + * Multiple clients can join a room, send messages, and see who's online. + */ + +import { createClient } from '../src/index'; +import type { PresenceMember } from '../src/index'; + +interface ChatMessage { + user: string; + text: string; + timestamp: number; +} + +async function createChatClient(userName: string, roomName: string) { + const client = createClient({ + baseURL: 'http://localhost:6001', + apiKey: 'ak_your_key:default', + }); + + console.log(`[${userName}] Joining room: ${roomName}...`); + + // Subscribe to chat room with presence + const subscription = await client.pubsub.subscribe(roomName, { + onMessage: (msg) => { + try { + const chatMsg: ChatMessage = JSON.parse(msg.data); + const time = new Date(chatMsg.timestamp).toLocaleTimeString(); + console.log(`[${time}] ${chatMsg.user}: ${chatMsg.text}`); + } catch { + console.log(`[${userName}] Received: ${msg.data}`); + } + }, + onError: (err) => { + console.error(`[${userName}] Error:`, err.message); + }, + onClose: () => { + console.log(`[${userName}] Disconnected from ${roomName}`); + }, + presence: { + enabled: true, + memberId: userName, + meta: { + displayName: userName, + joinedAt: Date.now(), + }, + onJoin: (member: PresenceMember) => { + console.log(`[${userName}] 👋 ${member.memberId} joined the room`); + if (member.meta) { + console.log(`[${userName}] Display name: ${member.meta.displayName}`); + } + }, + onLeave: (member: PresenceMember) => { + console.log(`[${userName}] 👋 ${member.memberId} left the room`); + }, + }, + }); + + console.log(`[${userName}] ✓ Joined ${roomName}`); + + // Send a join message + await sendMessage(client, roomName, userName, 'Hello everyone!'); + + // Helper to send messages + async function sendMessage(client: any, room: string, user: string, text: string) { + const chatMsg: ChatMessage = { + user, + text, + timestamp: Date.now(), + }; + await client.pubsub.publish(room, JSON.stringify(chatMsg)); + } + + // Get current presence + if (subscription.hasPresence()) { + const members = await subscription.getPresence(); + console.log(`[${userName}] Current members in room (${members.length}):`); + members.forEach(m => { + console.log(`[${userName}] - ${m.memberId} (joined at ${new Date(m.joinedAt).toLocaleTimeString()})`); + }); + } + + return { + client, + subscription, + sendMessage: (text: string) => sendMessage(client, roomName, userName, text), + }; +} + +async function main() { + const roomName = 'chat:lobby'; + + // Create first user + const alice = await createChatClient('Alice', roomName); + + // Wait a bit + await new Promise(resolve => setTimeout(resolve, 500)); + + // Create second user + const bob = await createChatClient('Bob', roomName); + + // Wait a bit + await new Promise(resolve => setTimeout(resolve, 500)); + + // Send some messages + await alice.sendMessage('Hey Bob! How are you?'); + await new Promise(resolve => setTimeout(resolve, 200)); + + await bob.sendMessage('Hi Alice! I\'m doing great, thanks!'); + await new Promise(resolve => setTimeout(resolve, 200)); + + await alice.sendMessage('That\'s awesome! Want to grab coffee later?'); + await new Promise(resolve => setTimeout(resolve, 200)); + + await bob.sendMessage('Sure! See you at 3pm?'); + await new Promise(resolve => setTimeout(resolve, 200)); + + await alice.sendMessage('Perfect! See you then! 👋'); + + // Wait to receive all messages + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Get final presence count + const presence = await alice.client.pubsub.getPresence(roomName); + console.log(`\nFinal presence count: ${presence.count} members`); + + // Leave room + console.log('\nClosing connections...'); + alice.subscription.close(); + await new Promise(resolve => setTimeout(resolve, 500)); + + bob.subscription.close(); + await new Promise(resolve => setTimeout(resolve, 500)); + + console.log('\n--- Chat example completed ---'); +} + +main().catch(console.error); diff --git a/package.json b/package.json index ea55365..62ce30c 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,36 @@ { "name": "@debros/network-ts-sdk", - "version": "0.4.3", - "description": "TypeScript SDK for DeBros Network Gateway", + "version": "0.6.0", + "description": "TypeScript SDK for DeBros Network Gateway - Database, PubSub, Cache, Storage, and more", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", "license": "MIT", "author": "DeBrosOfficial", + "keywords": [ + "debros", + "network", + "sdk", + "typescript", + "database", + "rqlite", + "pubsub", + "websocket", + "cache", + "olric", + "ipfs", + "storage", + "wasm", + "serverless", + "distributed", + "gateway" + ], "repository": { "type": "git", - "url": "https://github.com/DeBrosOfficial/network-ts-sdk/tree/v0.0.1" + "url": "https://github.com/DeBrosOfficial/network-ts-sdk" + }, + "bugs": { + "url": "https://github.com/DeBrosOfficial/network-ts-sdk/issues" }, "exports": { ".": { @@ -38,9 +59,11 @@ "@types/node": "^20.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", + "@vitest/coverage-v8": "^1.0.0", "dotenv": "^17.2.3", "eslint": "^8.0.0", "tsup": "^8.0.0", + "typedoc": "^0.25.0", "typescript": "^5.3.0", "vitest": "^1.0.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bac8acf..491a319 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: '@typescript-eslint/parser': specifier: ^6.0.0 version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + '@vitest/coverage-v8': + specifier: ^1.0.0 + version: 1.6.1(vitest@1.6.1(@types/node@20.19.23)) dotenv: specifier: ^17.2.3 version: 17.2.3 @@ -30,6 +33,9 @@ importers: tsup: specifier: ^8.0.0 version: 8.5.0(postcss@8.5.6)(typescript@5.9.3) + typedoc: + specifier: ^0.25.0 + version: 0.25.13(typescript@5.9.3) typescript: specifier: ^5.3.0 version: 5.9.3 @@ -39,6 +45,30 @@ importers: packages: + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.6': + resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.6': + resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -368,6 +398,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + '@jest/schemas@29.6.3': resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -587,6 +621,11 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitest/coverage-v8@1.6.1': + resolution: {integrity: sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==} + peerDependencies: + vitest: 1.6.1 + '@vitest/expect@1.6.1': resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} @@ -627,6 +666,9 @@ packages: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} + ansi-sequence-parser@1.1.3: + resolution: {integrity: sha512-+fksAx9eG3Ab6LDnLs3ZqZa8KVJ/jYnX+D4Qe1azX+LFGFAXqynCQLOdLpNYN/l9e7l6hMWwZbrnctqr6eSQSw==} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -912,6 +954,9 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -967,6 +1012,22 @@ packages: peerDependencies: ws: '*' + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -990,6 +1051,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1028,9 +1092,24 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lunr@2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + marked@4.3.0: + resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} + engines: {node: '>= 12'} + hasBin: true + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -1249,6 +1328,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shiki@0.14.7: + resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1311,6 +1393,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -1390,6 +1476,13 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} + typedoc@0.25.13: + resolution: {integrity: sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ==} + engines: {node: '>= 16'} + hasBin: true + peerDependencies: + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1465,6 +1558,12 @@ packages: jsdom: optional: true + vscode-oniguruma@1.7.0: + resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} + + vscode-textmate@8.0.0: + resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -1518,6 +1617,26 @@ packages: snapshots: + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.28.6': + dependencies: + '@babel/types': 7.28.6 + + '@babel/types@7.28.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@0.2.3': {} + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -1709,6 +1828,8 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@istanbuljs/schema@0.1.3': {} + '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.8 @@ -1908,6 +2029,25 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@vitest/coverage-v8@1.6.1(vitest@1.6.1(@types/node@20.19.23))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.19 + magicast: 0.3.5 + picocolors: 1.1.1 + std-env: 3.10.0 + strip-literal: 2.1.1 + test-exclude: 6.0.0 + vitest: 1.6.1(@types/node@20.19.23) + transitivePeerDependencies: + - supports-color + '@vitest/expect@1.6.1': dependencies: '@vitest/spy': 1.6.1 @@ -1958,6 +2098,8 @@ snapshots: ansi-regex@6.2.2: {} + ansi-sequence-parser@1.1.3: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -2316,6 +2458,8 @@ snapshots: has-flag@4.0.0: {} + html-escaper@2.0.2: {} + human-signals@5.0.0: {} ignore@5.3.2: {} @@ -2354,6 +2498,27 @@ snapshots: dependencies: ws: 8.18.3 + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -2374,6 +2539,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + jsonc-parser@3.3.1: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -2408,10 +2575,24 @@ snapshots: lru-cache@10.4.3: {} + lunr@2.3.9: {} + magic-string@0.30.19: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.3.5: + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.3 + + marked@4.3.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -2610,6 +2791,13 @@ snapshots: shebang-regex@3.0.0: {} + shiki@0.14.7: + dependencies: + ansi-sequence-parser: 1.1.3 + jsonc-parser: 3.3.1 + vscode-oniguruma: 1.7.0 + vscode-textmate: 8.0.0 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -2668,6 +2856,12 @@ snapshots: dependencies: has-flag: 4.0.0 + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + text-table@0.2.0: {} thenify-all@1.6.0: @@ -2743,6 +2937,14 @@ snapshots: type-fest@0.20.2: {} + typedoc@0.25.13(typescript@5.9.3): + dependencies: + lunr: 2.3.9 + marked: 4.3.0 + minimatch: 9.0.5 + shiki: 0.14.7 + typescript: 5.9.3 + typescript@5.9.3: {} ufo@1.6.1: {} @@ -2814,6 +3016,10 @@ snapshots: - supports-color - terser + vscode-oniguruma@1.7.0: {} + + vscode-textmate@8.0.0: {} + webidl-conversions@4.0.2: {} whatwg-url@7.1.0: diff --git a/src/auth/index.ts b/src/auth/index.ts new file mode 100644 index 0000000..b839661 --- /dev/null +++ b/src/auth/index.ts @@ -0,0 +1,3 @@ +export { AuthClient } from "./client"; +export type { AuthConfig, WhoAmI, StorageAdapter } from "./types"; +export { MemoryStorage, LocalStorageAdapter } from "./types"; diff --git a/src/cache/index.ts b/src/cache/index.ts new file mode 100644 index 0000000..8cc3592 --- /dev/null +++ b/src/cache/index.ts @@ -0,0 +1,14 @@ +export { CacheClient } from "./client"; +export type { + CacheGetRequest, + CacheGetResponse, + CachePutRequest, + CachePutResponse, + CacheDeleteRequest, + CacheDeleteResponse, + CacheMultiGetRequest, + CacheMultiGetResponse, + CacheScanRequest, + CacheScanResponse, + CacheHealthResponse, +} from "./client"; diff --git a/src/core/http.ts b/src/core/http.ts index 9a18315..470def4 100644 --- a/src/core/http.ts +++ b/src/core/http.ts @@ -25,6 +25,10 @@ export interface HttpClientConfig { maxRetries?: number; retryDelayMs?: number; fetch?: typeof fetch; + /** + * Enable debug logging (includes full SQL queries and args). Default: false + */ + debug?: boolean; /** * Callback invoked on network errors (after all retries exhausted). * Use this to trigger gateway failover at the application layer. @@ -63,6 +67,7 @@ export class HttpClient { private fetch: typeof fetch; private apiKey?: string; private jwt?: string; + private debug: boolean; private onNetworkError?: NetworkErrorCallback; constructor(config: HttpClientConfig) { @@ -72,6 +77,7 @@ export class HttpClient { this.retryDelayMs = config.retryDelayMs ?? 1000; // Use provided fetch or create one with proper TLS configuration for staging certificates this.fetch = config.fetch ?? createFetchWithTLSConfig(); + this.debug = config.debug ?? false; this.onNetworkError = config.onNetworkError; } @@ -256,7 +262,7 @@ export class HttpClient { const logMessage = `[HttpClient] ${method} ${path} completed in ${duration.toFixed( 2 )}ms`; - if (queryDetails) { + if (queryDetails && this.debug) { console.log(logMessage); console.log(`[HttpClient] ${queryDetails}`); } else { @@ -286,7 +292,7 @@ export class HttpClient { 2 )}ms:`; console.error(errorMessage, error); - if (queryDetails) { + if (queryDetails && this.debug) { console.error(`[HttpClient] ${queryDetails}`); } } diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..89f89d6 --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,10 @@ +export { HttpClient, type HttpClientConfig, type NetworkErrorCallback, type NetworkErrorContext } from "./http"; +export { WSClient, type WSClientConfig } from "./ws"; +export type { IHttpTransport, RequestOptions } from "./interfaces/IHttpTransport"; +export type { IWebSocketClient } from "./interfaces/IWebSocketClient"; +export type { IAuthStrategy, RequestContext } from "./interfaces/IAuthStrategy"; +export type { IRetryPolicy } from "./interfaces/IRetryPolicy"; +export { PathBasedAuthStrategy } from "./transport/AuthHeaderStrategy"; +export { ExponentialBackoffRetryPolicy } from "./transport/RequestRetryPolicy"; +export { RequestLogger } from "./transport/RequestLogger"; +export { TLSConfiguration } from "./transport/TLSConfiguration"; diff --git a/src/core/interfaces/IAuthStrategy.ts b/src/core/interfaces/IAuthStrategy.ts new file mode 100644 index 0000000..7aa2215 --- /dev/null +++ b/src/core/interfaces/IAuthStrategy.ts @@ -0,0 +1,28 @@ +/** + * Request context for authentication + */ +export interface RequestContext { + path: string; + method: string; +} + +/** + * Authentication strategy interface + * Provides abstraction for different authentication header strategies + */ +export interface IAuthStrategy { + /** + * Get authentication headers for a request + */ + getHeaders(context: RequestContext): Record; + + /** + * Set API key + */ + setApiKey(apiKey?: string): void; + + /** + * Set JWT token + */ + setJwt(jwt?: string): void; +} diff --git a/src/core/interfaces/IHttpTransport.ts b/src/core/interfaces/IHttpTransport.ts new file mode 100644 index 0000000..531fa01 --- /dev/null +++ b/src/core/interfaces/IHttpTransport.ts @@ -0,0 +1,73 @@ +/** + * HTTP Request options + */ +export interface RequestOptions { + headers?: Record; + query?: Record; + timeout?: number; +} + +/** + * HTTP Transport abstraction interface + * Provides a testable abstraction layer for HTTP operations + */ +export interface IHttpTransport { + /** + * Perform GET request + */ + get(path: string, options?: RequestOptions): Promise; + + /** + * Perform POST request + */ + post(path: string, body?: any, options?: RequestOptions): Promise; + + /** + * Perform PUT request + */ + put(path: string, body?: any, options?: RequestOptions): Promise; + + /** + * Perform DELETE request + */ + delete(path: string, options?: RequestOptions): Promise; + + /** + * Upload file using multipart/form-data + */ + uploadFile( + path: string, + formData: FormData, + options?: { timeout?: number } + ): Promise; + + /** + * Get binary response (returns Response object for streaming) + */ + getBinary(path: string): Promise; + + /** + * Get base URL + */ + getBaseURL(): string; + + /** + * Get API key + */ + getApiKey(): string | undefined; + + /** + * Get current token (JWT or API key) + */ + getToken(): string | undefined; + + /** + * Set API key for authentication + */ + setApiKey(apiKey?: string): void; + + /** + * Set JWT token for authentication + */ + setJwt(jwt?: string): void; +} diff --git a/src/core/interfaces/IRetryPolicy.ts b/src/core/interfaces/IRetryPolicy.ts new file mode 100644 index 0000000..ea05d43 --- /dev/null +++ b/src/core/interfaces/IRetryPolicy.ts @@ -0,0 +1,20 @@ +/** + * Retry policy interface + * Provides abstraction for retry logic and backoff strategies + */ +export interface IRetryPolicy { + /** + * Determine if request should be retried + */ + shouldRetry(error: any, attempt: number): boolean; + + /** + * Get delay before next retry attempt (in milliseconds) + */ + getDelay(attempt: number): number; + + /** + * Get maximum number of retry attempts + */ + getMaxRetries(): number; +} diff --git a/src/core/interfaces/IWebSocketClient.ts b/src/core/interfaces/IWebSocketClient.ts new file mode 100644 index 0000000..5985234 --- /dev/null +++ b/src/core/interfaces/IWebSocketClient.ts @@ -0,0 +1,60 @@ +/** + * WebSocket Client abstraction interface + * Provides a testable abstraction layer for WebSocket operations + */ +export interface IWebSocketClient { + /** + * Connect to WebSocket server + */ + connect(): Promise; + + /** + * Close WebSocket connection + */ + close(): void; + + /** + * Send data through WebSocket + */ + send(data: string): void; + + /** + * Register message handler + */ + onMessage(handler: (data: string) => void): void; + + /** + * Unregister message handler + */ + offMessage(handler: (data: string) => void): void; + + /** + * Register error handler + */ + onError(handler: (error: Error) => void): void; + + /** + * Unregister error handler + */ + offError(handler: (error: Error) => void): void; + + /** + * Register close handler + */ + onClose(handler: () => void): void; + + /** + * Unregister close handler + */ + offClose(handler: () => void): void; + + /** + * Check if WebSocket is connected + */ + isConnected(): boolean; + + /** + * Get WebSocket URL + */ + get url(): string; +} diff --git a/src/core/interfaces/index.ts b/src/core/interfaces/index.ts new file mode 100644 index 0000000..a85f365 --- /dev/null +++ b/src/core/interfaces/index.ts @@ -0,0 +1,4 @@ +export type { IHttpTransport, RequestOptions } from "./IHttpTransport"; +export type { IWebSocketClient } from "./IWebSocketClient"; +export type { IAuthStrategy, RequestContext } from "./IAuthStrategy"; +export type { IRetryPolicy } from "./IRetryPolicy"; diff --git a/src/core/transport/AuthHeaderStrategy.ts b/src/core/transport/AuthHeaderStrategy.ts new file mode 100644 index 0000000..90bf461 --- /dev/null +++ b/src/core/transport/AuthHeaderStrategy.ts @@ -0,0 +1,108 @@ +import type { IAuthStrategy, RequestContext } from "../interfaces/IAuthStrategy"; + +/** + * Authentication type for different operations + */ +type AuthType = "api-key-only" | "api-key-preferred" | "jwt-preferred" | "both"; + +/** + * Path-based authentication strategy + * Determines which auth credentials to use based on the request path + */ +export class PathBasedAuthStrategy implements IAuthStrategy { + private apiKey?: string; + private jwt?: string; + + /** + * Mapping of path patterns to auth types + */ + private readonly authRules: Array<{ pattern: string; type: AuthType }> = [ + // Database, PubSub, Proxy, Cache: prefer API key + { pattern: "/v1/rqlite/", type: "api-key-only" }, + { pattern: "/v1/pubsub/", type: "api-key-only" }, + { pattern: "/v1/proxy/", type: "api-key-only" }, + { pattern: "/v1/cache/", type: "api-key-only" }, + // Auth operations: prefer API key + { pattern: "/v1/auth/", type: "api-key-preferred" }, + ]; + + constructor(apiKey?: string, jwt?: string) { + this.apiKey = apiKey; + this.jwt = jwt; + } + + /** + * Get authentication headers for a request + */ + getHeaders(context: RequestContext): Record { + const headers: Record = {}; + const authType = this.detectAuthType(context.path); + + switch (authType) { + case "api-key-only": + if (this.apiKey) { + headers["X-API-Key"] = this.apiKey; + } else if (this.jwt) { + // Fallback to JWT if no API key + headers["Authorization"] = `Bearer ${this.jwt}`; + } + break; + + case "api-key-preferred": + if (this.apiKey) { + headers["X-API-Key"] = this.apiKey; + } + if (this.jwt) { + headers["Authorization"] = `Bearer ${this.jwt}`; + } + break; + + case "jwt-preferred": + if (this.jwt) { + headers["Authorization"] = `Bearer ${this.jwt}`; + } + if (this.apiKey) { + headers["X-API-Key"] = this.apiKey; + } + break; + + case "both": + if (this.jwt) { + headers["Authorization"] = `Bearer ${this.jwt}`; + } + if (this.apiKey) { + headers["X-API-Key"] = this.apiKey; + } + break; + } + + return headers; + } + + /** + * Set API key + */ + setApiKey(apiKey?: string): void { + this.apiKey = apiKey; + } + + /** + * Set JWT token + */ + setJwt(jwt?: string): void { + this.jwt = jwt; + } + + /** + * Detect auth type based on path + */ + private detectAuthType(path: string): AuthType { + for (const rule of this.authRules) { + if (path.includes(rule.pattern)) { + return rule.type; + } + } + // Default: send both if available + return "both"; + } +} diff --git a/src/core/transport/RequestLogger.ts b/src/core/transport/RequestLogger.ts new file mode 100644 index 0000000..dcda817 --- /dev/null +++ b/src/core/transport/RequestLogger.ts @@ -0,0 +1,116 @@ +/** + * Request logger for debugging HTTP operations + */ +export class RequestLogger { + private readonly debug: boolean; + + constructor(debug: boolean = false) { + this.debug = debug; + } + + /** + * Log successful request + */ + logSuccess( + method: string, + path: string, + duration: number, + queryDetails?: string + ): void { + if (typeof console === "undefined") return; + + const logMessage = `[HttpClient] ${method} ${path} completed in ${duration.toFixed(2)}ms`; + + if (queryDetails && this.debug) { + console.log(logMessage); + console.log(`[HttpClient] ${queryDetails}`); + } else { + console.log(logMessage); + } + } + + /** + * Log failed request + */ + logError( + method: string, + path: string, + duration: number, + error: any, + queryDetails?: string + ): void { + if (typeof console === "undefined") return; + + // Special handling for 404 on find-one (expected behavior) + const is404FindOne = + path === "/v1/rqlite/find-one" && + error?.httpStatus === 404; + + if (is404FindOne) { + console.warn( + `[HttpClient] ${method} ${path} returned 404 after ${duration.toFixed(2)}ms (expected for optional lookups)` + ); + return; + } + + const errorMessage = `[HttpClient] ${method} ${path} failed after ${duration.toFixed(2)}ms:`; + console.error(errorMessage, error); + + if (queryDetails && this.debug) { + console.error(`[HttpClient] ${queryDetails}`); + } + } + + /** + * Extract query details from request for logging + */ + extractQueryDetails(path: string, body?: any): string | null { + if (!this.debug) return null; + + const isRqliteOperation = path.includes("/v1/rqlite/"); + if (!isRqliteOperation || !body) return null; + + try { + const parsedBody = typeof body === "string" ? JSON.parse(body) : body; + + // Direct SQL query + if (parsedBody.sql) { + let details = `SQL: ${parsedBody.sql}`; + if (parsedBody.args && parsedBody.args.length > 0) { + details += ` | Args: [${parsedBody.args + .map((a: any) => (typeof a === "string" ? `"${a}"` : a)) + .join(", ")}]`; + } + return details; + } + + // Table-based query + if (parsedBody.table) { + let details = `Table: ${parsedBody.table}`; + if (parsedBody.criteria && Object.keys(parsedBody.criteria).length > 0) { + details += ` | Criteria: ${JSON.stringify(parsedBody.criteria)}`; + } + if (parsedBody.options) { + details += ` | Options: ${JSON.stringify(parsedBody.options)}`; + } + if (parsedBody.select) { + details += ` | Select: ${JSON.stringify(parsedBody.select)}`; + } + if (parsedBody.where) { + details += ` | Where: ${JSON.stringify(parsedBody.where)}`; + } + if (parsedBody.limit) { + details += ` | Limit: ${parsedBody.limit}`; + } + if (parsedBody.offset) { + details += ` | Offset: ${parsedBody.offset}`; + } + return details; + } + } catch { + // Failed to parse, ignore + } + + return null; + } +} diff --git a/src/core/transport/RequestRetryPolicy.ts b/src/core/transport/RequestRetryPolicy.ts new file mode 100644 index 0000000..75ca45b --- /dev/null +++ b/src/core/transport/RequestRetryPolicy.ts @@ -0,0 +1,53 @@ +import type { IRetryPolicy } from "../interfaces/IRetryPolicy"; +import { SDKError } from "../../errors"; + +/** + * Exponential backoff retry policy + * Retries failed requests with increasing delays + */ +export class ExponentialBackoffRetryPolicy implements IRetryPolicy { + private readonly maxRetries: number; + private readonly baseDelayMs: number; + + /** + * HTTP status codes that should trigger a retry + */ + private readonly retryableStatusCodes = [408, 429, 500, 502, 503, 504]; + + constructor(maxRetries: number = 3, baseDelayMs: number = 1000) { + this.maxRetries = maxRetries; + this.baseDelayMs = baseDelayMs; + } + + /** + * Determine if request should be retried + */ + shouldRetry(error: any, attempt: number): boolean { + // Don't retry if max attempts reached + if (attempt >= this.maxRetries) { + return false; + } + + // Retry on retryable HTTP errors + if (error instanceof SDKError) { + return this.retryableStatusCodes.includes(error.httpStatus); + } + + // Don't retry other errors + return false; + } + + /** + * Get delay before next retry (exponential backoff) + */ + getDelay(attempt: number): number { + return this.baseDelayMs * (attempt + 1); + } + + /** + * Get maximum number of retry attempts + */ + getMaxRetries(): number { + return this.maxRetries; + } +} diff --git a/src/core/transport/TLSConfiguration.ts b/src/core/transport/TLSConfiguration.ts new file mode 100644 index 0000000..2119e80 --- /dev/null +++ b/src/core/transport/TLSConfiguration.ts @@ -0,0 +1,53 @@ +/** + * TLS Configuration for development/staging environments + * + * WARNING: Only use this in development/testing environments! + * DO NOT disable certificate validation in production. + */ +export class TLSConfiguration { + /** + * Create fetch function with proper TLS configuration + */ + static createFetchWithTLSConfig(): typeof fetch { + // Only allow insecure TLS in development + if (this.shouldAllowInsecure()) { + this.configureInsecureTLS(); + } + + return globalThis.fetch; + } + + /** + * Check if insecure TLS should be allowed + */ + private static shouldAllowInsecure(): boolean { + // Check if we're in Node.js environment + if (typeof process === "undefined" || !process.versions?.node) { + return false; + } + + // Only allow in non-production with explicit flag + const isProduction = process.env.NODE_ENV === "production"; + const allowInsecure = process.env.DEBROS_ALLOW_INSECURE_TLS === "true"; + + return !isProduction && allowInsecure; + } + + /** + * Configure Node.js to allow insecure TLS + * WARNING: Only call in development! + */ + private static configureInsecureTLS(): void { + if (typeof process !== "undefined" && process.env) { + // Allow self-signed/staging certificates for development + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + + if (typeof console !== "undefined") { + console.warn( + "[TLSConfiguration] WARNING: TLS certificate validation disabled for development. " + + "DO NOT use in production!" + ); + } + } + } +} diff --git a/src/core/transport/index.ts b/src/core/transport/index.ts new file mode 100644 index 0000000..27152a0 --- /dev/null +++ b/src/core/transport/index.ts @@ -0,0 +1,4 @@ +export { PathBasedAuthStrategy } from "./AuthHeaderStrategy"; +export { ExponentialBackoffRetryPolicy } from "./RequestRetryPolicy"; +export { RequestLogger } from "./RequestLogger"; +export { TLSConfiguration } from "./TLSConfiguration"; diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..f8c8ee2 --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,13 @@ +export { DBClient } from "./client"; +export { QueryBuilder } from "./qb"; +export { Repository } from "./repository"; +export type { + Entity, + QueryResponse, + TransactionOp, + TransactionRequest, + SelectOptions, + FindOptions, + ColumnDefinition, +} from "./types"; +export { extractTableName, extractPrimaryKey } from "./types"; diff --git a/src/functions/index.ts b/src/functions/index.ts new file mode 100644 index 0000000..dd6fd21 --- /dev/null +++ b/src/functions/index.ts @@ -0,0 +1,2 @@ +export { FunctionsClient, type FunctionsClientConfig } from "./client"; +export type { FunctionResponse, SuccessResponse } from "./types"; diff --git a/src/index.ts b/src/index.ts index cdb63ae..0ee1839 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,6 +43,7 @@ export function createClient(config: ClientConfig): Client { timeout: config.timeout, maxRetries: config.maxRetries, retryDelayMs: config.retryDelayMs, + debug: config.debug, fetch: config.fetch, onNetworkError: config.onNetworkError, }); diff --git a/src/network/index.ts b/src/network/index.ts new file mode 100644 index 0000000..f5d2497 --- /dev/null +++ b/src/network/index.ts @@ -0,0 +1,7 @@ +export { NetworkClient } from "./client"; +export type { + PeerInfo, + NetworkStatus, + ProxyRequest, + ProxyResponse, +} from "./client"; diff --git a/src/pubsub/index.ts b/src/pubsub/index.ts new file mode 100644 index 0000000..fbe7dd4 --- /dev/null +++ b/src/pubsub/index.ts @@ -0,0 +1,12 @@ +export { PubSubClient, Subscription } from "./client"; +export type { + PubSubMessage, + RawEnvelope, + MessageHandler, + ErrorHandler, + CloseHandler, + PresenceMember, + PresenceResponse, + PresenceOptions, + SubscribeOptions, +} from "./types"; diff --git a/src/storage/index.ts b/src/storage/index.ts new file mode 100644 index 0000000..a255cc0 --- /dev/null +++ b/src/storage/index.ts @@ -0,0 +1,7 @@ +export { StorageClient } from "./client"; +export type { + StorageUploadResponse, + StoragePinRequest, + StoragePinResponse, + StorageStatus, +} from "./client"; diff --git a/src/utils/codec.ts b/src/utils/codec.ts new file mode 100644 index 0000000..7e0d5f0 --- /dev/null +++ b/src/utils/codec.ts @@ -0,0 +1,68 @@ +/** + * Base64 Codec for cross-platform encoding/decoding + * Works in both Node.js and browser environments + */ +export class Base64Codec { + /** + * Encode string or Uint8Array to base64 + */ + static encode(input: string | Uint8Array): string { + if (typeof input === "string") { + return this.encodeString(input); + } + return this.encodeBytes(input); + } + + /** + * Encode string to base64 + */ + static encodeString(str: string): string { + if (this.isNode()) { + return Buffer.from(str).toString("base64"); + } + // Browser + return btoa( + encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => + String.fromCharCode(parseInt(p1, 16)) + ) + ); + } + + /** + * Encode Uint8Array to base64 + */ + static encodeBytes(bytes: Uint8Array): string { + if (this.isNode()) { + return Buffer.from(bytes).toString("base64"); + } + // Browser + let binary = ""; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); + } + + /** + * Decode base64 to string + */ + static decode(b64: string): string { + if (this.isNode()) { + return Buffer.from(b64, "base64").toString("utf-8"); + } + // Browser + const binary = atob(b64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return new TextDecoder().decode(bytes); + } + + /** + * Check if running in Node.js environment + */ + private static isNode(): boolean { + return typeof Buffer !== "undefined"; + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..23df933 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,3 @@ +export { Base64Codec } from "./codec"; +export { retryWithBackoff, type RetryConfig } from "./retry"; +export { Platform } from "./platform"; diff --git a/src/utils/platform.ts b/src/utils/platform.ts new file mode 100644 index 0000000..8ceec89 --- /dev/null +++ b/src/utils/platform.ts @@ -0,0 +1,44 @@ +/** + * Platform detection utilities + * Helps determine runtime environment (Node.js vs Browser) + */ +export const Platform = { + /** + * Check if running in Node.js + */ + isNode: (): boolean => { + return typeof process !== "undefined" && !!process.versions?.node; + }, + + /** + * Check if running in browser + */ + isBrowser: (): boolean => { + return typeof window !== "undefined"; + }, + + /** + * Check if localStorage is available + */ + hasLocalStorage: (): boolean => { + try { + return typeof localStorage !== "undefined" && localStorage !== null; + } catch { + return false; + } + }, + + /** + * Check if Buffer is available (Node.js) + */ + hasBuffer: (): boolean => { + return typeof Buffer !== "undefined"; + }, + + /** + * Check if btoa/atob are available (Browser) + */ + hasBase64: (): boolean => { + return typeof btoa !== "undefined" && typeof atob !== "undefined"; + }, +}; diff --git a/src/utils/retry.ts b/src/utils/retry.ts new file mode 100644 index 0000000..5d755e5 --- /dev/null +++ b/src/utils/retry.ts @@ -0,0 +1,58 @@ +/** + * Retry configuration + */ +export interface RetryConfig { + /** + * Maximum number of retry attempts + */ + maxAttempts: number; + + /** + * Function to calculate backoff delay in milliseconds + */ + backoffMs: (attempt: number) => number; + + /** + * Function to determine if error should trigger retry + */ + shouldRetry: (error: any) => boolean; +} + +/** + * Retry an operation with exponential backoff + * @param operation - The async operation to retry + * @param config - Retry configuration + * @returns Promise resolving to operation result + * @throws Last error if all retries exhausted + */ +export async function retryWithBackoff( + operation: () => Promise, + config: RetryConfig +): Promise { + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= config.maxAttempts; attempt++) { + try { + return await operation(); + } catch (error: any) { + lastError = error instanceof Error ? error : new Error(String(error)); + + // Check if we should retry this error + if (!config.shouldRetry(error)) { + throw error; + } + + // If this was the last attempt, throw + if (attempt === config.maxAttempts) { + throw error; + } + + // Wait before next attempt + const delay = config.backoffMs(attempt); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + + // Fallback (should never reach here) + throw lastError || new Error("Retry failed"); +} diff --git a/tests/e2e/storage.test.ts b/tests/e2e/storage.test.ts index 8f7bfa9..0993dd4 100644 --- a/tests/e2e/storage.test.ts +++ b/tests/e2e/storage.test.ts @@ -85,7 +85,7 @@ describe("Storage", () => { const client = await createTestClient(); // First upload and pin a file const testContent = "File for status check"; - const testFile = new File([testContent], "status-test.txt", { + const testFile = new File([testContent], "status-test", { type: "text/plain", });