feat: channels on the direct-IIOS path — kernel-client + KernelClientAdapter
- iios-kernel-client: RestClient.discoverThreads (GET /v1/threads/discover) + leaveThread (DELETE /v1/threads/:id/me); DiscoveredThread type - KernelClientAdapter implements the four channel methods: browseChannels maps discovered public channels → ChannelSummary; createChannel → createThread (membership=channel, ADMIN, visibility/topic metadata); joinChannel → socket.openThread (governed public self-join); leaveChannel → rest.leaveThread - adapter channel test over the fake transport (13 kernel-adapter tests); 57 green Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
import { MessageSocket, RestClient } from '@insignia/iios-kernel-client';
|
||||
import type {
|
||||
AnnotationEvent,
|
||||
DiscoveredThread,
|
||||
Message as KernelMessage,
|
||||
MessageEvents,
|
||||
OpenThreadResult,
|
||||
@@ -17,7 +18,10 @@ import type {
|
||||
} from '@insignia/iios-kernel-client';
|
||||
import type { MessagingAdapter } from '../adapter';
|
||||
import type {
|
||||
ChannelSummary,
|
||||
ChannelVisibility,
|
||||
Conversation,
|
||||
CreateChannelInput,
|
||||
Membership,
|
||||
Message,
|
||||
MessageEvent,
|
||||
@@ -41,6 +45,8 @@ export interface RestPort {
|
||||
listThreads(filter?: { metadata?: Record<string, string> }): Promise<ThreadSummary[]>;
|
||||
createThread(opts: { membership?: string; creatorRole?: string; subject?: string; metadata?: Record<string, unknown> }): Promise<{ threadId: string }>;
|
||||
addParticipant(threadId: string, userId: string): Promise<void>;
|
||||
discoverThreads(filter?: { metadata?: Record<string, string> }): Promise<DiscoveredThread[]>;
|
||||
leaveThread(threadId: string): Promise<{ threadId: string; participantCount: number }>;
|
||||
}
|
||||
|
||||
export interface KernelClientAdapterConfig {
|
||||
@@ -165,6 +171,43 @@ export class KernelClientAdapter implements MessagingAdapter {
|
||||
await this.socket.markRead(threadId, messageId);
|
||||
}
|
||||
|
||||
// ── Channels ────────────────────────────────────────────────────
|
||||
async browseChannels(): Promise<ChannelSummary[]> {
|
||||
const found = await this.rest.discoverThreads({ metadata: { membership: 'channel', visibility: 'public' } });
|
||||
return found.map((d) => {
|
||||
const bag = (d.metadata as { topic?: string; visibility?: string } | null) ?? {};
|
||||
const visibility: ChannelVisibility = bag.visibility === 'private' ? 'private' : 'public';
|
||||
return {
|
||||
threadId: d.threadId,
|
||||
name: d.subject ?? 'channel',
|
||||
topic: bag.topic ?? null,
|
||||
visibility,
|
||||
memberCount: d.participantCount,
|
||||
joined: d.joined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async createChannel(input: CreateChannelInput): Promise<{ threadId: string }> {
|
||||
return this.rest.createThread({
|
||||
membership: 'channel',
|
||||
creatorRole: 'ADMIN',
|
||||
subject: input.name,
|
||||
metadata: { visibility: input.visibility, ...(input.topic ? { topic: input.topic } : {}) },
|
||||
});
|
||||
}
|
||||
|
||||
async joinChannel(threadId: string): Promise<void> {
|
||||
// Self-join is a governed open_thread; OPA allows it for a public channel.
|
||||
await this.socket.openThread(threadId);
|
||||
this.joined.add(threadId);
|
||||
}
|
||||
|
||||
async leaveChannel(threadId: string): Promise<void> {
|
||||
await this.rest.leaveThread(threadId);
|
||||
this.joined.delete(threadId);
|
||||
}
|
||||
|
||||
/** Detach socket handlers. Not part of the contract — call on teardown to avoid leaks. */
|
||||
close(): void {
|
||||
for (const off of this.offs) off();
|
||||
|
||||
Reference in New Issue
Block a user