UNPKG

@hivellm/synap

Version:

Official TypeScript/JavaScript SDK for Synap - High-Performance In-Memory Key-Value Store & Message Broker

1,099 lines (1,091 loc) 27.4 kB
// src/client.ts import { v4 as uuidv4 } from "uuid"; // src/types.ts var SynapError = class _SynapError extends Error { constructor(message, code, statusCode, requestId) { super(message); this.code = code; this.statusCode = statusCode; this.requestId = requestId; this.name = "SynapError"; Object.setPrototypeOf(this, _SynapError.prototype); } }; var NetworkError = class extends SynapError { constructor(message, originalError) { super(message, "NETWORK_ERROR"); this.originalError = originalError; this.name = "NetworkError"; } }; var TimeoutError = class extends SynapError { constructor(message, timeoutMs) { super(message, "TIMEOUT_ERROR"); this.timeoutMs = timeoutMs; this.name = "TimeoutError"; } }; var ServerError = class extends SynapError { constructor(message, statusCode, requestId) { super(message, "SERVER_ERROR", statusCode, requestId); this.name = "ServerError"; } }; // src/client.ts var SynapClient = class { baseUrl; timeout; debug; auth; constructor(options = {}) { this.baseUrl = options.url || "http://localhost:15500"; this.timeout = options.timeout || 3e4; this.debug = options.debug || false; this.auth = options.auth; } /** * Send a command to the Synap server using StreamableHTTP protocol */ async sendCommand(command, payload = {}) { const request = { command, request_id: uuidv4(), payload }; if (this.debug) { console.log("[Synap] Request:", JSON.stringify(request, null, 2)); } try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); const headers = { "Content-Type": "application/json", "Accept": "application/json", "Accept-Encoding": "gzip" }; if (this.auth) { if (this.auth.type === "basic" && this.auth.username && this.auth.password) { const credentials = btoa(`${this.auth.username}:${this.auth.password}`); headers["Authorization"] = `Basic ${credentials}`; } else if (this.auth.type === "api_key" && this.auth.apiKey) { headers["Authorization"] = `Bearer ${this.auth.apiKey}`; } } const response = await fetch(`${this.baseUrl}/api/v1/command`, { method: "POST", headers, body: JSON.stringify(request), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new ServerError( `HTTP ${response.status}: ${response.statusText}`, response.status, request.request_id ); } const data = await response.json(); if (this.debug) { console.log("[Synap] Response:", JSON.stringify(data, null, 2)); } if (!data.success) { throw new ServerError( data.error || "Unknown server error", void 0, data.request_id ); } return data.payload; } catch (error) { if (error instanceof ServerError) { throw error; } if (error instanceof Error) { if (error.name === "AbortError") { throw new TimeoutError( `Request timed out after ${this.timeout}ms`, this.timeout ); } throw new NetworkError( `Network error: ${error.message}`, error ); } throw new NetworkError("Unknown network error"); } } /** * Ping the server to check connectivity */ async ping() { try { const response = await fetch(`${this.baseUrl}/health`, { signal: AbortSignal.timeout(this.timeout) }); return response.ok; } catch { return false; } } /** * Get server health status */ async health() { const response = await fetch(`${this.baseUrl}/health`, { signal: AbortSignal.timeout(this.timeout) }); if (!response.ok) { throw new NetworkError("Health check failed"); } return response.json(); } /** * Close the client (cleanup) */ close() { } }; // src/kv.ts var KVStore = class { constructor(client) { this.client = client; } /** * Set a key-value pair */ async set(key, value, options) { const payload = { key, value }; if (options?.ttl) { payload.ttl = options.ttl; } const result = await this.client.sendCommand("kv.set", payload); return result.success; } /** * Get a value by key */ async get(key) { const result = await this.client.sendCommand("kv.get", { key }); if (result === null || result === void 0) { return null; } if (typeof result === "string") { try { return JSON.parse(result); } catch (error) { return result; } } return result; } /** * Delete a key */ async del(key) { const result = await this.client.sendCommand("kv.del", { key }); return result.deleted; } /** * Check if a key exists */ async exists(key) { const result = await this.client.sendCommand("kv.exists", { key }); return result.exists; } /** * Increment a numeric value */ async incr(key, amount = 1) { const result = await this.client.sendCommand("kv.incr", { key, amount }); return result.value; } /** * Decrement a numeric value */ async decr(key, amount = 1) { const result = await this.client.sendCommand("kv.decr", { key, amount }); return result.value; } /** * Set multiple key-value pairs atomically */ async mset(entries) { const pairs = Object.entries(entries).map(([key, value]) => ({ key, value })); const result = await this.client.sendCommand("kv.mset", { pairs }); return result.success; } /** * Get multiple values by keys */ async mget(keys) { const result = await this.client.sendCommand( "kv.mget", { keys } ); const valuesObj = {}; keys.forEach((key, index) => { valuesObj[key] = result.values[index]; }); return valuesObj; } /** * Delete multiple keys */ async mdel(keys) { const result = await this.client.sendCommand("kv.mdel", { keys }); return result.deleted; } /** * Scan keys with optional prefix */ async scan(prefix, limit = 100) { const payload = { limit }; if (prefix) { payload.prefix = prefix; } return this.client.sendCommand("kv.scan", payload); } /** * List all keys matching a pattern */ async keys(pattern = "*") { const result = await this.client.sendCommand("kv.keys", { pattern }); return result.keys; } /** * Get database size (number of keys) */ async dbsize() { const result = await this.client.sendCommand("kv.dbsize", {}); return result.size; } /** * Set expiration time for a key */ async expire(key, seconds) { const result = await this.client.sendCommand("kv.expire", { key, ttl: seconds }); return result.result; } /** * Get TTL for a key */ async ttl(key) { const result = await this.client.sendCommand("kv.ttl", { key }); return result.ttl; } /** * Remove expiration from a key */ async persist(key) { const result = await this.client.sendCommand("kv.persist", { key }); return result.result; } /** * Flush current database */ async flushdb() { const result = await this.client.sendCommand("kv.flushdb", {}); return result.flushed; } /** * Flush all databases */ async flushall() { const result = await this.client.sendCommand("kv.flushall", {}); return result.flushed; } /** * Get store statistics */ async stats() { return this.client.sendCommand("kv.stats", {}); } }; // src/queue.ts import { Subject, timer, EMPTY, defer } from "rxjs"; import { switchMap, retry, catchError, filter, takeUntil, share, mergeMap } from "rxjs/operators"; var QueueManager = class { constructor(client) { this.client = client; } stopSignals = /* @__PURE__ */ new Map(); /** * Create a new queue */ async createQueue(name, config) { const result = await this.client.sendCommand("queue.create", { name, config: config || {} }); return result.success; } /** * Publish a message to a queue */ async publish(queueName, payload, options) { const payloadBytes = typeof payload === "string" ? Array.from(new TextEncoder().encode(payload)) : Array.from(payload); const cmdPayload = { queue: queueName, payload: payloadBytes }; if (options?.priority !== void 0) { cmdPayload.priority = options.priority; } if (options?.max_retries !== void 0) { cmdPayload.max_retries = options.max_retries; } if (options?.headers) { cmdPayload.headers = options.headers; } const result = await this.client.sendCommand( "queue.publish", cmdPayload ); return result.message_id; } /** * Consume a message from a queue */ async consume(queueName, consumerId) { const result = await this.client.sendCommand( "queue.consume", { queue: queueName, consumer_id: consumerId } ); if (!result.message) { return null; } const message = result.message; if (Array.isArray(message.payload)) { message.payload = new Uint8Array(message.payload); } return message; } /** * Acknowledge message processing (ACK) */ async ack(queueName, messageId) { const result = await this.client.sendCommand("queue.ack", { queue: queueName, message_id: messageId }); return result.success; } /** * Negative acknowledge (NACK) - requeue or send to DLQ */ async nack(queueName, messageId, requeue = true) { const result = await this.client.sendCommand("queue.nack", { queue: queueName, message_id: messageId, requeue }); return result.success; } /** * Get queue statistics */ async stats(queueName) { return this.client.sendCommand("queue.stats", { queue: queueName }); } /** * List all queues */ async listQueues() { const result = await this.client.sendCommand("queue.list", {}); return result.queues; } /** * Purge all messages from a queue */ async purge(queueName) { const result = await this.client.sendCommand("queue.purge", { queue: queueName }); return result.purged; } /** * Delete a queue */ async deleteQueue(queueName) { const result = await this.client.sendCommand("queue.delete", { queue: queueName }); return result.deleted; } /** * Helper: Publish a string message */ async publishString(queueName, message, options) { return this.publish(queueName, message, options); } /** * Helper: Publish a JSON object */ async publishJSON(queueName, data, options) { const json = JSON.stringify(data); return this.publish(queueName, json, options); } /** * Helper: Consume and decode as string */ async consumeString(queueName, consumerId) { const message = await this.consume(queueName, consumerId); if (!message) { return { message: null, text: null }; } const text = new TextDecoder().decode(message.payload); return { message, text }; } /** * Helper: Consume and decode as JSON */ async consumeJSON(queueName, consumerId) { const result = await this.consumeString(queueName, consumerId); if (!result.text) { return { message: null, data: null }; } try { const data = JSON.parse(result.text); return { message: result.message, data }; } catch (error) { throw new Error(`Failed to parse JSON: ${error}`); } } // ==================== REACTIVE METHODS ==================== /** * Create a reactive message consumer as an Observable * * @example * ```typescript * synap.queue.observeMessages({ * queueName: 'tasks', * consumerId: 'worker-1', * pollingInterval: 500, * concurrency: 5 * }).subscribe({ * next: async (msg) => { * await processMessage(msg.data); * await msg.ack(); * }, * error: (err) => console.error('Error:', err) * }); * ``` */ observeMessages(options) { const { queueName, consumerId, pollingInterval = 1e3, concurrency = 1, requeueOnNack = true } = options; const stopKey = `${queueName}:${consumerId}`; const stopSignal = new Subject(); this.stopSignals.set(stopKey, stopSignal); return timer(0, pollingInterval).pipe( takeUntil(stopSignal), mergeMap( async () => { try { const result = await this.consumeJSON(queueName, consumerId); if (!result.message || !result.data) { return null; } const processedMessage = { message: result.message, data: result.data, ack: async () => { await this.ack(queueName, result.message.id); }, nack: async (requeue = requeueOnNack) => { await this.nack(queueName, result.message.id, requeue); } }; return processedMessage; } catch (error) { console.error(`Error consuming from ${queueName}:`, error); return null; } }, concurrency ), filter((msg) => msg !== null), share() ); } /** * Create a reactive message consumer with automatic ACK/NACK handling * * @example * ```typescript * synap.queue.observeMessagesAuto({ * queueName: 'tasks', * consumerId: 'worker-1', * }).subscribe({ * next: async (msg) => { * // Process message - will auto-ACK on success * await processMessage(msg.data); * }, * error: (err) => console.error('Error:', err) * }); * ``` */ observeMessagesAuto(options) { const opts = { ...options, autoAck: true, autoNack: true }; return this.observeMessages(opts); } /** * Create a reactive consumer that processes messages with a handler function * * @example * ```typescript * const subscription = synap.queue.processMessages({ * queueName: 'emails', * consumerId: 'email-worker', * concurrency: 10 * }, async (data) => { * await sendEmail(data); * }).subscribe({ * next: (result) => console.log('Processed:', result), * error: (err) => console.error('Error:', err) * }); * ``` */ processMessages(options, handler) { return this.observeMessages(options).pipe( mergeMap( async (msg) => { try { await handler(msg.data, msg.message); await msg.ack(); return { messageId: msg.message.id, success: true }; } catch (error) { await msg.nack(); return { messageId: msg.message.id, success: false, error }; } }, options.concurrency || 1 ) ); } /** * Stop a reactive consumer * * @param queueName - Queue name * @param consumerId - Consumer ID */ stopConsumer(queueName, consumerId) { const stopKey = `${queueName}:${consumerId}`; const stopSignal = this.stopSignals.get(stopKey); if (stopSignal) { stopSignal.next(); stopSignal.complete(); this.stopSignals.delete(stopKey); } } /** * Stop all reactive consumers */ stopAllConsumers() { this.stopSignals.forEach((signal) => { signal.next(); signal.complete(); }); this.stopSignals.clear(); } /** * Create an observable that emits queue statistics at regular intervals * * @param queueName - Queue name * @param interval - Polling interval in milliseconds (default: 5000) * * @example * ```typescript * synap.queue.observeStats('tasks', 1000).subscribe({ * next: (stats) => console.log('Queue depth:', stats.depth), * }); * ``` */ observeStats(queueName, interval = 5e3) { return timer(0, interval).pipe( switchMap(() => defer(() => this.stats(queueName))), retry({ count: 3, delay: 1e3 }), catchError((error) => { console.error(`Error fetching stats for ${queueName}:`, error); return EMPTY; }), share() ); } }; // src/stream.ts import { Subject as Subject2, timer as timer2, EMPTY as EMPTY2, defer as defer2 } from "rxjs"; import { switchMap as switchMap2, retry as retry2, catchError as catchError2, filter as filter2, takeUntil as takeUntil2, share as share2, map } from "rxjs/operators"; var StreamManager = class { constructor(client) { this.client = client; } stopSignals = /* @__PURE__ */ new Map(); /** * Create a new stream room */ async createRoom(roomName) { const result = await this.client.sendCommand("stream.create", { room: roomName }); return result.success; } /** * Publish an event to a stream room */ async publish(roomName, eventName, data, options) { const payload = { room: roomName, event: eventName, data }; if (options?.metadata) { payload.metadata = options.metadata; } const result = await this.client.sendCommand( "stream.publish", payload ); return result.offset; } /** * Consume events from a stream room (one-time fetch) */ async consume(roomName, subscriberId, fromOffset = 0) { const result = await this.client.sendCommand( "stream.consume", { room: roomName, subscriber_id: subscriberId, from_offset: fromOffset } ); const events = result.events || []; return events.map((event) => ({ ...event, data: this.parseEventData(event.data) })); } /** * Parse event data - handle both byte arrays and objects */ parseEventData(data) { if (Array.isArray(data)) { try { const text = new TextDecoder().decode(new Uint8Array(data)); return JSON.parse(text); } catch (error) { console.error("Failed to parse event data:", error); return data; } } return data; } /** * Get stream room statistics */ async stats(roomName) { return this.client.sendCommand("stream.stats", { room: roomName }); } /** * List all stream rooms */ async listRooms() { const result = await this.client.sendCommand("stream.list", {}); return result.rooms; } /** * Delete a stream room */ async deleteRoom(roomName) { const result = await this.client.sendCommand("stream.delete", { room: roomName }); return !!result.deleted; } // ==================== REACTIVE METHODS ==================== /** * Create a reactive event consumer as an Observable * * @example * ```typescript * synap.stream.observeEvents({ * roomName: 'chat-room', * subscriberId: 'user-123', * fromOffset: 0, * pollingInterval: 500 * }).subscribe({ * next: (event) => { * console.log('Event:', event.event, event.data); * } * }); * ``` */ observeEvents(options) { const { roomName, subscriberId, fromOffset = 0, pollingInterval = 1e3 } = options; const stopKey = `${roomName}:${subscriberId}`; const stopSignal = new Subject2(); this.stopSignals.set(stopKey, stopSignal); let currentOffset = fromOffset; return timer2(0, pollingInterval).pipe( takeUntil2(stopSignal), switchMap2(async () => { try { const events = await this.consume(roomName, subscriberId, currentOffset); if (events.length > 0) { currentOffset = events[events.length - 1].offset + 1; } return events; } catch (error) { console.error(`Error consuming from stream ${roomName}:`, error); return []; } }), // Flatten array of events into individual emissions switchMap2((events) => events), map((event) => ({ offset: event.offset, event: event.event, data: event.data, timestamp: event.timestamp })), share2() ); } /** * Create a reactive consumer that filters by event name * * @example * ```typescript * synap.stream.observeEvent({ * roomName: 'notifications', * subscriberId: 'user-1', * eventName: 'user.created' * }).subscribe({ * next: (event) => console.log('User created:', event.data) * }); * ``` */ observeEvent(options) { const { eventName, ...consumeOptions } = options; return this.observeEvents(consumeOptions).pipe( filter2((event) => event.event === eventName) ); } /** * Stop a reactive consumer * * @param roomName - Room name * @param subscriberId - Subscriber ID */ stopConsumer(roomName, subscriberId) { const stopKey = `${roomName}:${subscriberId}`; const stopSignal = this.stopSignals.get(stopKey); if (stopSignal) { stopSignal.next(); stopSignal.complete(); this.stopSignals.delete(stopKey); } } /** * Stop all reactive consumers */ stopAllConsumers() { this.stopSignals.forEach((signal) => { signal.next(); signal.complete(); }); this.stopSignals.clear(); } /** * Create an observable that emits stream statistics at regular intervals * * @param roomName - Room name * @param interval - Polling interval in milliseconds (default: 5000) * * @example * ```typescript * synap.stream.observeStats('chat-room', 3000).subscribe({ * next: (stats) => console.log('Events:', stats.max_offset), * }); * ``` */ observeStats(roomName, interval = 5e3) { return timer2(0, interval).pipe( switchMap2(() => defer2(() => this.stats(roomName))), retry2({ count: 3, delay: 1e3 }), catchError2((error) => { console.error(`Error fetching stats for ${roomName}:`, error); return EMPTY2; }), share2() ); } /** * Helper: Publish a typed event */ async publishEvent(roomName, eventName, data, options) { return this.publish(roomName, eventName, data, options); } }; // src/pubsub.ts import { Subject as Subject3 } from "rxjs"; import { share as share3, takeUntil as takeUntil3 } from "rxjs/operators"; var PubSubManager = class { constructor(client) { this.client = client; } subscriptions = /* @__PURE__ */ new Map(); /** * Publish a message to a topic */ async publish(topic, data, options) { const payload = { topic, data }; if (options?.priority !== void 0) { payload.priority = options.priority; } if (options?.headers) { payload.headers = options.headers; } const result = await this.client.sendCommand( "pubsub.publish", payload ); return result.success; } /** * Helper: Publish a typed message */ async publishMessage(topic, data, options) { return this.publish(topic, data, options); } // ==================== REACTIVE METHODS ==================== /** * Create a reactive pub/sub subscriber as an Observable * * Note: This method creates a simulated reactive subscription using polling. * For real-time WebSocket-based subscriptions, consider using the WebSocket API directly. * * @example * ```typescript * synap.pubsub.subscribe({ * topics: ['user.created', 'user.updated'], * subscriberId: 'subscriber-1' * }).subscribe({ * next: (message) => { * console.log('Topic:', message.topic); * console.log('Data:', message.data); * } * }); * ``` */ subscribe(options) { const { topics, subscriberId = `subscriber-${Date.now()}` } = options; const stopKey = `${subscriberId}:${topics.join(",")}`; const stopSignal = new Subject3(); this.subscriptions.set(stopKey, stopSignal); const messageSubject = new Subject3(); const setupSubscription = async () => { try { await this.client.sendCommand("pubsub.subscribe", { topics, subscriber_id: subscriberId }); } catch (error) { console.error(`Error subscribing to topics ${topics.join(", ")}:`, error); messageSubject.error(error); } }; setupSubscription(); return messageSubject.pipe( takeUntil3(stopSignal), share3() ); } /** * Subscribe to a single topic with reactive pattern * * @example * ```typescript * synap.pubsub.subscribeTopic('user.created').subscribe({ * next: (message) => console.log('User created:', message.data) * }); * ``` */ subscribeTopic(topic, subscriberId) { return this.subscribe({ topics: [topic], subscriberId }); } /** * Stop a reactive subscription * * @param subscriberId - Subscriber ID * @param topics - Topics that were subscribed to */ unsubscribe(subscriberId, topics) { const stopKey = `${subscriberId}:${topics.join(",")}`; const stopSignal = this.subscriptions.get(stopKey); if (stopSignal) { stopSignal.next(); stopSignal.complete(); this.subscriptions.delete(stopKey); } this.client.sendCommand("pubsub.unsubscribe", { subscriber_id: subscriberId, topics }).catch((error) => { console.error(`Error unsubscribing from topics:`, error); }); } /** * Stop all reactive subscriptions */ unsubscribeAll() { this.subscriptions.forEach((signal) => { signal.next(); signal.complete(); }); this.subscriptions.clear(); } /** * Get statistics for a topic (if supported by server) */ async stats(topic) { return this.client.sendCommand("pubsub.stats", { topic }); } /** * List all active topics */ async listTopics() { const result = await this.client.sendCommand("pubsub.list", {}); return result.topics; } }; // src/index.ts var Synap = class { client; /** Key-Value store operations */ kv; /** Queue system operations */ queue; /** Event Stream operations */ stream; /** Pub/Sub operations */ pubsub; constructor(options = {}) { this.client = new SynapClient(options); this.kv = new KVStore(this.client); this.queue = new QueueManager(this.client); this.stream = new StreamManager(this.client); this.pubsub = new PubSubManager(this.client); } /** * Ping the server */ async ping() { return this.client.ping(); } /** * Get server health */ async health() { return this.client.health(); } /** * Close the client */ close() { this.client.close(); } /** * Get the underlying HTTP client (for advanced usage) */ getClient() { return this.client; } }; var index_default = Synap; export { KVStore, NetworkError, PubSubManager, QueueManager, ServerError, StreamManager, Synap, SynapClient, SynapError, TimeoutError, index_default as default };