@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.
- package/dist/client.d.ts +2 -0
- package/dist/client.js +77 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -0
- package/dist/transports/http.d.ts +14 -0
- package/dist/transports/http.js +63 -0
- package/dist/transports/websocket.d.ts +16 -0
- package/dist/transports/websocket.js +91 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.js +3 -0
- package/package.json +26 -0
package/dist/client.d.ts
ADDED
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
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
|
+
}
|