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.1" have entirely different histories.
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 DeBrosOfficial
|
Copyright (c) [year] [fullname]
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
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)
|
timeout?: number; // Request timeout in ms (default: 30000)
|
||||||
maxRetries?: number; // Max retry attempts (default: 3)
|
maxRetries?: number; // Max retry attempts (default: 3)
|
||||||
retryDelayMs?: number; // Delay between retries (default: 1000)
|
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)
|
storage?: StorageAdapter; // For persisting JWT/API key (default: MemoryStorage)
|
||||||
wsConfig?: Partial<WSClientConfig>; // WebSocket configuration
|
wsConfig?: Partial<WSClientConfig>; // WebSocket configuration
|
||||||
fetch?: typeof fetch; // Custom fetch implementation
|
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",
|
"name": "@debros/network-ts-sdk",
|
||||||
"version": "0.6.0",
|
"version": "0.4.1",
|
||||||
"description": "TypeScript SDK for DeBros Network Gateway - Database, PubSub, Cache, Storage, and more",
|
"description": "TypeScript SDK for DeBros Network Gateway",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "DeBrosOfficial",
|
"author": "DeBrosOfficial",
|
||||||
"keywords": [
|
|
||||||
"debros",
|
|
||||||
"network",
|
|
||||||
"sdk",
|
|
||||||
"typescript",
|
|
||||||
"database",
|
|
||||||
"rqlite",
|
|
||||||
"pubsub",
|
|
||||||
"websocket",
|
|
||||||
"cache",
|
|
||||||
"olric",
|
|
||||||
"ipfs",
|
|
||||||
"storage",
|
|
||||||
"wasm",
|
|
||||||
"serverless",
|
|
||||||
"distributed",
|
|
||||||
"gateway"
|
|
||||||
],
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/DeBrosOfficial/network-ts-sdk"
|
"url": "https://github.com/DeBrosOfficial/network-ts-sdk/tree/v0.0.1"
|
||||||
},
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/DeBrosOfficial/network-ts-sdk/issues"
|
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
@ -59,11 +38,9 @@
|
|||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"@vitest/coverage-v8": "^1.0.0",
|
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"eslint": "^8.0.0",
|
"eslint": "^8.0.0",
|
||||||
"tsup": "^8.0.0",
|
"tsup": "^8.0.0",
|
||||||
"typedoc": "^0.25.0",
|
|
||||||
"typescript": "^5.3.0",
|
"typescript": "^5.3.0",
|
||||||
"vitest": "^1.0.0"
|
"vitest": "^1.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
206
pnpm-lock.yaml
generated
206
pnpm-lock.yaml
generated
@ -21,9 +21,6 @@ importers:
|
|||||||
'@typescript-eslint/parser':
|
'@typescript-eslint/parser':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.21.0(eslint@8.57.1)(typescript@5.9.3)
|
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:
|
dotenv:
|
||||||
specifier: ^17.2.3
|
specifier: ^17.2.3
|
||||||
version: 17.2.3
|
version: 17.2.3
|
||||||
@ -33,9 +30,6 @@ importers:
|
|||||||
tsup:
|
tsup:
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.5.0(postcss@8.5.6)(typescript@5.9.3)
|
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:
|
typescript:
|
||||||
specifier: ^5.3.0
|
specifier: ^5.3.0
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@ -45,30 +39,6 @@ importers:
|
|||||||
|
|
||||||
packages:
|
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':
|
'@esbuild/aix-ppc64@0.21.5':
|
||||||
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
|
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -398,10 +368,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
'@istanbuljs/schema@0.1.3':
|
|
||||||
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
'@jest/schemas@29.6.3':
|
'@jest/schemas@29.6.3':
|
||||||
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
|
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
|
||||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||||
@ -621,11 +587,6 @@ packages:
|
|||||||
'@ungap/structured-clone@1.3.0':
|
'@ungap/structured-clone@1.3.0':
|
||||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
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':
|
'@vitest/expect@1.6.1':
|
||||||
resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==}
|
resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==}
|
||||||
|
|
||||||
@ -666,9 +627,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
|
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
ansi-sequence-parser@1.1.3:
|
|
||||||
resolution: {integrity: sha512-+fksAx9eG3Ab6LDnLs3ZqZa8KVJ/jYnX+D4Qe1azX+LFGFAXqynCQLOdLpNYN/l9e7l6hMWwZbrnctqr6eSQSw==}
|
|
||||||
|
|
||||||
ansi-styles@4.3.0:
|
ansi-styles@4.3.0:
|
||||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -954,9 +912,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
html-escaper@2.0.2:
|
|
||||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
|
||||||
|
|
||||||
human-signals@5.0.0:
|
human-signals@5.0.0:
|
||||||
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
|
||||||
engines: {node: '>=16.17.0'}
|
engines: {node: '>=16.17.0'}
|
||||||
@ -1012,22 +967,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
ws: '*'
|
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:
|
jackspeak@3.4.3:
|
||||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||||
|
|
||||||
@ -1051,9 +990,6 @@ packages:
|
|||||||
json-stable-stringify-without-jsonify@1.0.1:
|
json-stable-stringify-without-jsonify@1.0.1:
|
||||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||||
|
|
||||||
jsonc-parser@3.3.1:
|
|
||||||
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
|
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
|
|
||||||
@ -1092,24 +1028,9 @@ packages:
|
|||||||
lru-cache@10.4.3:
|
lru-cache@10.4.3:
|
||||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||||
|
|
||||||
lunr@2.3.9:
|
|
||||||
resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
|
|
||||||
|
|
||||||
magic-string@0.30.19:
|
magic-string@0.30.19:
|
||||||
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
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:
|
merge-stream@2.0.0:
|
||||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||||
|
|
||||||
@ -1328,9 +1249,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
shiki@0.14.7:
|
|
||||||
resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==}
|
|
||||||
|
|
||||||
siginfo@2.0.0:
|
siginfo@2.0.0:
|
||||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||||
|
|
||||||
@ -1393,10 +1311,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
engines: {node: '>=8'}
|
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:
|
text-table@0.2.0:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
|
|
||||||
@ -1476,13 +1390,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
|
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
|
||||||
engines: {node: '>=10'}
|
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:
|
typescript@5.9.3:
|
||||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
@ -1558,12 +1465,6 @@ packages:
|
|||||||
jsdom:
|
jsdom:
|
||||||
optional: true
|
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:
|
webidl-conversions@4.0.2:
|
||||||
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
||||||
|
|
||||||
@ -1617,26 +1518,6 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
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':
|
'@esbuild/aix-ppc64@0.21.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -1828,8 +1709,6 @@ snapshots:
|
|||||||
wrap-ansi: 8.1.0
|
wrap-ansi: 8.1.0
|
||||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||||
|
|
||||||
'@istanbuljs/schema@0.1.3': {}
|
|
||||||
|
|
||||||
'@jest/schemas@29.6.3':
|
'@jest/schemas@29.6.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sinclair/typebox': 0.27.8
|
'@sinclair/typebox': 0.27.8
|
||||||
@ -2029,25 +1908,6 @@ snapshots:
|
|||||||
|
|
||||||
'@ungap/structured-clone@1.3.0': {}
|
'@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':
|
'@vitest/expect@1.6.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 1.6.1
|
'@vitest/spy': 1.6.1
|
||||||
@ -2098,8 +1958,6 @@ snapshots:
|
|||||||
|
|
||||||
ansi-regex@6.2.2: {}
|
ansi-regex@6.2.2: {}
|
||||||
|
|
||||||
ansi-sequence-parser@1.1.3: {}
|
|
||||||
|
|
||||||
ansi-styles@4.3.0:
|
ansi-styles@4.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-convert: 2.0.1
|
color-convert: 2.0.1
|
||||||
@ -2458,8 +2316,6 @@ snapshots:
|
|||||||
|
|
||||||
has-flag@4.0.0: {}
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
html-escaper@2.0.2: {}
|
|
||||||
|
|
||||||
human-signals@5.0.0: {}
|
human-signals@5.0.0: {}
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
@ -2498,27 +2354,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ws: 8.18.3
|
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:
|
jackspeak@3.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/cliui': 8.0.2
|
'@isaacs/cliui': 8.0.2
|
||||||
@ -2539,8 +2374,6 @@ snapshots:
|
|||||||
|
|
||||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||||
|
|
||||||
jsonc-parser@3.3.1: {}
|
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
json-buffer: 3.0.1
|
json-buffer: 3.0.1
|
||||||
@ -2575,24 +2408,10 @@ snapshots:
|
|||||||
|
|
||||||
lru-cache@10.4.3: {}
|
lru-cache@10.4.3: {}
|
||||||
|
|
||||||
lunr@2.3.9: {}
|
|
||||||
|
|
||||||
magic-string@0.30.19:
|
magic-string@0.30.19:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@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: {}
|
merge-stream@2.0.0: {}
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
@ -2791,13 +2610,6 @@ snapshots:
|
|||||||
|
|
||||||
shebang-regex@3.0.0: {}
|
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: {}
|
siginfo@2.0.0: {}
|
||||||
|
|
||||||
signal-exit@4.1.0: {}
|
signal-exit@4.1.0: {}
|
||||||
@ -2856,12 +2668,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
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: {}
|
text-table@0.2.0: {}
|
||||||
|
|
||||||
thenify-all@1.6.0:
|
thenify-all@1.6.0:
|
||||||
@ -2937,14 +2743,6 @@ snapshots:
|
|||||||
|
|
||||||
type-fest@0.20.2: {}
|
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: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
ufo@1.6.1: {}
|
ufo@1.6.1: {}
|
||||||
@ -3016,10 +2814,6 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- terser
|
||||||
|
|
||||||
vscode-oniguruma@1.7.0: {}
|
|
||||||
|
|
||||||
vscode-textmate@8.0.0: {}
|
|
||||||
|
|
||||||
webidl-conversions@4.0.2: {}
|
webidl-conversions@4.0.2: {}
|
||||||
|
|
||||||
whatwg-url@7.1.0:
|
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";
|
|
||||||
262
src/core/http.ts
262
src/core/http.ts
@ -1,39 +1,12 @@
|
|||||||
import { SDKError } from "../errors";
|
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 {
|
export interface HttpClientConfig {
|
||||||
baseURL: string;
|
baseURL: string | string[];
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
maxRetries?: number;
|
maxRetries?: number;
|
||||||
retryDelayMs?: number;
|
retryDelayMs?: number;
|
||||||
fetch?: typeof fetch;
|
fetch?: typeof fetch;
|
||||||
/**
|
gatewayHealthCheckCooldownMs?: number;
|
||||||
* 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -59,33 +32,38 @@ function createFetchWithTLSConfig(): typeof fetch {
|
|||||||
return globalThis.fetch;
|
return globalThis.fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GatewayHealth {
|
||||||
|
url: string;
|
||||||
|
unhealthyUntil: number | null; // Timestamp when gateway becomes healthy again
|
||||||
|
}
|
||||||
|
|
||||||
export class HttpClient {
|
export class HttpClient {
|
||||||
private baseURL: string;
|
private baseURLs: string[];
|
||||||
|
private currentURLIndex: number = 0;
|
||||||
private timeout: number;
|
private timeout: number;
|
||||||
private maxRetries: number;
|
private maxRetries: number;
|
||||||
private retryDelayMs: number;
|
private retryDelayMs: number;
|
||||||
private fetch: typeof fetch;
|
private fetch: typeof fetch;
|
||||||
private apiKey?: string;
|
private apiKey?: string;
|
||||||
private jwt?: string;
|
private jwt?: string;
|
||||||
private debug: boolean;
|
private gatewayHealthCheckCooldownMs: number;
|
||||||
private onNetworkError?: NetworkErrorCallback;
|
private gatewayHealth: Map<string, GatewayHealth>;
|
||||||
|
|
||||||
constructor(config: HttpClientConfig) {
|
constructor(config: HttpClientConfig) {
|
||||||
this.baseURL = config.baseURL.replace(/\/$/, "");
|
this.baseURLs = (Array.isArray(config.baseURL) ? config.baseURL : [config.baseURL])
|
||||||
|
.map(url => url.replace(/\/$/, ""));
|
||||||
this.timeout = config.timeout ?? 60000;
|
this.timeout = config.timeout ?? 60000;
|
||||||
this.maxRetries = config.maxRetries ?? 3;
|
this.maxRetries = config.maxRetries ?? 3;
|
||||||
this.retryDelayMs = config.retryDelayMs ?? 1000;
|
this.retryDelayMs = config.retryDelayMs ?? 1000;
|
||||||
|
this.gatewayHealthCheckCooldownMs = config.gatewayHealthCheckCooldownMs ?? 600000; // Default 10 minutes
|
||||||
// Use provided fetch or create one with proper TLS configuration for staging certificates
|
// Use provided fetch or create one with proper TLS configuration for staging certificates
|
||||||
this.fetch = config.fetch ?? createFetchWithTLSConfig();
|
this.fetch = config.fetch ?? createFetchWithTLSConfig();
|
||||||
this.debug = config.debug ?? false;
|
|
||||||
this.onNetworkError = config.onNetworkError;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Initialize gateway health tracking
|
||||||
* Set the network error callback
|
this.gatewayHealth = new Map();
|
||||||
*/
|
this.baseURLs.forEach(url => {
|
||||||
setOnNetworkError(callback: NetworkErrorCallback | undefined): void {
|
this.gatewayHealth.set(url, { url, unhealthyUntil: null });
|
||||||
this.onNetworkError = callback;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setApiKey(apiKey?: string) {
|
setApiKey(apiKey?: string) {
|
||||||
@ -161,10 +139,89 @@ export class HttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the base URL
|
* Get the current base URL (for single gateway or current gateway in multi-gateway setup)
|
||||||
*/
|
*/
|
||||||
getBaseURL(): string {
|
private getCurrentBaseURL(): string {
|
||||||
return this.baseURL;
|
return this.baseURLs[this.currentURLIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all base URLs (for WebSocket or other purposes that need all gateways)
|
||||||
|
*/
|
||||||
|
getBaseURLs(): string[] {
|
||||||
|
return [...this.baseURLs];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a gateway is healthy (not in cooldown period)
|
||||||
|
*/
|
||||||
|
private isGatewayHealthy(url: string): boolean {
|
||||||
|
const health = this.gatewayHealth.get(url);
|
||||||
|
if (!health || health.unhealthyUntil === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
if (now >= health.unhealthyUntil) {
|
||||||
|
// Cooldown period expired, mark as healthy again
|
||||||
|
health.unhealthyUntil = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a gateway as unhealthy for the cooldown period
|
||||||
|
*/
|
||||||
|
private markGatewayUnhealthy(url: string): void {
|
||||||
|
const health = this.gatewayHealth.get(url);
|
||||||
|
if (health) {
|
||||||
|
health.unhealthyUntil = Date.now() + this.gatewayHealthCheckCooldownMs;
|
||||||
|
if (typeof console !== "undefined") {
|
||||||
|
console.warn(
|
||||||
|
`[HttpClient] Gateway marked unhealthy for ${this.gatewayHealthCheckCooldownMs / 1000}s:`,
|
||||||
|
url
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try the next healthy gateway in the list
|
||||||
|
* Returns the index of the next healthy gateway, or -1 if none available
|
||||||
|
*/
|
||||||
|
private findNextHealthyGateway(): number {
|
||||||
|
if (this.baseURLs.length <= 1) {
|
||||||
|
return -1; // No other gateways to try
|
||||||
|
}
|
||||||
|
|
||||||
|
const startIndex = this.currentURLIndex;
|
||||||
|
let attempts = 0;
|
||||||
|
|
||||||
|
// Try each gateway once (excluding current)
|
||||||
|
while (attempts < this.baseURLs.length - 1) {
|
||||||
|
const nextIndex = (startIndex + attempts + 1) % this.baseURLs.length;
|
||||||
|
const nextUrl = this.baseURLs[nextIndex];
|
||||||
|
|
||||||
|
if (this.isGatewayHealthy(nextUrl)) {
|
||||||
|
return nextIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1; // No healthy gateways found
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move to the next healthy gateway
|
||||||
|
*/
|
||||||
|
private moveToNextGateway(): boolean {
|
||||||
|
const nextIndex = this.findNextHealthyGateway();
|
||||||
|
if (nextIndex === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.currentURLIndex = nextIndex;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async request<T = any>(
|
async request<T = any>(
|
||||||
@ -178,7 +235,7 @@ export class HttpClient {
|
|||||||
} = {}
|
} = {}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const startTime = performance.now(); // Track request start time
|
const startTime = performance.now(); // Track request start time
|
||||||
const url = new URL(this.baseURL + path);
|
const url = new URL(this.getCurrentBaseURL() + path);
|
||||||
if (options.query) {
|
if (options.query) {
|
||||||
Object.entries(options.query).forEach(([key, value]) => {
|
Object.entries(options.query).forEach(([key, value]) => {
|
||||||
url.searchParams.append(key, String(value));
|
url.searchParams.append(key, String(value));
|
||||||
@ -262,7 +319,7 @@ export class HttpClient {
|
|||||||
const logMessage = `[HttpClient] ${method} ${path} completed in ${duration.toFixed(
|
const logMessage = `[HttpClient] ${method} ${path} completed in ${duration.toFixed(
|
||||||
2
|
2
|
||||||
)}ms`;
|
)}ms`;
|
||||||
if (queryDetails && this.debug) {
|
if (queryDetails) {
|
||||||
console.log(logMessage);
|
console.log(logMessage);
|
||||||
console.log(`[HttpClient] ${queryDetails}`);
|
console.log(`[HttpClient] ${queryDetails}`);
|
||||||
} else {
|
} else {
|
||||||
@ -292,32 +349,11 @@ export class HttpClient {
|
|||||||
2
|
2
|
||||||
)}ms:`;
|
)}ms:`;
|
||||||
console.error(errorMessage, error);
|
console.error(errorMessage, error);
|
||||||
if (queryDetails && this.debug) {
|
if (queryDetails) {
|
||||||
console.error(`[HttpClient] ${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;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
@ -328,8 +364,11 @@ export class HttpClient {
|
|||||||
url: string,
|
url: string,
|
||||||
options: RequestInit,
|
options: RequestInit,
|
||||||
attempt: number = 0,
|
attempt: number = 0,
|
||||||
startTime?: number // Track start time for timing across retries
|
startTime?: number, // Track start time for timing across retries
|
||||||
|
gatewayAttempt: number = 0 // Track gateway failover attempts
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
const currentGatewayUrl = this.getCurrentBaseURL();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.fetch(url, options);
|
const response = await this.fetch(url, options);
|
||||||
|
|
||||||
@ -354,20 +393,50 @@ export class HttpClient {
|
|||||||
error instanceof SDKError &&
|
error instanceof SDKError &&
|
||||||
[408, 429, 500, 502, 503, 504].includes(error.httpStatus);
|
[408, 429, 500, 502, 503, 504].includes(error.httpStatus);
|
||||||
|
|
||||||
// Retry on same gateway for retryable HTTP errors
|
const isNetworkError =
|
||||||
|
error instanceof TypeError ||
|
||||||
|
(error instanceof Error && error.message.includes('fetch'));
|
||||||
|
|
||||||
|
// Retry on the same gateway first (for retryable HTTP errors)
|
||||||
if (isRetryableError && attempt < this.maxRetries) {
|
if (isRetryableError && attempt < this.maxRetries) {
|
||||||
if (typeof console !== "undefined") {
|
if (typeof console !== "undefined") {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[HttpClient] Retrying request (attempt ${attempt + 1}/${this.maxRetries})`
|
`[HttpClient] Retrying request on same gateway (attempt ${attempt + 1}/${this.maxRetries}):`,
|
||||||
|
currentGatewayUrl
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await new Promise((resolve) =>
|
await new Promise((resolve) =>
|
||||||
setTimeout(resolve, this.retryDelayMs * (attempt + 1))
|
setTimeout(resolve, this.retryDelayMs * (attempt + 1))
|
||||||
);
|
);
|
||||||
return this.requestWithRetry(url, options, attempt + 1, startTime);
|
return this.requestWithRetry(url, options, attempt + 1, startTime, gatewayAttempt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all retries on current gateway failed, mark it unhealthy and try next gateway
|
||||||
|
if ((isNetworkError || isRetryableError) && gatewayAttempt < this.baseURLs.length - 1) {
|
||||||
|
// Mark current gateway as unhealthy
|
||||||
|
this.markGatewayUnhealthy(currentGatewayUrl);
|
||||||
|
|
||||||
|
// Try to move to next healthy gateway
|
||||||
|
if (this.moveToNextGateway()) {
|
||||||
|
if (typeof console !== "undefined") {
|
||||||
|
console.warn(
|
||||||
|
`[HttpClient] Gateway exhausted retries, trying next gateway (${gatewayAttempt + 1}/${this.baseURLs.length - 1}):`,
|
||||||
|
this.getCurrentBaseURL()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update URL to use the new gateway
|
||||||
|
const currentPath = url.substring(url.indexOf('/', 8)); // Get path after protocol://host
|
||||||
|
const newUrl = this.getCurrentBaseURL() + currentPath;
|
||||||
|
|
||||||
|
// Small delay before trying next gateway
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, this.retryDelayMs));
|
||||||
|
|
||||||
|
// Reset attempt counter for new gateway
|
||||||
|
return this.requestWithRetry(newUrl, options, 0, startTime, gatewayAttempt + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// All retries exhausted - throw error for app to handle
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -414,7 +483,7 @@ export class HttpClient {
|
|||||||
}
|
}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const startTime = performance.now(); // Track upload start time
|
const startTime = performance.now(); // Track upload start time
|
||||||
const url = new URL(this.baseURL + path);
|
const url = new URL(this.getCurrentBaseURL() + path);
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
...this.getAuthHeaders(path),
|
...this.getAuthHeaders(path),
|
||||||
// Don't set Content-Type - browser will set it with boundary
|
// Don't set Content-Type - browser will set it with boundary
|
||||||
@ -457,25 +526,6 @@ export class HttpClient {
|
|||||||
error
|
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;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
@ -486,7 +536,7 @@ export class HttpClient {
|
|||||||
* Get a binary response (returns Response object for streaming)
|
* Get a binary response (returns Response object for streaming)
|
||||||
*/
|
*/
|
||||||
async getBinary(path: string): Promise<Response> {
|
async getBinary(path: string): Promise<Response> {
|
||||||
const url = new URL(this.baseURL + path);
|
const url = new URL(this.getCurrentBaseURL() + path);
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
...this.getAuthHeaders(path),
|
...this.getAuthHeaders(path),
|
||||||
};
|
};
|
||||||
@ -504,33 +554,17 @@ export class HttpClient {
|
|||||||
const response = await this.fetch(url.toString(), fetchOptions);
|
const response = await this.fetch(url.toString(), fetchOptions);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
const errorBody = await response.json().catch(() => ({
|
const error = await response.json().catch(() => ({
|
||||||
error: response.statusText,
|
error: response.statusText,
|
||||||
}));
|
}));
|
||||||
throw SDKError.fromResponse(response.status, errorBody);
|
throw SDKError.fromResponse(response.status, error);
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
if (error instanceof SDKError) {
|
||||||
// Call the network error callback if configured
|
throw error;
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 WebSocket from "isomorphic-ws";
|
||||||
import { SDKError } from "../errors";
|
import { SDKError } from "../errors";
|
||||||
import { NetworkErrorCallback } from "./http";
|
|
||||||
|
|
||||||
export interface WSClientConfig {
|
export interface WSClientConfig {
|
||||||
wsURL: string;
|
wsURL: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
authToken?: string;
|
authToken?: string;
|
||||||
WebSocket?: typeof WebSocket;
|
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;
|
export type WSMessageHandler = (data: string) => void;
|
||||||
@ -21,15 +15,13 @@ export type WSOpenHandler = () => void;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple WebSocket client with minimal abstractions
|
* Simple WebSocket client with minimal abstractions
|
||||||
* No complex reconnection, no failover - keep it simple
|
* No complex reconnection, no heartbeats - keep it simple
|
||||||
* Gateway failover is handled at the application layer
|
|
||||||
*/
|
*/
|
||||||
export class WSClient {
|
export class WSClient {
|
||||||
private wsURL: string;
|
private url: string;
|
||||||
private timeout: number;
|
private timeout: number;
|
||||||
private authToken?: string;
|
private authToken?: string;
|
||||||
private WebSocketClass: typeof WebSocket;
|
private WebSocketClass: typeof WebSocket;
|
||||||
private onNetworkError?: NetworkErrorCallback;
|
|
||||||
|
|
||||||
private ws?: WebSocket;
|
private ws?: WebSocket;
|
||||||
private messageHandlers: Set<WSMessageHandler> = new Set();
|
private messageHandlers: Set<WSMessageHandler> = new Set();
|
||||||
@ -39,25 +31,10 @@ export class WSClient {
|
|||||||
private isClosed = false;
|
private isClosed = false;
|
||||||
|
|
||||||
constructor(config: WSClientConfig) {
|
constructor(config: WSClientConfig) {
|
||||||
this.wsURL = config.wsURL;
|
this.url = config.wsURL;
|
||||||
this.timeout = config.timeout ?? 30000;
|
this.timeout = config.timeout ?? 30000;
|
||||||
this.authToken = config.authToken;
|
this.authToken = config.authToken;
|
||||||
this.WebSocketClass = config.WebSocket ?? WebSocket;
|
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(() => {
|
const timeout = setTimeout(() => {
|
||||||
this.ws?.close();
|
this.ws?.close();
|
||||||
const error = new SDKError("WebSocket connection timeout", 408, "WS_TIMEOUT");
|
reject(
|
||||||
|
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);
|
|
||||||
}, this.timeout);
|
}, this.timeout);
|
||||||
|
|
||||||
this.ws.addEventListener("open", () => {
|
this.ws.addEventListener("open", () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
console.log("[WSClient] Connected to", this.wsURL);
|
console.log("[WSClient] Connected to", this.url);
|
||||||
this.openHandlers.forEach((handler) => handler());
|
this.openHandlers.forEach((handler) => handler());
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
@ -103,19 +70,7 @@ export class WSClient {
|
|||||||
console.error("[WSClient] WebSocket error:", event);
|
console.error("[WSClient] WebSocket error:", event);
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
const error = new SDKError("WebSocket error", 500, "WS_ERROR", event);
|
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));
|
this.errorHandlers.forEach((handler) => handler(error));
|
||||||
reject(error);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ws.addEventListener("close", () => {
|
this.ws.addEventListener("close", () => {
|
||||||
@ -133,7 +88,7 @@ export class WSClient {
|
|||||||
* Build WebSocket URL with auth token
|
* Build WebSocket URL with auth token
|
||||||
*/
|
*/
|
||||||
private buildWSUrl(): string {
|
private buildWSUrl(): string {
|
||||||
let url = this.wsURL;
|
let url = this.url;
|
||||||
|
|
||||||
if (this.authToken) {
|
if (this.authToken) {
|
||||||
const separator = url.includes("?") ? "&" : "?";
|
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";
|
|
||||||
21
src/index.ts
21
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 { AuthClient } from "./auth/client";
|
||||||
import { DBClient } from "./db/client";
|
import { DBClient } from "./db/client";
|
||||||
import { PubSubClient } from "./pubsub/client";
|
import { PubSubClient } from "./pubsub/client";
|
||||||
@ -17,14 +17,9 @@ export interface ClientConfig extends Omit<HttpClientConfig, "fetch"> {
|
|||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
jwt?: string;
|
jwt?: string;
|
||||||
storage?: StorageAdapter;
|
storage?: StorageAdapter;
|
||||||
wsConfig?: Partial<Omit<WSClientConfig, "wsURL">>;
|
wsConfig?: Partial<WSClientConfig>;
|
||||||
functionsConfig?: FunctionsClientConfig;
|
functionsConfig?: FunctionsClientConfig;
|
||||||
fetch?: typeof fetch;
|
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 {
|
export interface Client {
|
||||||
@ -43,9 +38,7 @@ export function createClient(config: ClientConfig): Client {
|
|||||||
timeout: config.timeout,
|
timeout: config.timeout,
|
||||||
maxRetries: config.maxRetries,
|
maxRetries: config.maxRetries,
|
||||||
retryDelayMs: config.retryDelayMs,
|
retryDelayMs: config.retryDelayMs,
|
||||||
debug: config.debug,
|
|
||||||
fetch: config.fetch,
|
fetch: config.fetch,
|
||||||
onNetworkError: config.onNetworkError,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const auth = new AuthClient({
|
const auth = new AuthClient({
|
||||||
@ -55,14 +48,17 @@ export function createClient(config: ClientConfig): Client {
|
|||||||
jwt: config.jwt,
|
jwt: config.jwt,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Derive WebSocket URL from baseURL
|
// Derive WebSocket URL from baseURL if not explicitly provided
|
||||||
const wsURL = config.baseURL.replace(/^http/, "ws").replace(/\/$/, "");
|
// If multiple base URLs are provided, use the first one for WebSocket (primary gateway)
|
||||||
|
const primaryBaseURL = Array.isArray(config.baseURL) ? config.baseURL[0] : config.baseURL;
|
||||||
|
const wsURL =
|
||||||
|
config.wsConfig?.wsURL ??
|
||||||
|
primaryBaseURL.replace(/^http/, "ws").replace(/\/$/, "");
|
||||||
|
|
||||||
const db = new DBClient(httpClient);
|
const db = new DBClient(httpClient);
|
||||||
const pubsub = new PubSubClient(httpClient, {
|
const pubsub = new PubSubClient(httpClient, {
|
||||||
...config.wsConfig,
|
...config.wsConfig,
|
||||||
wsURL,
|
wsURL,
|
||||||
onNetworkError: config.onNetworkError,
|
|
||||||
});
|
});
|
||||||
const network = new NetworkClient(httpClient);
|
const network = new NetworkClient(httpClient);
|
||||||
const cache = new CacheClient(httpClient);
|
const cache = new CacheClient(httpClient);
|
||||||
@ -81,7 +77,6 @@ export function createClient(config: ClientConfig): Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { HttpClient } from "./core/http";
|
export { HttpClient } from "./core/http";
|
||||||
export type { NetworkErrorCallback, NetworkErrorContext } from "./core/http";
|
|
||||||
export { WSClient } from "./core/ws";
|
export { WSClient } from "./core/ws";
|
||||||
export { AuthClient } from "./auth/client";
|
export { AuthClient } from "./auth/client";
|
||||||
export { DBClient } from "./db/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
|
* 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 {
|
export class PubSubClient {
|
||||||
private httpClient: HttpClient;
|
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();
|
const client = await createTestClient();
|
||||||
// First upload and pin a file
|
// First upload and pin a file
|
||||||
const testContent = "File for status check";
|
const testContent = "File for status check";
|
||||||
const testFile = new File([testContent], "status-test", {
|
const testFile = new File([testContent], "status-test.txt", {
|
||||||
type: "text/plain",
|
type: "text/plain",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user