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