a lot of fixing

This commit is contained in:
anonpenguin23 2026-01-20 10:41:28 +02:00
parent 26c0169aaf
commit 15b3bb382e
32 changed files with 1414 additions and 7 deletions

View File

@ -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

View File

@ -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<WSClientConfig>; // WebSocket configuration
fetch?: typeof fetch; // Custom fetch implementation

100
examples/basic-usage.ts Normal file
View File

@ -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);

170
examples/database-crud.ts Normal file
View File

@ -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<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);

140
examples/pubsub-chat.ts Normal file
View File

@ -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);

View File

@ -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"
},

206
pnpm-lock.yaml generated
View File

@ -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:

3
src/auth/index.ts Normal file
View File

@ -0,0 +1,3 @@
export { AuthClient } from "./client";
export type { AuthConfig, WhoAmI, StorageAdapter } from "./types";
export { MemoryStorage, LocalStorageAdapter } from "./types";

14
src/cache/index.ts vendored Normal file
View File

@ -0,0 +1,14 @@
export { CacheClient } from "./client";
export type {
CacheGetRequest,
CacheGetResponse,
CachePutRequest,
CachePutResponse,
CacheDeleteRequest,
CacheDeleteResponse,
CacheMultiGetRequest,
CacheMultiGetResponse,
CacheScanRequest,
CacheScanResponse,
CacheHealthResponse,
} from "./client";

View File

@ -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}`);
}
}

10
src/core/index.ts Normal file
View File

@ -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";

View File

@ -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<string, string>;
/**
* Set API key
*/
setApiKey(apiKey?: string): void;
/**
* Set JWT token
*/
setJwt(jwt?: string): void;
}

View File

@ -0,0 +1,73 @@
/**
* 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;
}

View File

@ -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;
}

View File

@ -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<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;
}

View File

@ -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";

View File

@ -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<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";
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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!"
);
}
}
}
}

View File

@ -0,0 +1,4 @@
export { PathBasedAuthStrategy } from "./AuthHeaderStrategy";
export { ExponentialBackoffRetryPolicy } from "./RequestRetryPolicy";
export { RequestLogger } from "./RequestLogger";
export { TLSConfiguration } from "./TLSConfiguration";

13
src/db/index.ts Normal file
View File

@ -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";

2
src/functions/index.ts Normal file
View File

@ -0,0 +1,2 @@
export { FunctionsClient, type FunctionsClientConfig } from "./client";
export type { FunctionResponse, SuccessResponse } from "./types";

View File

@ -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,
});

7
src/network/index.ts Normal file
View File

@ -0,0 +1,7 @@
export { NetworkClient } from "./client";
export type {
PeerInfo,
NetworkStatus,
ProxyRequest,
ProxyResponse,
} from "./client";

12
src/pubsub/index.ts Normal file
View File

@ -0,0 +1,12 @@
export { PubSubClient, Subscription } from "./client";
export type {
PubSubMessage,
RawEnvelope,
MessageHandler,
ErrorHandler,
CloseHandler,
PresenceMember,
PresenceResponse,
PresenceOptions,
SubscribeOptions,
} from "./types";

7
src/storage/index.ts Normal file
View File

@ -0,0 +1,7 @@
export { StorageClient } from "./client";
export type {
StorageUploadResponse,
StoragePinRequest,
StoragePinResponse,
StorageStatus,
} from "./client";

68
src/utils/codec.ts Normal file
View File

@ -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";
}
}

3
src/utils/index.ts Normal file
View File

@ -0,0 +1,3 @@
export { Base64Codec } from "./codec";
export { retryWithBackoff, type RetryConfig } from "./retry";
export { Platform } from "./platform";

44
src/utils/platform.ts Normal file
View File

@ -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";
},
};

58
src/utils/retry.ts Normal file
View File

@ -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<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");
}

View File

@ -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",
});