feat(p6): kernel-client route methods + @insignia/iios-community-web SDK

Task 6.5: add createBinding/listBindings/simulateRoute/listRouteDecisions/
approveDecision/denyDecision to RestClient + RouteBinding/RouteDecision types.
New @insignia/iios-community-web package (CommunityProvider, useBindings,
useRoutePreview, useRouteDecisions). Boundary allowlist: community-web ->
contracts + kernel-client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 15:12:50 +05:30
parent 095ea869aa
commit 1ee737c025
9 changed files with 210 additions and 1 deletions
+2
View File
@@ -0,0 +1,2 @@
export { CommunityProvider, useBindings, useRoutePreview, useRouteDecisions } from './react';
export type { RouteBinding, RouteDecision } from '@insignia/iios-kernel-client';
+88
View File
@@ -0,0 +1,88 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { RestClient, type RouteBinding, type RouteDecision } from '@insignia/iios-kernel-client';
const CommunityContext = createContext<RestClient | null>(null);
export function CommunityProvider({
serviceUrl,
token,
children,
}: {
serviceUrl: string;
token: string;
children: React.ReactNode;
}): React.ReactElement {
const client = useMemo(() => new RestClient({ serviceUrl, token }), [serviceUrl, token]);
return <CommunityContext.Provider value={client}>{children}</CommunityContext.Provider>;
}
function useClient(): RestClient {
const c = useContext(CommunityContext);
if (!c) throw new Error('community hooks must be used within <CommunityProvider>');
return c;
}
/** Manage route bindings (origin→destination rules). */
export function useBindings(): {
bindings: RouteBinding[];
refresh: () => Promise<void>;
createBinding: (input: Partial<RouteBinding>) => Promise<RouteBinding>;
} {
const client = useClient();
const [bindings, setBindings] = useState<RouteBinding[]>([]);
const refresh = useCallback(async () => {
try {
setBindings(await client.listBindings());
} catch {
/* transient */
}
}, [client]);
useEffect(() => {
void refresh();
}, [refresh]);
const createBinding = async (input: Partial<RouteBinding>) => {
const b = await client.createBinding(input);
await refresh();
return b;
};
return { bindings, refresh, createBinding };
}
/** Preview-first: simulate an interaction against the bindings (no send). */
export function useRoutePreview(): (interactionId: string, originChannelType: string, originRef?: string) => Promise<RouteDecision[]> {
const client = useClient();
return async (interactionId, originChannelType, originRef) =>
(await client.simulateRoute({ interactionId, originChannelType, originRef })).decisions;
}
/** Pending decisions + admin approve/deny. */
export function useRouteDecisions(opts?: { state?: string; pollMs?: number }): {
decisions: RouteDecision[];
refresh: () => Promise<void>;
approve: (id: string) => Promise<void>;
deny: (id: string) => Promise<void>;
} {
const client = useClient();
const [decisions, setDecisions] = useState<RouteDecision[]>([]);
const refresh = useCallback(async () => {
try {
setDecisions(await client.listRouteDecisions(opts?.state));
} catch {
/* transient */
}
}, [client, opts?.state]);
useEffect(() => {
void refresh();
const t = setInterval(() => void refresh(), opts?.pollMs ?? 3000);
return () => clearInterval(t);
}, [refresh, opts?.pollMs]);
const approve = async (id: string) => {
await client.approveDecision(id);
await refresh();
};
const deny = async (id: string) => {
await client.denyDecision(id);
await refresh();
};
return { decisions, refresh, approve, deny };
}