feat(sdk): P2.5 @insignia/iios-kernel-client — REST + socket facade
MessageSocket (open/send/read/typing via emitWithAck, reconnect re-opens thread, no socket.io types leak) + RestClient (ingest/getThreadMessages/sendMessage). tsup ESM+dts. Facade unit test with injectable fake socket (3 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { io } from 'socket.io-client';
|
||||
import type { Message, OpenThreadResult, MessageEvents, SocketLike } from './types';
|
||||
|
||||
export interface MessageSocketConfig {
|
||||
serviceUrl: string;
|
||||
token: string;
|
||||
autoConnect?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Framework-agnostic facade over the `/message` Socket.io namespace (ports the
|
||||
* support-sdk MessageClient). No socket.io types leak out; RPCs use emitWithAck.
|
||||
* On reconnect it re-opens the current thread so subscriptions resume with no
|
||||
* lost messages (the docs' disconnect/reconnect requirement).
|
||||
*/
|
||||
export class MessageSocket {
|
||||
private readonly socket: SocketLike;
|
||||
private currentThreadId: string | null = null;
|
||||
|
||||
constructor(config: MessageSocketConfig, socket?: SocketLike) {
|
||||
this.socket =
|
||||
socket ??
|
||||
(io(`${config.serviceUrl.replace(/\/$/, '')}/message`, {
|
||||
auth: { token: config.token },
|
||||
transports: ['websocket'],
|
||||
autoConnect: config.autoConnect ?? true,
|
||||
}) as unknown as SocketLike);
|
||||
|
||||
// Re-open the active thread after a reconnect.
|
||||
this.socket.on('connect', () => {
|
||||
if (this.currentThreadId) void this.socket.emitWithAck('open_thread', { threadId: this.currentThreadId });
|
||||
});
|
||||
}
|
||||
|
||||
connect(): void {
|
||||
this.socket.connect();
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this.socket.disconnect();
|
||||
}
|
||||
|
||||
/** Subscribe to a server event; returns an unsubscribe fn. */
|
||||
on<E extends keyof MessageEvents>(event: E, handler: MessageEvents[E]): () => void {
|
||||
const fn = handler as (...args: unknown[]) => void;
|
||||
this.socket.on(event, fn);
|
||||
return () => this.socket.off(event, fn);
|
||||
}
|
||||
|
||||
async openThread(threadId?: string): Promise<OpenThreadResult> {
|
||||
const result = (await this.socket.emitWithAck('open_thread', { threadId })) as OpenThreadResult;
|
||||
this.currentThreadId = result.threadId;
|
||||
return result;
|
||||
}
|
||||
|
||||
async sendMessage(threadId: string, content: string, opts?: { contentRef?: string }): Promise<Message> {
|
||||
return (await this.socket.emitWithAck('send_message', {
|
||||
threadId,
|
||||
content,
|
||||
contentRef: opts?.contentRef,
|
||||
})) as Message;
|
||||
}
|
||||
|
||||
async markRead(threadId: string, interactionId: string): Promise<{ ok: boolean }> {
|
||||
return (await this.socket.emitWithAck('read', { threadId, interactionId })) as { ok: boolean };
|
||||
}
|
||||
|
||||
typing(threadId: string): void {
|
||||
this.socket.emit('typing', { threadId });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user