@inf-minds/relay-client 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ import type { StreamClient, StreamClientOptions } from './types.js';
2
+ export declare function createStreamClient(options: StreamClientOptions): StreamClient;
package/dist/client.js ADDED
@@ -0,0 +1,77 @@
1
+ // ABOUTME: Stream client implementation
2
+ // ABOUTME: Provides unified interface over transports with auto-fallback
3
+ import { OFFSET_BEGINNING } from '@inf-minds/streams-protocol';
4
+ import { createHTTPTransport } from './transports/http.js';
5
+ import { createWebSocketTransport } from './transports/websocket.js';
6
+ export function createStreamClient(options) {
7
+ const { baseUrl, auth, transport = 'auto' } = options;
8
+ const getToken = auth?.getToken ?? (auth?.token ? async () => auth.token : undefined);
9
+ // Select transport based on option
10
+ function selectTransport(preferred) {
11
+ switch (preferred) {
12
+ case 'websocket':
13
+ return createWebSocketTransport({ baseUrl, getToken });
14
+ case 'sse':
15
+ // SSE uses HTTP transport with ?live=sse
16
+ return createHTTPTransport({ baseUrl, getToken });
17
+ case 'http':
18
+ case 'auto':
19
+ default:
20
+ return createHTTPTransport({ baseUrl, getToken });
21
+ }
22
+ }
23
+ const activeTransport = selectTransport(transport);
24
+ // Connect WebSocket if using that transport
25
+ if (transport === 'websocket') {
26
+ activeTransport.connect?.();
27
+ }
28
+ return {
29
+ subscribe(streamId, opts = {}) {
30
+ const { offset: initialOffset, onEvent, persistOffset, loadOffset, } = opts;
31
+ let currentOffset = initialOffset ?? OFFSET_BEGINNING;
32
+ let status = {
33
+ upToDate: false,
34
+ closed: false,
35
+ offset: currentOffset,
36
+ };
37
+ // Load persisted offset if available
38
+ if (loadOffset) {
39
+ const loaded = loadOffset();
40
+ if (loaded instanceof Promise) {
41
+ loaded.then((o) => {
42
+ if (o)
43
+ currentOffset = o;
44
+ });
45
+ }
46
+ else if (loaded) {
47
+ currentOffset = loaded;
48
+ }
49
+ }
50
+ const unsubscribe = activeTransport.subscribe(streamId, currentOffset, (event, newOffset) => {
51
+ currentOffset = newOffset;
52
+ status.offset = newOffset;
53
+ if (onEvent) {
54
+ onEvent(event, newOffset);
55
+ }
56
+ if (persistOffset) {
57
+ const result = persistOffset(newOffset);
58
+ if (result instanceof Promise) {
59
+ result.catch((err) => console.error('Failed to persist offset:', err));
60
+ }
61
+ }
62
+ });
63
+ return {
64
+ get status() {
65
+ return status;
66
+ },
67
+ get offset() {
68
+ return currentOffset;
69
+ },
70
+ unsubscribe,
71
+ };
72
+ },
73
+ close() {
74
+ activeTransport.close();
75
+ },
76
+ };
77
+ }
@@ -0,0 +1,4 @@
1
+ export type { StreamClient, StreamClientOptions, StreamSubscription, SubscribeOptions, StreamStatus, TransportType, ConnectionStatus, } from './types.js';
2
+ export { createStreamClient } from './client.js';
3
+ export { createHTTPTransport } from './transports/http.js';
4
+ export { createWebSocketTransport } from './transports/websocket.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ // ABOUTME: Relay client entry point
2
+ // ABOUTME: Exports createStreamClient and types
3
+ export { createStreamClient } from './client.js';
4
+ // Export transports for advanced use
5
+ export { createHTTPTransport } from './transports/http.js';
6
+ export { createWebSocketTransport } from './transports/websocket.js';
@@ -0,0 +1,14 @@
1
+ export interface HTTPTransportOptions {
2
+ baseUrl: string;
3
+ getToken?: () => Promise<string>;
4
+ }
5
+ export interface EventHandler {
6
+ (event: unknown, offset: string): void;
7
+ }
8
+ export interface Unsubscribe {
9
+ (): void;
10
+ }
11
+ export declare function createHTTPTransport(options: HTTPTransportOptions): {
12
+ subscribe(streamId: string, offset: string, handler: EventHandler): Unsubscribe;
13
+ close(): void;
14
+ };
@@ -0,0 +1,63 @@
1
+ // ABOUTME: HTTP long-poll transport implementation
2
+ // ABOUTME: Polls /v1/streams endpoint with long-poll mode
3
+ import { parseStreamHeaders, } from '@inf-minds/streams-protocol';
4
+ export function createHTTPTransport(options) {
5
+ const subscriptions = new Map();
6
+ async function poll(streamId) {
7
+ const sub = subscriptions.get(streamId);
8
+ if (!sub || !sub.active)
9
+ return;
10
+ try {
11
+ const headers = {};
12
+ if (options.getToken) {
13
+ headers['Authorization'] = `Bearer ${await options.getToken()}`;
14
+ }
15
+ const url = `${options.baseUrl}/v1/streams/${streamId}?offset=${sub.offset}&live=long-poll`;
16
+ const response = await fetch(url, { headers });
17
+ if (!response.ok) {
18
+ throw new Error(`HTTP ${response.status}`);
19
+ }
20
+ const streamHeaders = parseStreamHeaders(response.headers);
21
+ const events = await response.json();
22
+ for (const event of events) {
23
+ sub.handler(event, streamHeaders.nextOffset);
24
+ }
25
+ sub.offset = streamHeaders.nextOffset;
26
+ // Check if stream is closed
27
+ if (streamHeaders.closed) {
28
+ sub.active = false;
29
+ return;
30
+ }
31
+ // Continue polling
32
+ if (sub.active) {
33
+ setTimeout(() => poll(streamId), 100);
34
+ }
35
+ }
36
+ catch (error) {
37
+ console.error('HTTP transport error:', error);
38
+ // Retry after delay
39
+ if (sub.active) {
40
+ setTimeout(() => poll(streamId), 3000);
41
+ }
42
+ }
43
+ }
44
+ return {
45
+ subscribe(streamId, offset, handler) {
46
+ subscriptions.set(streamId, { offset, handler, active: true });
47
+ poll(streamId);
48
+ return () => {
49
+ const sub = subscriptions.get(streamId);
50
+ if (sub) {
51
+ sub.active = false;
52
+ }
53
+ subscriptions.delete(streamId);
54
+ };
55
+ },
56
+ close() {
57
+ for (const sub of subscriptions.values()) {
58
+ sub.active = false;
59
+ }
60
+ subscriptions.clear();
61
+ },
62
+ };
63
+ }
@@ -0,0 +1,16 @@
1
+ export interface WebSocketTransportOptions {
2
+ baseUrl: string;
3
+ getToken?: () => Promise<string>;
4
+ }
5
+ export interface EventHandler {
6
+ (event: unknown, offset: string): void;
7
+ }
8
+ export interface Unsubscribe {
9
+ (): void;
10
+ }
11
+ export declare function createWebSocketTransport(options: WebSocketTransportOptions): {
12
+ connect(): Promise<void>;
13
+ subscribe(streamId: string, offset: string, handler: EventHandler): Unsubscribe;
14
+ close(): void;
15
+ readonly ws: WebSocket | null;
16
+ };
@@ -0,0 +1,91 @@
1
+ // ABOUTME: WebSocket transport implementation
2
+ // ABOUTME: Manages subscriptions and real-time event delivery
3
+ import { serializeWSMessage, parseWSServerMessage, } from '@inf-minds/streams-protocol';
4
+ export function createWebSocketTransport(options) {
5
+ let ws = null;
6
+ const subscriptions = new Map();
7
+ const pendingSubscribes = [];
8
+ function getWsUrl() {
9
+ const url = new URL(options.baseUrl);
10
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
11
+ url.pathname = '/ws';
12
+ return url.toString();
13
+ }
14
+ function send(msg) {
15
+ if (ws && ws.readyState === WebSocket.OPEN) {
16
+ ws.send(serializeWSMessage(msg));
17
+ }
18
+ }
19
+ function handleMessage(data) {
20
+ const msg = parseWSServerMessage(data);
21
+ if (!msg)
22
+ return;
23
+ switch (msg.type) {
24
+ case 'subscribed':
25
+ // Subscription confirmed
26
+ break;
27
+ case 'event':
28
+ const handler = subscriptions.get(msg.streamId);
29
+ if (handler) {
30
+ handler(msg.event, msg.nextOffset);
31
+ }
32
+ break;
33
+ case 'status':
34
+ // Could trigger status callback
35
+ break;
36
+ case 'error':
37
+ console.error('WebSocket error:', msg.message);
38
+ break;
39
+ case 'pong':
40
+ // Keepalive response
41
+ break;
42
+ }
43
+ }
44
+ return {
45
+ async connect() {
46
+ return new Promise((resolve, reject) => {
47
+ ws = new WebSocket(getWsUrl());
48
+ ws.onopen = () => {
49
+ // Send pending subscribes
50
+ for (const { streamId, offset } of pendingSubscribes) {
51
+ send({ type: 'subscribe', streamId, offset });
52
+ }
53
+ pendingSubscribes.length = 0;
54
+ resolve();
55
+ };
56
+ ws.onmessage = (event) => {
57
+ handleMessage(event.data);
58
+ };
59
+ ws.onerror = (error) => {
60
+ reject(error);
61
+ };
62
+ ws.onclose = () => {
63
+ ws = null;
64
+ };
65
+ });
66
+ },
67
+ subscribe(streamId, offset, handler) {
68
+ subscriptions.set(streamId, handler);
69
+ if (ws && ws.readyState === WebSocket.OPEN) {
70
+ send({ type: 'subscribe', streamId, offset });
71
+ }
72
+ else {
73
+ pendingSubscribes.push({ streamId, offset });
74
+ }
75
+ return () => {
76
+ subscriptions.delete(streamId);
77
+ send({ type: 'unsubscribe', streamId });
78
+ };
79
+ },
80
+ close() {
81
+ if (ws) {
82
+ ws.close();
83
+ ws = null;
84
+ }
85
+ subscriptions.clear();
86
+ },
87
+ get ws() {
88
+ return ws;
89
+ },
90
+ };
91
+ }
@@ -0,0 +1,34 @@
1
+ export type TransportType = 'websocket' | 'sse' | 'http' | 'auto';
2
+ export type ConnectionStatus = 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'error';
3
+ export interface StreamClientOptions {
4
+ baseUrl: string;
5
+ auth?: {
6
+ token?: string;
7
+ getToken?: () => Promise<string>;
8
+ };
9
+ transport?: TransportType;
10
+ onConnectionChange?: (status: ConnectionStatus) => void;
11
+ }
12
+ export interface SubscribeOptions<T> {
13
+ offset?: string;
14
+ onEvent?: (event: T, offset: string) => void;
15
+ onStatus?: (status: StreamStatus) => void;
16
+ onError?: (error: Error) => void;
17
+ onReconnect?: (offset: string) => void;
18
+ persistOffset?: (offset: string) => void | Promise<void>;
19
+ loadOffset?: () => string | undefined | Promise<string | undefined>;
20
+ }
21
+ export interface StreamStatus {
22
+ upToDate: boolean;
23
+ closed: boolean;
24
+ offset: string;
25
+ }
26
+ export interface StreamSubscription<T> {
27
+ readonly status: StreamStatus;
28
+ readonly offset: string;
29
+ unsubscribe(): void;
30
+ }
31
+ export interface StreamClient {
32
+ subscribe<T>(streamId: string, options?: SubscribeOptions<T>): StreamSubscription<T>;
33
+ close(): void;
34
+ }
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ // ABOUTME: Type definitions for relay client
2
+ // ABOUTME: Defines client options, subscription, and callbacks
3
+ export {};
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@inf-minds/relay-client",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": ["dist"],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "test": "vitest run",
17
+ "typecheck": "tsc --noEmit"
18
+ },
19
+ "dependencies": {
20
+ "@inf-minds/streams-protocol": "workspace:*"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.7.0",
24
+ "vitest": "^2.1.0"
25
+ }
26
+ }