feat(sdk): P3.4 @insignia/iios-inbox-web + kernel-client inbox REST
RestClient.listInboxItems/patchInboxItem + InboxItem type; iios-inbox-web InboxProvider + useInbox (poll + snooze/done/reopen). Boundary allowlist extended. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@insignia/iios-inbox-web",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
|
||||
"files": ["dist"],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@insignia/iios-kernel-client": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"react": "^19.0.0",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { InboxProvider, useInbox } from './react';
|
||||
export type { InboxItem, InboxState } from '@insignia/iios-kernel-client';
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { RestClient, type InboxItem, type InboxState } from '@insignia/iios-kernel-client';
|
||||
|
||||
const InboxContext = createContext<RestClient | null>(null);
|
||||
|
||||
/** Provides an inbox REST client to the inbox hooks. */
|
||||
export function InboxProvider({
|
||||
serviceUrl,
|
||||
token,
|
||||
children,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
token: string;
|
||||
children: React.ReactNode;
|
||||
}): React.ReactElement {
|
||||
const client = useMemo(() => new RestClient({ serviceUrl, token }), [serviceUrl, token]);
|
||||
return <InboxContext.Provider value={client}>{children}</InboxContext.Provider>;
|
||||
}
|
||||
|
||||
function useClient(): RestClient {
|
||||
const c = useContext(InboxContext);
|
||||
if (!c) throw new Error('useInbox must be used within <InboxProvider>');
|
||||
return c;
|
||||
}
|
||||
|
||||
/** The caller's inbox items + actions. Polls on an interval (default 3s). */
|
||||
export function useInbox(opts?: { state?: InboxState; pollMs?: number }): {
|
||||
items: InboxItem[];
|
||||
refresh: () => Promise<void>;
|
||||
snooze: (id: string) => Promise<void>;
|
||||
done: (id: string) => Promise<void>;
|
||||
reopen: (id: string) => Promise<void>;
|
||||
} {
|
||||
const client = useClient();
|
||||
const [items, setItems] = useState<InboxItem[]>([]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
setItems(await client.listInboxItems(opts?.state));
|
||||
} catch {
|
||||
/* transient — keep last items */
|
||||
}
|
||||
}, [client, opts?.state]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
const t = setInterval(() => void refresh(), opts?.pollMs ?? 3000);
|
||||
return () => clearInterval(t);
|
||||
}, [refresh, opts?.pollMs]);
|
||||
|
||||
const patch = async (id: string, state: InboxState): Promise<void> => {
|
||||
await client.patchInboxItem(id, { state });
|
||||
await refresh();
|
||||
};
|
||||
|
||||
return {
|
||||
items,
|
||||
refresh,
|
||||
snooze: (id) => patch(id, 'SNOOZED'),
|
||||
done: (id) => patch(id, 'DONE'),
|
||||
reopen: (id) => patch(id, 'OPEN'),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"jsx": "react-jsx",
|
||||
"types": ["react"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
external: ['react', 'react-dom', '@insignia/iios-kernel-client'],
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IngestInteractionRequest } from '@insignia/iios-contracts';
|
||||
import type { Message } from './types';
|
||||
import type { Message, InboxItem, InboxState } from './types';
|
||||
|
||||
export interface RestConfig {
|
||||
serviceUrl: string;
|
||||
@@ -40,6 +40,23 @@ export class RestClient {
|
||||
return (await r.json()) as Message;
|
||||
}
|
||||
|
||||
async listInboxItems(state?: InboxState): Promise<InboxItem[]> {
|
||||
const q = state ? `?state=${encodeURIComponent(state)}` : '';
|
||||
const r = await fetch(this.url(`/v1/inbox/items${q}`), { headers: this.headers() });
|
||||
if (!r.ok) throw new Error(`listInboxItems ${r.status}`);
|
||||
return (await r.json()) as InboxItem[];
|
||||
}
|
||||
|
||||
async patchInboxItem(id: string, body: { state: InboxState; reason?: string }): Promise<InboxItem> {
|
||||
const r = await fetch(this.url(`/v1/inbox/items/${id}`), {
|
||||
method: 'PATCH',
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!r.ok) throw new Error(`patchInboxItem ${r.status}`);
|
||||
return (await r.json()) as InboxItem;
|
||||
}
|
||||
|
||||
async ingest(body: IngestInteractionRequest, idempotencyKey: string): Promise<unknown> {
|
||||
const r = await fetch(this.url('/v1/interactions/ingest'), {
|
||||
method: 'POST',
|
||||
|
||||
@@ -32,6 +32,23 @@ export interface MessageEvents {
|
||||
typing: (e: TypingEvent) => void;
|
||||
}
|
||||
|
||||
export type InboxState = 'OPEN' | 'SNOOZED' | 'DONE' | 'ARCHIVED' | 'CANCELLED' | 'STALE';
|
||||
|
||||
export interface InboxItem {
|
||||
id: string;
|
||||
ownerActorId: string;
|
||||
kind: string;
|
||||
state: InboxState;
|
||||
title: string;
|
||||
summary?: string | null;
|
||||
priority: string;
|
||||
threadId?: string | null;
|
||||
sourceInteractionId?: string | null;
|
||||
traceId?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** Minimal socket surface so the facade can be unit-tested with a fake. */
|
||||
export interface SocketLike {
|
||||
on(event: string, handler: (...args: unknown[]) => void): unknown;
|
||||
|
||||
Generated
+19
@@ -51,6 +51,25 @@ importers:
|
||||
|
||||
packages/iios-contracts: {}
|
||||
|
||||
packages/iios-inbox-web:
|
||||
dependencies:
|
||||
'@insignia/iios-kernel-client':
|
||||
specifier: workspace:*
|
||||
version: link:../iios-kernel-client
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.17
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.7
|
||||
tsup:
|
||||
specifier: ^8.3.5
|
||||
version: 8.5.1(jiti@2.7.0)(postcss@8.5.16)(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.7.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/iios-kernel-client:
|
||||
dependencies:
|
||||
'@insignia/iios-contracts':
|
||||
|
||||
@@ -19,6 +19,7 @@ const ALLOWED = {
|
||||
'@insignia/iios-kernel-client': ['@insignia/iios-contracts'],
|
||||
'@insignia/iios-service': ['@insignia/iios-contracts', '@insignia/iios-testkit'],
|
||||
'@insignia/iios-message-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'],
|
||||
'@insignia/iios-inbox-web': ['@insignia/iios-contracts', '@insignia/iios-kernel-client'],
|
||||
};
|
||||
const KNOWN = Object.keys(ALLOWED);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user