mirror of
https://github.com/DeBrosOfficial/network-ts-sdk.git
synced 2026-01-30 21:13:03 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15b3bb382e | ||
|
|
26c0169aaf | ||
|
|
58097e3ff8 |
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) [year] [fullname]
|
Copyright (c) 2026 DeBrosOfficial
|
||||||
|
|
||||||
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,6 +367,7 @@ 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
|
||||||
|
|||||||
100
examples/basic-usage.ts
Normal file
100
examples/basic-usage.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* Basic Usage Example
|
||||||
|
*
|
||||||
|
* This example demonstrates the fundamental usage of the DeBros Network SDK.
|
||||||
|
* It covers client initialization, database operations, pub/sub, and caching.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createClient } from '../src/index';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// 1. Create client
|
||||||
|
const client = createClient({
|
||||||
|
baseURL: 'http://localhost:6001',
|
||||||
|
apiKey: 'ak_your_key:default',
|
||||||
|
debug: true, // Enable debug logging
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✓ Client created');
|
||||||
|
|
||||||
|
// 2. Database operations
|
||||||
|
console.log('\n--- Database Operations ---');
|
||||||
|
|
||||||
|
// Create table
|
||||||
|
await client.db.createTable(
|
||||||
|
`CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||||
|
)`
|
||||||
|
);
|
||||||
|
console.log('✓ Table created');
|
||||||
|
|
||||||
|
// Insert data
|
||||||
|
const result = await client.db.exec(
|
||||||
|
'INSERT INTO users (name, email) VALUES (?, ?)',
|
||||||
|
['Alice Johnson', 'alice@example.com']
|
||||||
|
);
|
||||||
|
console.log(`✓ Inserted user with ID: ${result.last_insert_id}`);
|
||||||
|
|
||||||
|
// Query data
|
||||||
|
const users = await client.db.query(
|
||||||
|
'SELECT * FROM users WHERE email = ?',
|
||||||
|
['alice@example.com']
|
||||||
|
);
|
||||||
|
console.log('✓ Found users:', users);
|
||||||
|
|
||||||
|
// 3. Pub/Sub messaging
|
||||||
|
console.log('\n--- Pub/Sub Messaging ---');
|
||||||
|
|
||||||
|
const subscription = await client.pubsub.subscribe('demo-topic', {
|
||||||
|
onMessage: (msg) => {
|
||||||
|
console.log(`✓ Received message: "${msg.data}" at ${new Date(msg.timestamp).toISOString()}`);
|
||||||
|
},
|
||||||
|
onError: (err) => console.error('Subscription error:', err),
|
||||||
|
});
|
||||||
|
console.log('✓ Subscribed to demo-topic');
|
||||||
|
|
||||||
|
// Publish a message
|
||||||
|
await client.pubsub.publish('demo-topic', 'Hello from the SDK!');
|
||||||
|
console.log('✓ Published message');
|
||||||
|
|
||||||
|
// Wait a bit for message delivery
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// Close subscription
|
||||||
|
subscription.close();
|
||||||
|
console.log('✓ Subscription closed');
|
||||||
|
|
||||||
|
// 4. Cache operations
|
||||||
|
console.log('\n--- Cache Operations ---');
|
||||||
|
|
||||||
|
// Put value with 1-hour TTL
|
||||||
|
await client.cache.put('default', 'user:alice', {
|
||||||
|
id: result.last_insert_id,
|
||||||
|
name: 'Alice Johnson',
|
||||||
|
email: 'alice@example.com',
|
||||||
|
}, '1h');
|
||||||
|
console.log('✓ Cached user data');
|
||||||
|
|
||||||
|
// Get value
|
||||||
|
const cached = await client.cache.get('default', 'user:alice');
|
||||||
|
if (cached) {
|
||||||
|
console.log('✓ Retrieved from cache:', cached.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Network health check
|
||||||
|
console.log('\n--- Network Operations ---');
|
||||||
|
|
||||||
|
const healthy = await client.network.health();
|
||||||
|
console.log(`✓ Gateway health: ${healthy ? 'OK' : 'FAIL'}`);
|
||||||
|
|
||||||
|
const status = await client.network.status();
|
||||||
|
console.log(`✓ Network status: ${status.peer_count} peers connected`);
|
||||||
|
|
||||||
|
console.log('\n--- Example completed successfully ---');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run example
|
||||||
|
main().catch(console.error);
|
||||||
170
examples/database-crud.ts
Normal file
170
examples/database-crud.ts
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
/**
|
||||||
|
* Database CRUD Operations Example
|
||||||
|
*
|
||||||
|
* Demonstrates comprehensive database operations including:
|
||||||
|
* - Table creation and schema management
|
||||||
|
* - Insert, Update, Delete operations
|
||||||
|
* - QueryBuilder fluent API
|
||||||
|
* - Repository pattern (ORM-style)
|
||||||
|
* - Transactions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createClient } from '../src/index';
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id?: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
age: number;
|
||||||
|
active?: number;
|
||||||
|
created_at?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const client = createClient({
|
||||||
|
baseURL: 'http://localhost:6001',
|
||||||
|
apiKey: 'ak_your_key:default',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Create table
|
||||||
|
console.log('Creating users table...');
|
||||||
|
await client.db.createTable(
|
||||||
|
`CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
age INTEGER,
|
||||||
|
active INTEGER DEFAULT 1,
|
||||||
|
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||||
|
)`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Raw SQL INSERT
|
||||||
|
console.log('\n--- Raw SQL Operations ---');
|
||||||
|
const insertResult = await client.db.exec(
|
||||||
|
'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
|
||||||
|
['Bob Smith', 'bob@example.com', 30]
|
||||||
|
);
|
||||||
|
console.log('Inserted ID:', insertResult.last_insert_id);
|
||||||
|
|
||||||
|
// 3. Raw SQL UPDATE
|
||||||
|
await client.db.exec(
|
||||||
|
'UPDATE users SET age = ? WHERE id = ?',
|
||||||
|
[31, insertResult.last_insert_id]
|
||||||
|
);
|
||||||
|
console.log('Updated user age');
|
||||||
|
|
||||||
|
// 4. Raw SQL SELECT
|
||||||
|
const users = await client.db.query<User>(
|
||||||
|
'SELECT * FROM users WHERE email = ?',
|
||||||
|
['bob@example.com']
|
||||||
|
);
|
||||||
|
console.log('Found users:', users);
|
||||||
|
|
||||||
|
// 5. QueryBuilder
|
||||||
|
console.log('\n--- QueryBuilder Operations ---');
|
||||||
|
|
||||||
|
// Insert multiple users for querying
|
||||||
|
await client.db.exec("INSERT INTO users (name, email, age) VALUES (?, ?, ?)", ["Charlie", "charlie@example.com", 25]);
|
||||||
|
await client.db.exec("INSERT INTO users (name, email, age) VALUES (?, ?, ?)", ["Diana", "diana@example.com", 35]);
|
||||||
|
await client.db.exec("INSERT INTO users (name, email, age) VALUES (?, ?, ?)", ["Eve", "eve@example.com", 28]);
|
||||||
|
|
||||||
|
// Complex query with QueryBuilder
|
||||||
|
const activeUsers = await client.db
|
||||||
|
.createQueryBuilder('users')
|
||||||
|
.select('id', 'name', 'email', 'age')
|
||||||
|
.where('active = ?', [1])
|
||||||
|
.andWhere('age > ?', [25])
|
||||||
|
.orderBy('age DESC')
|
||||||
|
.limit(10)
|
||||||
|
.getMany<User>();
|
||||||
|
|
||||||
|
console.log('Active users over 25:', activeUsers);
|
||||||
|
|
||||||
|
// Get single user
|
||||||
|
const singleUser = await client.db
|
||||||
|
.createQueryBuilder('users')
|
||||||
|
.where('email = ?', ['charlie@example.com'])
|
||||||
|
.getOne<User>();
|
||||||
|
|
||||||
|
console.log('Single user:', singleUser);
|
||||||
|
|
||||||
|
// Count users
|
||||||
|
const count = await client.db
|
||||||
|
.createQueryBuilder('users')
|
||||||
|
.where('age > ?', [25])
|
||||||
|
.count();
|
||||||
|
|
||||||
|
console.log('Users over 25:', count);
|
||||||
|
|
||||||
|
// 6. Repository Pattern (ORM)
|
||||||
|
console.log('\n--- Repository Pattern ---');
|
||||||
|
|
||||||
|
const userRepo = client.db.repository<User>('users');
|
||||||
|
|
||||||
|
// Find all
|
||||||
|
const allUsers = await userRepo.find({});
|
||||||
|
console.log('All users:', allUsers.length);
|
||||||
|
|
||||||
|
// Find with criteria
|
||||||
|
const youngUsers = await userRepo.find({ age: 25 });
|
||||||
|
console.log('Users aged 25:', youngUsers);
|
||||||
|
|
||||||
|
// Find one
|
||||||
|
const diana = await userRepo.findOne({ email: 'diana@example.com' });
|
||||||
|
console.log('Found Diana:', diana);
|
||||||
|
|
||||||
|
// Save (insert new)
|
||||||
|
const newUser: User = {
|
||||||
|
name: 'Frank',
|
||||||
|
email: 'frank@example.com',
|
||||||
|
age: 40,
|
||||||
|
};
|
||||||
|
await userRepo.save(newUser);
|
||||||
|
console.log('Saved new user:', newUser);
|
||||||
|
|
||||||
|
// Save (update existing)
|
||||||
|
if (diana) {
|
||||||
|
diana.age = 36;
|
||||||
|
await userRepo.save(diana);
|
||||||
|
console.log('Updated Diana:', diana);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove
|
||||||
|
if (newUser.id) {
|
||||||
|
await userRepo.remove(newUser);
|
||||||
|
console.log('Deleted Frank');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Transactions
|
||||||
|
console.log('\n--- Transaction Operations ---');
|
||||||
|
|
||||||
|
const txResults = await client.db.transaction([
|
||||||
|
{
|
||||||
|
kind: 'exec',
|
||||||
|
sql: 'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
|
||||||
|
args: ['Grace', 'grace@example.com', 27],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'exec',
|
||||||
|
sql: 'UPDATE users SET active = ? WHERE age < ?',
|
||||||
|
args: [0, 26],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'query',
|
||||||
|
sql: 'SELECT COUNT(*) as count FROM users WHERE active = ?',
|
||||||
|
args: [1],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('Transaction results:', txResults);
|
||||||
|
|
||||||
|
// 8. Get schema
|
||||||
|
console.log('\n--- Schema Information ---');
|
||||||
|
const schema = await client.db.getSchema();
|
||||||
|
console.log('Database schema:', schema);
|
||||||
|
|
||||||
|
console.log('\n--- CRUD operations completed successfully ---');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
140
examples/pubsub-chat.ts
Normal file
140
examples/pubsub-chat.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* Pub/Sub Chat Example
|
||||||
|
*
|
||||||
|
* Demonstrates a simple chat application using pub/sub with presence tracking.
|
||||||
|
* Multiple clients can join a room, send messages, and see who's online.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createClient } from '../src/index';
|
||||||
|
import type { PresenceMember } from '../src/index';
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
user: string;
|
||||||
|
text: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createChatClient(userName: string, roomName: string) {
|
||||||
|
const client = createClient({
|
||||||
|
baseURL: 'http://localhost:6001',
|
||||||
|
apiKey: 'ak_your_key:default',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[${userName}] Joining room: ${roomName}...`);
|
||||||
|
|
||||||
|
// Subscribe to chat room with presence
|
||||||
|
const subscription = await client.pubsub.subscribe(roomName, {
|
||||||
|
onMessage: (msg) => {
|
||||||
|
try {
|
||||||
|
const chatMsg: ChatMessage = JSON.parse(msg.data);
|
||||||
|
const time = new Date(chatMsg.timestamp).toLocaleTimeString();
|
||||||
|
console.log(`[${time}] ${chatMsg.user}: ${chatMsg.text}`);
|
||||||
|
} catch {
|
||||||
|
console.log(`[${userName}] Received: ${msg.data}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
console.error(`[${userName}] Error:`, err.message);
|
||||||
|
},
|
||||||
|
onClose: () => {
|
||||||
|
console.log(`[${userName}] Disconnected from ${roomName}`);
|
||||||
|
},
|
||||||
|
presence: {
|
||||||
|
enabled: true,
|
||||||
|
memberId: userName,
|
||||||
|
meta: {
|
||||||
|
displayName: userName,
|
||||||
|
joinedAt: Date.now(),
|
||||||
|
},
|
||||||
|
onJoin: (member: PresenceMember) => {
|
||||||
|
console.log(`[${userName}] 👋 ${member.memberId} joined the room`);
|
||||||
|
if (member.meta) {
|
||||||
|
console.log(`[${userName}] Display name: ${member.meta.displayName}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLeave: (member: PresenceMember) => {
|
||||||
|
console.log(`[${userName}] 👋 ${member.memberId} left the room`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[${userName}] ✓ Joined ${roomName}`);
|
||||||
|
|
||||||
|
// Send a join message
|
||||||
|
await sendMessage(client, roomName, userName, 'Hello everyone!');
|
||||||
|
|
||||||
|
// Helper to send messages
|
||||||
|
async function sendMessage(client: any, room: string, user: string, text: string) {
|
||||||
|
const chatMsg: ChatMessage = {
|
||||||
|
user,
|
||||||
|
text,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
await client.pubsub.publish(room, JSON.stringify(chatMsg));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current presence
|
||||||
|
if (subscription.hasPresence()) {
|
||||||
|
const members = await subscription.getPresence();
|
||||||
|
console.log(`[${userName}] Current members in room (${members.length}):`);
|
||||||
|
members.forEach(m => {
|
||||||
|
console.log(`[${userName}] - ${m.memberId} (joined at ${new Date(m.joinedAt).toLocaleTimeString()})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
client,
|
||||||
|
subscription,
|
||||||
|
sendMessage: (text: string) => sendMessage(client, roomName, userName, text),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const roomName = 'chat:lobby';
|
||||||
|
|
||||||
|
// Create first user
|
||||||
|
const alice = await createChatClient('Alice', roomName);
|
||||||
|
|
||||||
|
// Wait a bit
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
// Create second user
|
||||||
|
const bob = await createChatClient('Bob', roomName);
|
||||||
|
|
||||||
|
// Wait a bit
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
// Send some messages
|
||||||
|
await alice.sendMessage('Hey Bob! How are you?');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
await bob.sendMessage('Hi Alice! I\'m doing great, thanks!');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
await alice.sendMessage('That\'s awesome! Want to grab coffee later?');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
await bob.sendMessage('Sure! See you at 3pm?');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
await alice.sendMessage('Perfect! See you then! 👋');
|
||||||
|
|
||||||
|
// Wait to receive all messages
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// Get final presence count
|
||||||
|
const presence = await alice.client.pubsub.getPresence(roomName);
|
||||||
|
console.log(`\nFinal presence count: ${presence.count} members`);
|
||||||
|
|
||||||
|
// Leave room
|
||||||
|
console.log('\nClosing connections...');
|
||||||
|
alice.subscription.close();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
bob.subscription.close();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
console.log('\n--- Chat example completed ---');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
29
package.json
29
package.json
@ -1,15 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "@debros/network-ts-sdk",
|
"name": "@debros/network-ts-sdk",
|
||||||
"version": "0.4.2",
|
"version": "0.6.0",
|
||||||
"description": "TypeScript SDK for DeBros Network Gateway",
|
"description": "TypeScript SDK for DeBros Network Gateway - Database, PubSub, Cache, Storage, and more",
|
||||||
"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/tree/v0.0.1"
|
"url": "https://github.com/DeBrosOfficial/network-ts-sdk"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/DeBrosOfficial/network-ts-sdk/issues"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
@ -38,9 +59,11 @@
|
|||||||
"@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,6 +21,9 @@ 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
|
||||||
@ -30,6 +33,9 @@ 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
|
||||||
@ -39,6 +45,30 @@ 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'}
|
||||||
@ -368,6 +398,10 @@ 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}
|
||||||
@ -587,6 +621,11 @@ 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==}
|
||||||
|
|
||||||
@ -627,6 +666,9 @@ 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'}
|
||||||
@ -912,6 +954,9 @@ 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'}
|
||||||
@ -967,6 +1012,22 @@ 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==}
|
||||||
|
|
||||||
@ -990,6 +1051,9 @@ 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==}
|
||||||
|
|
||||||
@ -1028,9 +1092,24 @@ 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==}
|
||||||
|
|
||||||
@ -1249,6 +1328,9 @@ 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==}
|
||||||
|
|
||||||
@ -1311,6 +1393,10 @@ 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==}
|
||||||
|
|
||||||
@ -1390,6 +1476,13 @@ 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'}
|
||||||
@ -1465,6 +1558,12 @@ 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==}
|
||||||
|
|
||||||
@ -1518,6 +1617,26 @@ 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
|
||||||
|
|
||||||
@ -1709,6 +1828,8 @@ 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
|
||||||
@ -1908,6 +2029,25 @@ 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
|
||||||
@ -1958,6 +2098,8 @@ 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
|
||||||
@ -2316,6 +2458,8 @@ 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: {}
|
||||||
@ -2354,6 +2498,27 @@ 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
|
||||||
@ -2374,6 +2539,8 @@ 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
|
||||||
@ -2408,10 +2575,24 @@ 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: {}
|
||||||
@ -2610,6 +2791,13 @@ 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: {}
|
||||||
@ -2668,6 +2856,12 @@ 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:
|
||||||
@ -2743,6 +2937,14 @@ 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: {}
|
||||||
@ -2814,6 +3016,10 @@ 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:
|
||||||
|
|||||||
3
src/auth/index.ts
Normal file
3
src/auth/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { AuthClient } from "./client";
|
||||||
|
export type { AuthConfig, WhoAmI, StorageAdapter } from "./types";
|
||||||
|
export { MemoryStorage, LocalStorageAdapter } from "./types";
|
||||||
14
src/cache/index.ts
vendored
Normal file
14
src/cache/index.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export { CacheClient } from "./client";
|
||||||
|
export type {
|
||||||
|
CacheGetRequest,
|
||||||
|
CacheGetResponse,
|
||||||
|
CachePutRequest,
|
||||||
|
CachePutResponse,
|
||||||
|
CacheDeleteRequest,
|
||||||
|
CacheDeleteResponse,
|
||||||
|
CacheMultiGetRequest,
|
||||||
|
CacheMultiGetResponse,
|
||||||
|
CacheScanRequest,
|
||||||
|
CacheScanResponse,
|
||||||
|
CacheHealthResponse,
|
||||||
|
} from "./client";
|
||||||
107
src/core/http.ts
107
src/core/http.ts
@ -1,11 +1,39 @@
|
|||||||
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;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
maxRetries?: number;
|
maxRetries?: number;
|
||||||
retryDelayMs?: number;
|
retryDelayMs?: number;
|
||||||
fetch?: typeof fetch;
|
fetch?: typeof fetch;
|
||||||
|
/**
|
||||||
|
* Enable debug logging (includes full SQL queries and args). Default: false
|
||||||
|
*/
|
||||||
|
debug?: boolean;
|
||||||
|
/**
|
||||||
|
* Callback invoked on network errors (after all retries exhausted).
|
||||||
|
* Use this to trigger gateway failover at the application layer.
|
||||||
|
*/
|
||||||
|
onNetworkError?: NetworkErrorCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,6 +67,8 @@ export class HttpClient {
|
|||||||
private fetch: typeof fetch;
|
private fetch: typeof fetch;
|
||||||
private apiKey?: string;
|
private apiKey?: string;
|
||||||
private jwt?: string;
|
private jwt?: string;
|
||||||
|
private debug: boolean;
|
||||||
|
private onNetworkError?: NetworkErrorCallback;
|
||||||
|
|
||||||
constructor(config: HttpClientConfig) {
|
constructor(config: HttpClientConfig) {
|
||||||
this.baseURL = config.baseURL.replace(/\/$/, "");
|
this.baseURL = config.baseURL.replace(/\/$/, "");
|
||||||
@ -47,6 +77,15 @@ export class HttpClient {
|
|||||||
this.retryDelayMs = config.retryDelayMs ?? 1000;
|
this.retryDelayMs = config.retryDelayMs ?? 1000;
|
||||||
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the network error callback
|
||||||
|
*/
|
||||||
|
setOnNetworkError(callback: NetworkErrorCallback | undefined): void {
|
||||||
|
this.onNetworkError = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
setApiKey(apiKey?: string) {
|
setApiKey(apiKey?: string) {
|
||||||
@ -223,7 +262,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) {
|
if (queryDetails && this.debug) {
|
||||||
console.log(logMessage);
|
console.log(logMessage);
|
||||||
console.log(`[HttpClient] ${queryDetails}`);
|
console.log(`[HttpClient] ${queryDetails}`);
|
||||||
} else {
|
} else {
|
||||||
@ -253,11 +292,32 @@ export class HttpClient {
|
|||||||
2
|
2
|
||||||
)}ms:`;
|
)}ms:`;
|
||||||
console.error(errorMessage, error);
|
console.error(errorMessage, error);
|
||||||
if (queryDetails) {
|
if (queryDetails && this.debug) {
|
||||||
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);
|
||||||
@ -397,6 +457,25 @@ 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);
|
||||||
@ -425,17 +504,33 @@ 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 error = await response.json().catch(() => ({
|
const errorBody = await response.json().catch(() => ({
|
||||||
error: response.statusText,
|
error: response.statusText,
|
||||||
}));
|
}));
|
||||||
throw SDKError.fromResponse(response.status, error);
|
throw SDKError.fromResponse(response.status, errorBody);
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
if (error instanceof SDKError) {
|
|
||||||
throw 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: "GET",
|
||||||
|
path,
|
||||||
|
isRetry: false,
|
||||||
|
attempt: 0,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/core/index.ts
Normal file
10
src/core/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export { HttpClient, type HttpClientConfig, type NetworkErrorCallback, type NetworkErrorContext } from "./http";
|
||||||
|
export { WSClient, type WSClientConfig } from "./ws";
|
||||||
|
export type { IHttpTransport, RequestOptions } from "./interfaces/IHttpTransport";
|
||||||
|
export type { IWebSocketClient } from "./interfaces/IWebSocketClient";
|
||||||
|
export type { IAuthStrategy, RequestContext } from "./interfaces/IAuthStrategy";
|
||||||
|
export type { IRetryPolicy } from "./interfaces/IRetryPolicy";
|
||||||
|
export { PathBasedAuthStrategy } from "./transport/AuthHeaderStrategy";
|
||||||
|
export { ExponentialBackoffRetryPolicy } from "./transport/RequestRetryPolicy";
|
||||||
|
export { RequestLogger } from "./transport/RequestLogger";
|
||||||
|
export { TLSConfiguration } from "./transport/TLSConfiguration";
|
||||||
28
src/core/interfaces/IAuthStrategy.ts
Normal file
28
src/core/interfaces/IAuthStrategy.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Request context for authentication
|
||||||
|
*/
|
||||||
|
export interface RequestContext {
|
||||||
|
path: string;
|
||||||
|
method: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication strategy interface
|
||||||
|
* Provides abstraction for different authentication header strategies
|
||||||
|
*/
|
||||||
|
export interface IAuthStrategy {
|
||||||
|
/**
|
||||||
|
* Get authentication headers for a request
|
||||||
|
*/
|
||||||
|
getHeaders(context: RequestContext): Record<string, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set API key
|
||||||
|
*/
|
||||||
|
setApiKey(apiKey?: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set JWT token
|
||||||
|
*/
|
||||||
|
setJwt(jwt?: string): void;
|
||||||
|
}
|
||||||
73
src/core/interfaces/IHttpTransport.ts
Normal file
73
src/core/interfaces/IHttpTransport.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* HTTP Request options
|
||||||
|
*/
|
||||||
|
export interface RequestOptions {
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
query?: Record<string, string | number | boolean>;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP Transport abstraction interface
|
||||||
|
* Provides a testable abstraction layer for HTTP operations
|
||||||
|
*/
|
||||||
|
export interface IHttpTransport {
|
||||||
|
/**
|
||||||
|
* Perform GET request
|
||||||
|
*/
|
||||||
|
get<T = any>(path: string, options?: RequestOptions): Promise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform POST request
|
||||||
|
*/
|
||||||
|
post<T = any>(path: string, body?: any, options?: RequestOptions): Promise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform PUT request
|
||||||
|
*/
|
||||||
|
put<T = any>(path: string, body?: any, options?: RequestOptions): Promise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform DELETE request
|
||||||
|
*/
|
||||||
|
delete<T = any>(path: string, options?: RequestOptions): Promise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload file using multipart/form-data
|
||||||
|
*/
|
||||||
|
uploadFile<T = any>(
|
||||||
|
path: string,
|
||||||
|
formData: FormData,
|
||||||
|
options?: { timeout?: number }
|
||||||
|
): Promise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get binary response (returns Response object for streaming)
|
||||||
|
*/
|
||||||
|
getBinary(path: string): Promise<Response>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get base URL
|
||||||
|
*/
|
||||||
|
getBaseURL(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API key
|
||||||
|
*/
|
||||||
|
getApiKey(): string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current token (JWT or API key)
|
||||||
|
*/
|
||||||
|
getToken(): string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set API key for authentication
|
||||||
|
*/
|
||||||
|
setApiKey(apiKey?: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set JWT token for authentication
|
||||||
|
*/
|
||||||
|
setJwt(jwt?: string): void;
|
||||||
|
}
|
||||||
20
src/core/interfaces/IRetryPolicy.ts
Normal file
20
src/core/interfaces/IRetryPolicy.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Retry policy interface
|
||||||
|
* Provides abstraction for retry logic and backoff strategies
|
||||||
|
*/
|
||||||
|
export interface IRetryPolicy {
|
||||||
|
/**
|
||||||
|
* Determine if request should be retried
|
||||||
|
*/
|
||||||
|
shouldRetry(error: any, attempt: number): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get delay before next retry attempt (in milliseconds)
|
||||||
|
*/
|
||||||
|
getDelay(attempt: number): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get maximum number of retry attempts
|
||||||
|
*/
|
||||||
|
getMaxRetries(): number;
|
||||||
|
}
|
||||||
60
src/core/interfaces/IWebSocketClient.ts
Normal file
60
src/core/interfaces/IWebSocketClient.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* WebSocket Client abstraction interface
|
||||||
|
* Provides a testable abstraction layer for WebSocket operations
|
||||||
|
*/
|
||||||
|
export interface IWebSocketClient {
|
||||||
|
/**
|
||||||
|
* Connect to WebSocket server
|
||||||
|
*/
|
||||||
|
connect(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close WebSocket connection
|
||||||
|
*/
|
||||||
|
close(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send data through WebSocket
|
||||||
|
*/
|
||||||
|
send(data: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register message handler
|
||||||
|
*/
|
||||||
|
onMessage(handler: (data: string) => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister message handler
|
||||||
|
*/
|
||||||
|
offMessage(handler: (data: string) => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register error handler
|
||||||
|
*/
|
||||||
|
onError(handler: (error: Error) => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister error handler
|
||||||
|
*/
|
||||||
|
offError(handler: (error: Error) => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register close handler
|
||||||
|
*/
|
||||||
|
onClose(handler: () => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister close handler
|
||||||
|
*/
|
||||||
|
offClose(handler: () => void): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if WebSocket is connected
|
||||||
|
*/
|
||||||
|
isConnected(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get WebSocket URL
|
||||||
|
*/
|
||||||
|
get url(): string;
|
||||||
|
}
|
||||||
4
src/core/interfaces/index.ts
Normal file
4
src/core/interfaces/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type { IHttpTransport, RequestOptions } from "./IHttpTransport";
|
||||||
|
export type { IWebSocketClient } from "./IWebSocketClient";
|
||||||
|
export type { IAuthStrategy, RequestContext } from "./IAuthStrategy";
|
||||||
|
export type { IRetryPolicy } from "./IRetryPolicy";
|
||||||
108
src/core/transport/AuthHeaderStrategy.ts
Normal file
108
src/core/transport/AuthHeaderStrategy.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import type { IAuthStrategy, RequestContext } from "../interfaces/IAuthStrategy";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication type for different operations
|
||||||
|
*/
|
||||||
|
type AuthType = "api-key-only" | "api-key-preferred" | "jwt-preferred" | "both";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path-based authentication strategy
|
||||||
|
* Determines which auth credentials to use based on the request path
|
||||||
|
*/
|
||||||
|
export class PathBasedAuthStrategy implements IAuthStrategy {
|
||||||
|
private apiKey?: string;
|
||||||
|
private jwt?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of path patterns to auth types
|
||||||
|
*/
|
||||||
|
private readonly authRules: Array<{ pattern: string; type: AuthType }> = [
|
||||||
|
// Database, PubSub, Proxy, Cache: prefer API key
|
||||||
|
{ pattern: "/v1/rqlite/", type: "api-key-only" },
|
||||||
|
{ pattern: "/v1/pubsub/", type: "api-key-only" },
|
||||||
|
{ pattern: "/v1/proxy/", type: "api-key-only" },
|
||||||
|
{ pattern: "/v1/cache/", type: "api-key-only" },
|
||||||
|
// Auth operations: prefer API key
|
||||||
|
{ pattern: "/v1/auth/", type: "api-key-preferred" },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(apiKey?: string, jwt?: string) {
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
this.jwt = jwt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get authentication headers for a request
|
||||||
|
*/
|
||||||
|
getHeaders(context: RequestContext): Record<string, string> {
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
const authType = this.detectAuthType(context.path);
|
||||||
|
|
||||||
|
switch (authType) {
|
||||||
|
case "api-key-only":
|
||||||
|
if (this.apiKey) {
|
||||||
|
headers["X-API-Key"] = this.apiKey;
|
||||||
|
} else if (this.jwt) {
|
||||||
|
// Fallback to JWT if no API key
|
||||||
|
headers["Authorization"] = `Bearer ${this.jwt}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "api-key-preferred":
|
||||||
|
if (this.apiKey) {
|
||||||
|
headers["X-API-Key"] = this.apiKey;
|
||||||
|
}
|
||||||
|
if (this.jwt) {
|
||||||
|
headers["Authorization"] = `Bearer ${this.jwt}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "jwt-preferred":
|
||||||
|
if (this.jwt) {
|
||||||
|
headers["Authorization"] = `Bearer ${this.jwt}`;
|
||||||
|
}
|
||||||
|
if (this.apiKey) {
|
||||||
|
headers["X-API-Key"] = this.apiKey;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "both":
|
||||||
|
if (this.jwt) {
|
||||||
|
headers["Authorization"] = `Bearer ${this.jwt}`;
|
||||||
|
}
|
||||||
|
if (this.apiKey) {
|
||||||
|
headers["X-API-Key"] = this.apiKey;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set API key
|
||||||
|
*/
|
||||||
|
setApiKey(apiKey?: string): void {
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set JWT token
|
||||||
|
*/
|
||||||
|
setJwt(jwt?: string): void {
|
||||||
|
this.jwt = jwt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect auth type based on path
|
||||||
|
*/
|
||||||
|
private detectAuthType(path: string): AuthType {
|
||||||
|
for (const rule of this.authRules) {
|
||||||
|
if (path.includes(rule.pattern)) {
|
||||||
|
return rule.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default: send both if available
|
||||||
|
return "both";
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/core/transport/RequestLogger.ts
Normal file
116
src/core/transport/RequestLogger.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* Request logger for debugging HTTP operations
|
||||||
|
*/
|
||||||
|
export class RequestLogger {
|
||||||
|
private readonly debug: boolean;
|
||||||
|
|
||||||
|
constructor(debug: boolean = false) {
|
||||||
|
this.debug = debug;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log successful request
|
||||||
|
*/
|
||||||
|
logSuccess(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
duration: number,
|
||||||
|
queryDetails?: string
|
||||||
|
): void {
|
||||||
|
if (typeof console === "undefined") return;
|
||||||
|
|
||||||
|
const logMessage = `[HttpClient] ${method} ${path} completed in ${duration.toFixed(2)}ms`;
|
||||||
|
|
||||||
|
if (queryDetails && this.debug) {
|
||||||
|
console.log(logMessage);
|
||||||
|
console.log(`[HttpClient] ${queryDetails}`);
|
||||||
|
} else {
|
||||||
|
console.log(logMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log failed request
|
||||||
|
*/
|
||||||
|
logError(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
duration: number,
|
||||||
|
error: any,
|
||||||
|
queryDetails?: string
|
||||||
|
): void {
|
||||||
|
if (typeof console === "undefined") return;
|
||||||
|
|
||||||
|
// Special handling for 404 on find-one (expected behavior)
|
||||||
|
const is404FindOne =
|
||||||
|
path === "/v1/rqlite/find-one" &&
|
||||||
|
error?.httpStatus === 404;
|
||||||
|
|
||||||
|
if (is404FindOne) {
|
||||||
|
console.warn(
|
||||||
|
`[HttpClient] ${method} ${path} returned 404 after ${duration.toFixed(2)}ms (expected for optional lookups)`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage = `[HttpClient] ${method} ${path} failed after ${duration.toFixed(2)}ms:`;
|
||||||
|
console.error(errorMessage, error);
|
||||||
|
|
||||||
|
if (queryDetails && this.debug) {
|
||||||
|
console.error(`[HttpClient] ${queryDetails}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract query details from request for logging
|
||||||
|
*/
|
||||||
|
extractQueryDetails(path: string, body?: any): string | null {
|
||||||
|
if (!this.debug) return null;
|
||||||
|
|
||||||
|
const isRqliteOperation = path.includes("/v1/rqlite/");
|
||||||
|
if (!isRqliteOperation || !body) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedBody = typeof body === "string" ? JSON.parse(body) : body;
|
||||||
|
|
||||||
|
// Direct SQL query
|
||||||
|
if (parsedBody.sql) {
|
||||||
|
let details = `SQL: ${parsedBody.sql}`;
|
||||||
|
if (parsedBody.args && parsedBody.args.length > 0) {
|
||||||
|
details += ` | Args: [${parsedBody.args
|
||||||
|
.map((a: any) => (typeof a === "string" ? `"${a}"` : a))
|
||||||
|
.join(", ")}]`;
|
||||||
|
}
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table-based query
|
||||||
|
if (parsedBody.table) {
|
||||||
|
let details = `Table: ${parsedBody.table}`;
|
||||||
|
if (parsedBody.criteria && Object.keys(parsedBody.criteria).length > 0) {
|
||||||
|
details += ` | Criteria: ${JSON.stringify(parsedBody.criteria)}`;
|
||||||
|
}
|
||||||
|
if (parsedBody.options) {
|
||||||
|
details += ` | Options: ${JSON.stringify(parsedBody.options)}`;
|
||||||
|
}
|
||||||
|
if (parsedBody.select) {
|
||||||
|
details += ` | Select: ${JSON.stringify(parsedBody.select)}`;
|
||||||
|
}
|
||||||
|
if (parsedBody.where) {
|
||||||
|
details += ` | Where: ${JSON.stringify(parsedBody.where)}`;
|
||||||
|
}
|
||||||
|
if (parsedBody.limit) {
|
||||||
|
details += ` | Limit: ${parsedBody.limit}`;
|
||||||
|
}
|
||||||
|
if (parsedBody.offset) {
|
||||||
|
details += ` | Offset: ${parsedBody.offset}`;
|
||||||
|
}
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Failed to parse, ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/core/transport/RequestRetryPolicy.ts
Normal file
53
src/core/transport/RequestRetryPolicy.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import type { IRetryPolicy } from "../interfaces/IRetryPolicy";
|
||||||
|
import { SDKError } from "../../errors";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exponential backoff retry policy
|
||||||
|
* Retries failed requests with increasing delays
|
||||||
|
*/
|
||||||
|
export class ExponentialBackoffRetryPolicy implements IRetryPolicy {
|
||||||
|
private readonly maxRetries: number;
|
||||||
|
private readonly baseDelayMs: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP status codes that should trigger a retry
|
||||||
|
*/
|
||||||
|
private readonly retryableStatusCodes = [408, 429, 500, 502, 503, 504];
|
||||||
|
|
||||||
|
constructor(maxRetries: number = 3, baseDelayMs: number = 1000) {
|
||||||
|
this.maxRetries = maxRetries;
|
||||||
|
this.baseDelayMs = baseDelayMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if request should be retried
|
||||||
|
*/
|
||||||
|
shouldRetry(error: any, attempt: number): boolean {
|
||||||
|
// Don't retry if max attempts reached
|
||||||
|
if (attempt >= this.maxRetries) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry on retryable HTTP errors
|
||||||
|
if (error instanceof SDKError) {
|
||||||
|
return this.retryableStatusCodes.includes(error.httpStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't retry other errors
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get delay before next retry (exponential backoff)
|
||||||
|
*/
|
||||||
|
getDelay(attempt: number): number {
|
||||||
|
return this.baseDelayMs * (attempt + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get maximum number of retry attempts
|
||||||
|
*/
|
||||||
|
getMaxRetries(): number {
|
||||||
|
return this.maxRetries;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/core/transport/TLSConfiguration.ts
Normal file
53
src/core/transport/TLSConfiguration.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* TLS Configuration for development/staging environments
|
||||||
|
*
|
||||||
|
* WARNING: Only use this in development/testing environments!
|
||||||
|
* DO NOT disable certificate validation in production.
|
||||||
|
*/
|
||||||
|
export class TLSConfiguration {
|
||||||
|
/**
|
||||||
|
* Create fetch function with proper TLS configuration
|
||||||
|
*/
|
||||||
|
static createFetchWithTLSConfig(): typeof fetch {
|
||||||
|
// Only allow insecure TLS in development
|
||||||
|
if (this.shouldAllowInsecure()) {
|
||||||
|
this.configureInsecureTLS();
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalThis.fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if insecure TLS should be allowed
|
||||||
|
*/
|
||||||
|
private static shouldAllowInsecure(): boolean {
|
||||||
|
// Check if we're in Node.js environment
|
||||||
|
if (typeof process === "undefined" || !process.versions?.node) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow in non-production with explicit flag
|
||||||
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
const allowInsecure = process.env.DEBROS_ALLOW_INSECURE_TLS === "true";
|
||||||
|
|
||||||
|
return !isProduction && allowInsecure;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure Node.js to allow insecure TLS
|
||||||
|
* WARNING: Only call in development!
|
||||||
|
*/
|
||||||
|
private static configureInsecureTLS(): void {
|
||||||
|
if (typeof process !== "undefined" && process.env) {
|
||||||
|
// Allow self-signed/staging certificates for development
|
||||||
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
|
|
||||||
|
if (typeof console !== "undefined") {
|
||||||
|
console.warn(
|
||||||
|
"[TLSConfiguration] WARNING: TLS certificate validation disabled for development. " +
|
||||||
|
"DO NOT use in production!"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/core/transport/index.ts
Normal file
4
src/core/transport/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { PathBasedAuthStrategy } from "./AuthHeaderStrategy";
|
||||||
|
export { ExponentialBackoffRetryPolicy } from "./RequestRetryPolicy";
|
||||||
|
export { RequestLogger } from "./RequestLogger";
|
||||||
|
export { TLSConfiguration } from "./TLSConfiguration";
|
||||||
@ -1,11 +1,17 @@
|
|||||||
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;
|
||||||
@ -23,6 +29,7 @@ export class WSClient {
|
|||||||
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();
|
||||||
@ -36,6 +43,14 @@ export class WSClient {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,9 +72,19 @@ export class WSClient {
|
|||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
this.ws?.close();
|
this.ws?.close();
|
||||||
reject(
|
const error = new SDKError("WebSocket connection timeout", 408, "WS_TIMEOUT");
|
||||||
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", () => {
|
||||||
@ -78,6 +103,17 @@ 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);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
|||||||
13
src/db/index.ts
Normal file
13
src/db/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export { DBClient } from "./client";
|
||||||
|
export { QueryBuilder } from "./qb";
|
||||||
|
export { Repository } from "./repository";
|
||||||
|
export type {
|
||||||
|
Entity,
|
||||||
|
QueryResponse,
|
||||||
|
TransactionOp,
|
||||||
|
TransactionRequest,
|
||||||
|
SelectOptions,
|
||||||
|
FindOptions,
|
||||||
|
ColumnDefinition,
|
||||||
|
} from "./types";
|
||||||
|
export { extractTableName, extractPrimaryKey } from "./types";
|
||||||
2
src/functions/index.ts
Normal file
2
src/functions/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { FunctionsClient, type FunctionsClientConfig } from "./client";
|
||||||
|
export type { FunctionResponse, SuccessResponse } from "./types";
|
||||||
11
src/index.ts
11
src/index.ts
@ -1,4 +1,4 @@
|
|||||||
import { HttpClient, HttpClientConfig } from "./core/http";
|
import { HttpClient, HttpClientConfig, NetworkErrorCallback } 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";
|
||||||
@ -20,6 +20,11 @@ export interface ClientConfig extends Omit<HttpClientConfig, "fetch"> {
|
|||||||
wsConfig?: Partial<Omit<WSClientConfig, "wsURL">>;
|
wsConfig?: Partial<Omit<WSClientConfig, "wsURL">>;
|
||||||
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 {
|
||||||
@ -38,7 +43,9 @@ 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,6 +62,7 @@ export function createClient(config: ClientConfig): Client {
|
|||||||
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);
|
||||||
@ -73,6 +81,7 @@ 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";
|
||||||
|
|||||||
7
src/network/index.ts
Normal file
7
src/network/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export { NetworkClient } from "./client";
|
||||||
|
export type {
|
||||||
|
PeerInfo,
|
||||||
|
NetworkStatus,
|
||||||
|
ProxyRequest,
|
||||||
|
ProxyResponse,
|
||||||
|
} from "./client";
|
||||||
12
src/pubsub/index.ts
Normal file
12
src/pubsub/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export { PubSubClient, Subscription } from "./client";
|
||||||
|
export type {
|
||||||
|
PubSubMessage,
|
||||||
|
RawEnvelope,
|
||||||
|
MessageHandler,
|
||||||
|
ErrorHandler,
|
||||||
|
CloseHandler,
|
||||||
|
PresenceMember,
|
||||||
|
PresenceResponse,
|
||||||
|
PresenceOptions,
|
||||||
|
SubscribeOptions,
|
||||||
|
} from "./types";
|
||||||
7
src/storage/index.ts
Normal file
7
src/storage/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export { StorageClient } from "./client";
|
||||||
|
export type {
|
||||||
|
StorageUploadResponse,
|
||||||
|
StoragePinRequest,
|
||||||
|
StoragePinResponse,
|
||||||
|
StorageStatus,
|
||||||
|
} from "./client";
|
||||||
68
src/utils/codec.ts
Normal file
68
src/utils/codec.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Base64 Codec for cross-platform encoding/decoding
|
||||||
|
* Works in both Node.js and browser environments
|
||||||
|
*/
|
||||||
|
export class Base64Codec {
|
||||||
|
/**
|
||||||
|
* Encode string or Uint8Array to base64
|
||||||
|
*/
|
||||||
|
static encode(input: string | Uint8Array): string {
|
||||||
|
if (typeof input === "string") {
|
||||||
|
return this.encodeString(input);
|
||||||
|
}
|
||||||
|
return this.encodeBytes(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode string to base64
|
||||||
|
*/
|
||||||
|
static encodeString(str: string): string {
|
||||||
|
if (this.isNode()) {
|
||||||
|
return Buffer.from(str).toString("base64");
|
||||||
|
}
|
||||||
|
// Browser
|
||||||
|
return btoa(
|
||||||
|
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) =>
|
||||||
|
String.fromCharCode(parseInt(p1, 16))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode Uint8Array to base64
|
||||||
|
*/
|
||||||
|
static encodeBytes(bytes: Uint8Array): string {
|
||||||
|
if (this.isNode()) {
|
||||||
|
return Buffer.from(bytes).toString("base64");
|
||||||
|
}
|
||||||
|
// Browser
|
||||||
|
let binary = "";
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode base64 to string
|
||||||
|
*/
|
||||||
|
static decode(b64: string): string {
|
||||||
|
if (this.isNode()) {
|
||||||
|
return Buffer.from(b64, "base64").toString("utf-8");
|
||||||
|
}
|
||||||
|
// Browser
|
||||||
|
const binary = atob(b64);
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return new TextDecoder().decode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running in Node.js environment
|
||||||
|
*/
|
||||||
|
private static isNode(): boolean {
|
||||||
|
return typeof Buffer !== "undefined";
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/utils/index.ts
Normal file
3
src/utils/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { Base64Codec } from "./codec";
|
||||||
|
export { retryWithBackoff, type RetryConfig } from "./retry";
|
||||||
|
export { Platform } from "./platform";
|
||||||
44
src/utils/platform.ts
Normal file
44
src/utils/platform.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Platform detection utilities
|
||||||
|
* Helps determine runtime environment (Node.js vs Browser)
|
||||||
|
*/
|
||||||
|
export const Platform = {
|
||||||
|
/**
|
||||||
|
* Check if running in Node.js
|
||||||
|
*/
|
||||||
|
isNode: (): boolean => {
|
||||||
|
return typeof process !== "undefined" && !!process.versions?.node;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running in browser
|
||||||
|
*/
|
||||||
|
isBrowser: (): boolean => {
|
||||||
|
return typeof window !== "undefined";
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if localStorage is available
|
||||||
|
*/
|
||||||
|
hasLocalStorage: (): boolean => {
|
||||||
|
try {
|
||||||
|
return typeof localStorage !== "undefined" && localStorage !== null;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Buffer is available (Node.js)
|
||||||
|
*/
|
||||||
|
hasBuffer: (): boolean => {
|
||||||
|
return typeof Buffer !== "undefined";
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if btoa/atob are available (Browser)
|
||||||
|
*/
|
||||||
|
hasBase64: (): boolean => {
|
||||||
|
return typeof btoa !== "undefined" && typeof atob !== "undefined";
|
||||||
|
},
|
||||||
|
};
|
||||||
58
src/utils/retry.ts
Normal file
58
src/utils/retry.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Retry configuration
|
||||||
|
*/
|
||||||
|
export interface RetryConfig {
|
||||||
|
/**
|
||||||
|
* Maximum number of retry attempts
|
||||||
|
*/
|
||||||
|
maxAttempts: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to calculate backoff delay in milliseconds
|
||||||
|
*/
|
||||||
|
backoffMs: (attempt: number) => number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to determine if error should trigger retry
|
||||||
|
*/
|
||||||
|
shouldRetry: (error: any) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry an operation with exponential backoff
|
||||||
|
* @param operation - The async operation to retry
|
||||||
|
* @param config - Retry configuration
|
||||||
|
* @returns Promise resolving to operation result
|
||||||
|
* @throws Last error if all retries exhausted
|
||||||
|
*/
|
||||||
|
export async function retryWithBackoff<T>(
|
||||||
|
operation: () => Promise<T>,
|
||||||
|
config: RetryConfig
|
||||||
|
): Promise<T> {
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error: any) {
|
||||||
|
lastError = error instanceof Error ? error : new Error(String(error));
|
||||||
|
|
||||||
|
// Check if we should retry this error
|
||||||
|
if (!config.shouldRetry(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this was the last attempt, throw
|
||||||
|
if (attempt === config.maxAttempts) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before next attempt
|
||||||
|
const delay = config.backoffMs(attempt);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback (should never reach here)
|
||||||
|
throw lastError || new Error("Retry failed");
|
||||||
|
}
|
||||||
@ -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.txt", {
|
const testFile = new File([testContent], "status-test", {
|
||||||
type: "text/plain",
|
type: "text/plain",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user