mirror of
https://github.com/DeBrosOfficial/network-ts-sdk.git
synced 2026-01-30 13:03:04 +00:00
Compare commits
No commits in common. "main" and "v0.4.0" have entirely different histories.
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 DeBrosOfficial
|
||||
Copyright (c) [year] [fullname]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@ -367,7 +367,6 @@ 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<WSClientConfig>; // WebSocket configuration
|
||||
fetch?: typeof fetch; // Custom fetch implementation
|
||||
|
||||
@ -1,100 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
@ -1,170 +0,0 @@
|
||||
/**
|
||||
* 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<User>(
|
||||
'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<User>();
|
||||
|
||||
console.log('Active users over 25:', activeUsers);
|
||||
|
||||
// Get single user
|
||||
const singleUser = await client.db
|
||||
.createQueryBuilder('users')
|
||||
.where('email = ?', ['charlie@example.com'])
|
||||
.getOne<User>();
|
||||
|
||||
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<User>('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);
|
||||
@ -1,140 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
29
package.json
29
package.json
@ -1,36 +1,15 @@
|
||||
{
|
||||
"name": "@debros/network-ts-sdk",
|
||||
"version": "0.6.0",
|
||||
"description": "TypeScript SDK for DeBros Network Gateway - Database, PubSub, Cache, Storage, and more",
|
||||
"version": "0.4.0",
|
||||
"description": "TypeScript SDK for DeBros Network Gateway",
|
||||
"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"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/DeBrosOfficial/network-ts-sdk/issues"
|
||||
"url": "https://github.com/DeBrosOfficial/network-ts-sdk/tree/v0.0.1"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
@ -59,11 +38,9 @@
|
||||
"@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"
|
||||
},
|
||||
|
||||
206
pnpm-lock.yaml
generated
206
pnpm-lock.yaml
generated
@ -21,9 +21,6 @@ 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
|
||||
@ -33,9 +30,6 @@ 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
|
||||
@ -45,30 +39,6 @@ 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'}
|
||||
@ -398,10 +368,6 @@ 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}
|
||||
@ -621,11 +587,6 @@ 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==}
|
||||
|
||||
@ -666,9 +627,6 @@ 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'}
|
||||
@ -954,9 +912,6 @@ 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'}
|
||||
@ -1012,22 +967,6 @@ 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==}
|
||||
|
||||
@ -1051,9 +990,6 @@ 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==}
|
||||
|
||||
@ -1092,24 +1028,9 @@ 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==}
|
||||
|
||||
@ -1328,9 +1249,6 @@ 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==}
|
||||
|
||||
@ -1393,10 +1311,6 @@ 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==}
|
||||
|
||||
@ -1476,13 +1390,6 @@ 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'}
|
||||
@ -1558,12 +1465,6 @@ 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==}
|
||||
|
||||
@ -1617,26 +1518,6 @@ 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
|
||||
|
||||
@ -1828,8 +1709,6 @@ 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
|
||||
@ -2029,25 +1908,6 @@ 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
|
||||
@ -2098,8 +1958,6 @@ snapshots:
|
||||
|
||||
ansi-regex@6.2.2: {}
|
||||
|
||||
ansi-sequence-parser@1.1.3: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
@ -2458,8 +2316,6 @@ snapshots:
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
human-signals@5.0.0: {}
|
||||
|
||||
ignore@5.3.2: {}
|
||||
@ -2498,27 +2354,6 @@ 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
|
||||
@ -2539,8 +2374,6 @@ snapshots:
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
jsonc-parser@3.3.1: {}
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
@ -2575,24 +2408,10 @@ 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: {}
|
||||
@ -2791,13 +2610,6 @@ 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: {}
|
||||
@ -2856,12 +2668,6 @@ 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:
|
||||
@ -2937,14 +2743,6 @@ 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: {}
|
||||
@ -3016,10 +2814,6 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vscode-oniguruma@1.7.0: {}
|
||||
|
||||
vscode-textmate@8.0.0: {}
|
||||
|
||||
webidl-conversions@4.0.2: {}
|
||||
|
||||
whatwg-url@7.1.0:
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
export { AuthClient } from "./client";
|
||||
export type { AuthConfig, WhoAmI, StorageAdapter } from "./types";
|
||||
export { MemoryStorage, LocalStorageAdapter } from "./types";
|
||||
14
src/cache/index.ts
vendored
14
src/cache/index.ts
vendored
@ -1,14 +0,0 @@
|
||||
export { CacheClient } from "./client";
|
||||
export type {
|
||||
CacheGetRequest,
|
||||
CacheGetResponse,
|
||||
CachePutRequest,
|
||||
CachePutResponse,
|
||||
CacheDeleteRequest,
|
||||
CacheDeleteResponse,
|
||||
CacheMultiGetRequest,
|
||||
CacheMultiGetResponse,
|
||||
CacheScanRequest,
|
||||
CacheScanResponse,
|
||||
CacheHealthResponse,
|
||||
} from "./client";
|
||||
133
src/core/http.ts
133
src/core/http.ts
@ -1,39 +1,11 @@
|
||||
import { SDKError } from "../errors";
|
||||
|
||||
/**
|
||||
* Context provided to the onNetworkError callback
|
||||
*/
|
||||
export interface NetworkErrorContext {
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" | "WS";
|
||||
path: string;
|
||||
isRetry: boolean;
|
||||
attempt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when a network error occurs.
|
||||
* Use this to trigger gateway failover or other error handling.
|
||||
*/
|
||||
export type NetworkErrorCallback = (
|
||||
error: SDKError,
|
||||
context: NetworkErrorContext
|
||||
) => void;
|
||||
|
||||
export interface HttpClientConfig {
|
||||
baseURL: string;
|
||||
timeout?: number;
|
||||
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.
|
||||
*/
|
||||
onNetworkError?: NetworkErrorCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -67,25 +39,14 @@ export class HttpClient {
|
||||
private fetch: typeof fetch;
|
||||
private apiKey?: string;
|
||||
private jwt?: string;
|
||||
private debug: boolean;
|
||||
private onNetworkError?: NetworkErrorCallback;
|
||||
|
||||
constructor(config: HttpClientConfig) {
|
||||
this.baseURL = config.baseURL.replace(/\/$/, "");
|
||||
this.timeout = config.timeout ?? 60000;
|
||||
this.timeout = config.timeout ?? 60000; // Increased from 30s to 60s for pub/sub operations
|
||||
this.maxRetries = config.maxRetries ?? 3;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the network error callback
|
||||
*/
|
||||
setOnNetworkError(callback: NetworkErrorCallback | undefined): void {
|
||||
this.onNetworkError = callback;
|
||||
}
|
||||
|
||||
setApiKey(apiKey?: string) {
|
||||
@ -160,13 +121,6 @@ export class HttpClient {
|
||||
return this.apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base URL
|
||||
*/
|
||||
getBaseURL(): string {
|
||||
return this.baseURL;
|
||||
}
|
||||
|
||||
async request<T = any>(
|
||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||
path: string,
|
||||
@ -262,7 +216,7 @@ export class HttpClient {
|
||||
const logMessage = `[HttpClient] ${method} ${path} completed in ${duration.toFixed(
|
||||
2
|
||||
)}ms`;
|
||||
if (queryDetails && this.debug) {
|
||||
if (queryDetails) {
|
||||
console.log(logMessage);
|
||||
console.log(`[HttpClient] ${queryDetails}`);
|
||||
} else {
|
||||
@ -292,32 +246,11 @@ export class HttpClient {
|
||||
2
|
||||
)}ms:`;
|
||||
console.error(errorMessage, error);
|
||||
if (queryDetails && this.debug) {
|
||||
if (queryDetails) {
|
||||
console.error(`[HttpClient] ${queryDetails}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call the network error callback if configured
|
||||
// This allows the app to trigger gateway failover
|
||||
if (this.onNetworkError) {
|
||||
// Convert native errors (TypeError, AbortError) to SDKError for the callback
|
||||
const sdkError =
|
||||
error instanceof SDKError
|
||||
? error
|
||||
: new SDKError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
0, // httpStatus 0 indicates network-level failure
|
||||
"NETWORK_ERROR"
|
||||
);
|
||||
this.onNetworkError(sdkError, {
|
||||
method,
|
||||
path,
|
||||
isRetry: false,
|
||||
attempt: this.maxRetries, // All retries exhausted
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
@ -343,31 +276,22 @@ export class HttpClient {
|
||||
throw SDKError.fromResponse(response.status, body);
|
||||
}
|
||||
|
||||
// Request succeeded - return response
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (contentType?.includes("application/json")) {
|
||||
return response.json();
|
||||
}
|
||||
return response.text();
|
||||
} catch (error) {
|
||||
const isRetryableError =
|
||||
if (
|
||||
error instanceof SDKError &&
|
||||
[408, 429, 500, 502, 503, 504].includes(error.httpStatus);
|
||||
|
||||
// Retry on same gateway for retryable HTTP errors
|
||||
if (isRetryableError && attempt < this.maxRetries) {
|
||||
if (typeof console !== "undefined") {
|
||||
console.warn(
|
||||
`[HttpClient] Retrying request (attempt ${attempt + 1}/${this.maxRetries})`
|
||||
);
|
||||
}
|
||||
attempt < this.maxRetries &&
|
||||
[408, 429, 500, 502, 503, 504].includes(error.httpStatus)
|
||||
) {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, this.retryDelayMs * (attempt + 1))
|
||||
);
|
||||
return this.requestWithRetry(url, options, attempt + 1, startTime);
|
||||
}
|
||||
|
||||
// All retries exhausted - throw error for app to handle
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -457,25 +381,6 @@ export class HttpClient {
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Call the network error callback if configured
|
||||
if (this.onNetworkError) {
|
||||
const sdkError =
|
||||
error instanceof SDKError
|
||||
? error
|
||||
: new SDKError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
0,
|
||||
"NETWORK_ERROR"
|
||||
);
|
||||
this.onNetworkError(sdkError, {
|
||||
method: "POST",
|
||||
path,
|
||||
isRetry: false,
|
||||
attempt: this.maxRetries,
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
@ -504,33 +409,17 @@ export class HttpClient {
|
||||
const response = await this.fetch(url.toString(), fetchOptions);
|
||||
if (!response.ok) {
|
||||
clearTimeout(timeoutId);
|
||||
const errorBody = await response.json().catch(() => ({
|
||||
const error = await response.json().catch(() => ({
|
||||
error: response.statusText,
|
||||
}));
|
||||
throw SDKError.fromResponse(response.status, errorBody);
|
||||
throw SDKError.fromResponse(response.status, error);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Call the network error callback if configured
|
||||
if (this.onNetworkError) {
|
||||
const sdkError =
|
||||
error instanceof SDKError
|
||||
? error
|
||||
: new SDKError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
0,
|
||||
"NETWORK_ERROR"
|
||||
);
|
||||
this.onNetworkError(sdkError, {
|
||||
method: "GET",
|
||||
path,
|
||||
isRetry: false,
|
||||
attempt: 0,
|
||||
});
|
||||
if (error instanceof SDKError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
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";
|
||||
@ -1,28 +0,0 @@
|
||||
/**
|
||||
* 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<string, string>;
|
||||
|
||||
/**
|
||||
* Set API key
|
||||
*/
|
||||
setApiKey(apiKey?: string): void;
|
||||
|
||||
/**
|
||||
* Set JWT token
|
||||
*/
|
||||
setJwt(jwt?: string): void;
|
||||
}
|
||||
@ -1,73 +0,0 @@
|
||||
/**
|
||||
* HTTP Request options
|
||||
*/
|
||||
export interface RequestOptions {
|
||||
headers?: Record<string, string>;
|
||||
query?: Record<string, string | number | boolean>;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP Transport abstraction interface
|
||||
* Provides a testable abstraction layer for HTTP operations
|
||||
*/
|
||||
export interface IHttpTransport {
|
||||
/**
|
||||
* Perform GET request
|
||||
*/
|
||||
get<T = any>(path: string, options?: RequestOptions): Promise<T>;
|
||||
|
||||
/**
|
||||
* Perform POST request
|
||||
*/
|
||||
post<T = any>(path: string, body?: any, options?: RequestOptions): Promise<T>;
|
||||
|
||||
/**
|
||||
* Perform PUT request
|
||||
*/
|
||||
put<T = any>(path: string, body?: any, options?: RequestOptions): Promise<T>;
|
||||
|
||||
/**
|
||||
* Perform DELETE request
|
||||
*/
|
||||
delete<T = any>(path: string, options?: RequestOptions): Promise<T>;
|
||||
|
||||
/**
|
||||
* Upload file using multipart/form-data
|
||||
*/
|
||||
uploadFile<T = any>(
|
||||
path: string,
|
||||
formData: FormData,
|
||||
options?: { timeout?: number }
|
||||
): Promise<T>;
|
||||
|
||||
/**
|
||||
* Get binary response (returns Response object for streaming)
|
||||
*/
|
||||
getBinary(path: string): Promise<Response>;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
/**
|
||||
* WebSocket Client abstraction interface
|
||||
* Provides a testable abstraction layer for WebSocket operations
|
||||
*/
|
||||
export interface IWebSocketClient {
|
||||
/**
|
||||
* Connect to WebSocket server
|
||||
*/
|
||||
connect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export type { IHttpTransport, RequestOptions } from "./IHttpTransport";
|
||||
export type { IWebSocketClient } from "./IWebSocketClient";
|
||||
export type { IAuthStrategy, RequestContext } from "./IAuthStrategy";
|
||||
export type { IRetryPolicy } from "./IRetryPolicy";
|
||||
@ -1,108 +0,0 @@
|
||||
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<string, string> {
|
||||
const headers: Record<string, string> = {};
|
||||
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";
|
||||
}
|
||||
}
|
||||
@ -1,116 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
/**
|
||||
* 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!"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export { PathBasedAuthStrategy } from "./AuthHeaderStrategy";
|
||||
export { ExponentialBackoffRetryPolicy } from "./RequestRetryPolicy";
|
||||
export { RequestLogger } from "./RequestLogger";
|
||||
export { TLSConfiguration } from "./TLSConfiguration";
|
||||
@ -1,17 +1,11 @@
|
||||
import WebSocket from "isomorphic-ws";
|
||||
import { SDKError } from "../errors";
|
||||
import { NetworkErrorCallback } from "./http";
|
||||
|
||||
export interface WSClientConfig {
|
||||
wsURL: string;
|
||||
timeout?: number;
|
||||
authToken?: string;
|
||||
WebSocket?: typeof WebSocket;
|
||||
/**
|
||||
* Callback invoked on WebSocket errors.
|
||||
* Use this to trigger gateway failover at the application layer.
|
||||
*/
|
||||
onNetworkError?: NetworkErrorCallback;
|
||||
}
|
||||
|
||||
export type WSMessageHandler = (data: string) => void;
|
||||
@ -21,15 +15,13 @@ export type WSOpenHandler = () => void;
|
||||
|
||||
/**
|
||||
* Simple WebSocket client with minimal abstractions
|
||||
* No complex reconnection, no failover - keep it simple
|
||||
* Gateway failover is handled at the application layer
|
||||
* No complex reconnection, no heartbeats - keep it simple
|
||||
*/
|
||||
export class WSClient {
|
||||
private wsURL: string;
|
||||
private url: string;
|
||||
private timeout: number;
|
||||
private authToken?: string;
|
||||
private WebSocketClass: typeof WebSocket;
|
||||
private onNetworkError?: NetworkErrorCallback;
|
||||
|
||||
private ws?: WebSocket;
|
||||
private messageHandlers: Set<WSMessageHandler> = new Set();
|
||||
@ -39,25 +31,10 @@ export class WSClient {
|
||||
private isClosed = false;
|
||||
|
||||
constructor(config: WSClientConfig) {
|
||||
this.wsURL = config.wsURL;
|
||||
this.url = config.wsURL;
|
||||
this.timeout = config.timeout ?? 30000;
|
||||
this.authToken = config.authToken;
|
||||
this.WebSocketClass = config.WebSocket ?? WebSocket;
|
||||
this.onNetworkError = config.onNetworkError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the network error callback
|
||||
*/
|
||||
setOnNetworkError(callback: NetworkErrorCallback | undefined): void {
|
||||
this.onNetworkError = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current WebSocket URL
|
||||
*/
|
||||
get url(): string {
|
||||
return this.wsURL;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,24 +49,14 @@ export class WSClient {
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
this.ws?.close();
|
||||
const error = new SDKError("WebSocket connection timeout", 408, "WS_TIMEOUT");
|
||||
|
||||
// Call the network error callback if configured
|
||||
if (this.onNetworkError) {
|
||||
this.onNetworkError(error, {
|
||||
method: "WS",
|
||||
path: this.wsURL,
|
||||
isRetry: false,
|
||||
attempt: 0,
|
||||
});
|
||||
}
|
||||
|
||||
reject(error);
|
||||
reject(
|
||||
new SDKError("WebSocket connection timeout", 408, "WS_TIMEOUT")
|
||||
);
|
||||
}, this.timeout);
|
||||
|
||||
this.ws.addEventListener("open", () => {
|
||||
clearTimeout(timeout);
|
||||
console.log("[WSClient] Connected to", this.wsURL);
|
||||
console.log("[WSClient] Connected to", this.url);
|
||||
this.openHandlers.forEach((handler) => handler());
|
||||
resolve();
|
||||
});
|
||||
@ -103,19 +70,7 @@ export class WSClient {
|
||||
console.error("[WSClient] WebSocket error:", event);
|
||||
clearTimeout(timeout);
|
||||
const error = new SDKError("WebSocket error", 500, "WS_ERROR", event);
|
||||
|
||||
// Call the network error callback if configured
|
||||
if (this.onNetworkError) {
|
||||
this.onNetworkError(error, {
|
||||
method: "WS",
|
||||
path: this.wsURL,
|
||||
isRetry: false,
|
||||
attempt: 0,
|
||||
});
|
||||
}
|
||||
|
||||
this.errorHandlers.forEach((handler) => handler(error));
|
||||
reject(error);
|
||||
});
|
||||
|
||||
this.ws.addEventListener("close", () => {
|
||||
@ -133,7 +88,7 @@ export class WSClient {
|
||||
* Build WebSocket URL with auth token
|
||||
*/
|
||||
private buildWSUrl(): string {
|
||||
let url = this.wsURL;
|
||||
let url = this.url;
|
||||
|
||||
if (this.authToken) {
|
||||
const separator = url.includes("?") ? "&" : "?";
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
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";
|
||||
@ -1,2 +0,0 @@
|
||||
export { FunctionsClient, type FunctionsClientConfig } from "./client";
|
||||
export type { FunctionResponse, SuccessResponse } from "./types";
|
||||
19
src/index.ts
19
src/index.ts
@ -1,4 +1,4 @@
|
||||
import { HttpClient, HttpClientConfig, NetworkErrorCallback } from "./core/http";
|
||||
import { HttpClient, HttpClientConfig } from "./core/http";
|
||||
import { AuthClient } from "./auth/client";
|
||||
import { DBClient } from "./db/client";
|
||||
import { PubSubClient } from "./pubsub/client";
|
||||
@ -17,14 +17,9 @@ export interface ClientConfig extends Omit<HttpClientConfig, "fetch"> {
|
||||
apiKey?: string;
|
||||
jwt?: string;
|
||||
storage?: StorageAdapter;
|
||||
wsConfig?: Partial<Omit<WSClientConfig, "wsURL">>;
|
||||
wsConfig?: Partial<WSClientConfig>;
|
||||
functionsConfig?: FunctionsClientConfig;
|
||||
fetch?: typeof fetch;
|
||||
/**
|
||||
* Callback invoked on network errors (HTTP and WebSocket).
|
||||
* Use this to trigger gateway failover at the application layer.
|
||||
*/
|
||||
onNetworkError?: NetworkErrorCallback;
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
@ -43,9 +38,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,
|
||||
});
|
||||
|
||||
const auth = new AuthClient({
|
||||
@ -55,14 +48,15 @@ export function createClient(config: ClientConfig): Client {
|
||||
jwt: config.jwt,
|
||||
});
|
||||
|
||||
// Derive WebSocket URL from baseURL
|
||||
const wsURL = config.baseURL.replace(/^http/, "ws").replace(/\/$/, "");
|
||||
// Derive WebSocket URL from baseURL if not explicitly provided
|
||||
const wsURL =
|
||||
config.wsConfig?.wsURL ??
|
||||
config.baseURL.replace(/^http/, "ws").replace(/\/$/, "");
|
||||
|
||||
const db = new DBClient(httpClient);
|
||||
const pubsub = new PubSubClient(httpClient, {
|
||||
...config.wsConfig,
|
||||
wsURL,
|
||||
onNetworkError: config.onNetworkError,
|
||||
});
|
||||
const network = new NetworkClient(httpClient);
|
||||
const cache = new CacheClient(httpClient);
|
||||
@ -81,7 +75,6 @@ export function createClient(config: ClientConfig): Client {
|
||||
}
|
||||
|
||||
export { HttpClient } from "./core/http";
|
||||
export type { NetworkErrorCallback, NetworkErrorContext } from "./core/http";
|
||||
export { WSClient } from "./core/ws";
|
||||
export { AuthClient } from "./auth/client";
|
||||
export { DBClient } from "./db/client";
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
export { NetworkClient } from "./client";
|
||||
export type {
|
||||
PeerInfo,
|
||||
NetworkStatus,
|
||||
ProxyRequest,
|
||||
ProxyResponse,
|
||||
} from "./client";
|
||||
@ -55,7 +55,7 @@ function base64Decode(b64: string): string {
|
||||
|
||||
/**
|
||||
* Simple PubSub client - one WebSocket connection per topic
|
||||
* Gateway failover is handled at the application layer
|
||||
* No connection pooling, no reference counting - keep it simple
|
||||
*/
|
||||
export class PubSubClient {
|
||||
private httpClient: HttpClient;
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
export { PubSubClient, Subscription } from "./client";
|
||||
export type {
|
||||
PubSubMessage,
|
||||
RawEnvelope,
|
||||
MessageHandler,
|
||||
ErrorHandler,
|
||||
CloseHandler,
|
||||
PresenceMember,
|
||||
PresenceResponse,
|
||||
PresenceOptions,
|
||||
SubscribeOptions,
|
||||
} from "./types";
|
||||
@ -1,7 +0,0 @@
|
||||
export { StorageClient } from "./client";
|
||||
export type {
|
||||
StorageUploadResponse,
|
||||
StoragePinRequest,
|
||||
StoragePinResponse,
|
||||
StorageStatus,
|
||||
} from "./client";
|
||||
@ -1,68 +0,0 @@
|
||||
/**
|
||||
* 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";
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export { Base64Codec } from "./codec";
|
||||
export { retryWithBackoff, type RetryConfig } from "./retry";
|
||||
export { Platform } from "./platform";
|
||||
@ -1,44 +0,0 @@
|
||||
/**
|
||||
* 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";
|
||||
},
|
||||
};
|
||||
@ -1,58 +0,0 @@
|
||||
/**
|
||||
* 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<T>(
|
||||
operation: () => Promise<T>,
|
||||
config: RetryConfig
|
||||
): Promise<T> {
|
||||
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");
|
||||
}
|
||||
@ -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", {
|
||||
const testFile = new File([testContent], "status-test.txt", {
|
||||
type: "text/plain",
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user