mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 07:03:18 +00:00
Let plugins stream live events and binary data to the app (#8527)
* feat(plugins): stream events and bytes between plugins and the app
Plugins can declare a subscription with defineSubscription, handle it with
server.handle(contract, (input, { push, signal })), and listen from a surface
with useSubscription or client.subscribe. A top-level Uint8Array in an event,
an RPC output, or an RPC input crosses the wire as a binary frame (opcodes
0x20-0x2f) without JSON or base64; a nested one fails validation on the sender.
Each subscriber runs its own handler, owned by the session that subscribed,
so release, disconnect, plugin stop and reload need no plugin code. The
runtime buffers pushes until the subscribe response has gone out, so a client
never sees an event for a subscription it does not know yet.
Gated on server_info.features.pluginSubscriptions; binary outputs reach only
clients that advertise plugin_binary_payloads.
* fix(plugins): close subscription gaps found in review
- Losing daemon.manage now releases the session's plugin subscriptions;
binary events bypass the outbound permission check, so streams would
otherwise outlive the revocation.
- A subscribe that times out sends unsubscribe, since the plugin may
still start the handler after the daemon gave up.
- The byte limit leaves room for the largest frame header and routing,
and lives in assertPluginWireValue so events, outputs and inputs share it.
- A release right after the host accepted keeps `ready` resolved, so a
handler that returns at once reports closed rather than an error.
- Session trace logging tolerates loggers without isLevelEnabled.
* fix(plugins): withhold binary plugin results after plugin access is revoked
A binary RPC output or event skipped the outbound permission check its
JSON form passes, so an RPC in flight when daemon.manage was revoked still
delivered its bytes. Each frame now checks the permission of the JSON
message it stands in for.
* test(plugins): cover subscriptions in a real browser instead of JSDOM
The app subscription tests mounted hooks in JSDOM, which docs/testing.md
rules out. The adapter and contract behavior is now a plain unit test,
and a Playwright spec drives the diff-watch and media-stream examples:
JSON events following file changes, streamed bytes, and an upload.
* test(plugins): add the plain subscription unit test and the browser spec
* fix(plugins): apply subscription schema transforms once
Both ends parsed each subscription value, and the sender sent its parse
output, so the receiver parsed a schema's output as fresh input. An event
schema like z.string().transform((s) => s.length) closed the stream as
invalid. The sender now validates and sends the value as given; the
receiver's parse produces what the handler or callback sees.
* fix(plugins): parse a subscription's input once in the plugin process
server.handle wrapped subscription handlers in a second input parse, so a
transforming input schema rejected its own parsed output before the
handler ran.
This commit is contained in:
@@ -323,3 +323,68 @@ describe("agent session reload SDK boundary", () => {
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin subscription SDK boundary", () => {
|
||||
it("types contracts, overloaded handlers, the client hook, and the client entry handle", () => {
|
||||
expect(
|
||||
typeErrors(`
|
||||
import { defineRpc, defineSubscription, type SubscriptionEvent, type SubscriptionInput } from "@getpaseo/plugin";
|
||||
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
||||
import { useRpc, useSubscription } from "@getpaseo/plugin/client";
|
||||
import type { PluginServerContext, PluginSubscriptionContext } from "@getpaseo/plugin/server";
|
||||
import { z } from "zod";
|
||||
const diffWatch = defineSubscription({
|
||||
name: "diff.watch",
|
||||
input: z.object({ a: z.string(), b: z.string() }),
|
||||
event: z.object({ path: z.string(), status: z.enum(["added", "modified", "deleted"]) }),
|
||||
});
|
||||
const frames = defineSubscription({
|
||||
name: "screen.capture",
|
||||
input: z.object({ display: z.number() }),
|
||||
event: z.union([z.instanceof(Uint8Array), z.object({ type: z.literal("resized"), width: z.number() })]),
|
||||
});
|
||||
const thumbnail = defineRpc({ name: "thumbnail.render", input: z.object({ path: z.string() }), output: z.instanceof(Uint8Array) });
|
||||
const input: SubscriptionInput<typeof diffWatch> = { a: "main", b: "HEAD" };
|
||||
const event: SubscriptionEvent<typeof diffWatch> = { path: "a.ts", status: "added" };
|
||||
function server(server: PluginServerContext) {
|
||||
server.handle(diffWatch, ({ a, b }, { push, signal, paseo }: PluginSubscriptionContext<SubscriptionEvent<typeof diffWatch>>) => {
|
||||
const pushed: Promise<void> = push({ path: a + b, status: "modified" });
|
||||
// @ts-expect-error events follow the contract
|
||||
void push({ path: 1, status: "modified" });
|
||||
signal.addEventListener("abort", () => undefined, { once: true });
|
||||
});
|
||||
server.handle(frames, async (_input, { push }) => {
|
||||
await push(new Uint8Array(4));
|
||||
await push({ type: "resized", width: 2 });
|
||||
});
|
||||
server.handle(thumbnail, ({ path }) => new Uint8Array(path.length));
|
||||
// @ts-expect-error subscription handlers receive push, not an output
|
||||
server.handle(diffWatch, () => ({ path: "a", status: "added" as const }) as unknown as number);
|
||||
}
|
||||
function Panel() {
|
||||
const state = useSubscription(diffWatch, { a: "main", b: "HEAD" }, (change) => {
|
||||
const status: "added" | "modified" | "deleted" = change.status;
|
||||
});
|
||||
if (state.status === "error") state.error.message;
|
||||
// @ts-expect-error the hook has no ready state, only live
|
||||
if (state.status === "ready") return null;
|
||||
useSubscription(frames, { display: 1 }, (event) => {
|
||||
if (event instanceof Uint8Array) event.byteLength;
|
||||
else event.width;
|
||||
});
|
||||
const render = useRpc(thumbnail);
|
||||
void render({ path: "x" }).then((png: Uint8Array) => png.byteLength);
|
||||
return null;
|
||||
}
|
||||
function client(client: PluginClientContext) {
|
||||
const controller = new AbortController();
|
||||
const remover = client.subscribe(diffWatch, { a: "main", b: "HEAD" }, (change) => change.path, { signal: controller.signal });
|
||||
const ready: Promise<void> = remover.ready;
|
||||
const released: Promise<void> = remover.release();
|
||||
remover();
|
||||
return () => {};
|
||||
}
|
||||
`),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,13 @@ import type { PaseoApi } from "@getpaseo/client";
|
||||
import type { AgentTimelineItem, JsonValue } from "@getpaseo/protocol/agent-types";
|
||||
import type { ZodType, input as ZodInput, output as ZodOutput } from "zod";
|
||||
import type { PluginFile } from "./file.js";
|
||||
import type { PluginRpcContract } from "../rpc.js";
|
||||
import type {
|
||||
PluginRpcContract,
|
||||
PluginSubscribeOptions,
|
||||
PluginSubscriptionContract,
|
||||
PluginSubscriptionHandle,
|
||||
PluginWireInput,
|
||||
} from "../rpc.js";
|
||||
import type {
|
||||
PluginButtonRegistration,
|
||||
PluginHeaderButtonContribution,
|
||||
@@ -203,6 +209,16 @@ export interface PluginClientContext extends PluginCommandCapabilities, PluginCl
|
||||
contribution: PluginTimelineRendererContribution<Schema>,
|
||||
): PluginCleanup;
|
||||
openPanel(id: string, options: PluginClientOpenPanelOptions): void;
|
||||
/**
|
||||
* Subscribes to a plugin subscription on this client's host. Paseo re-subscribes after a
|
||||
* reconnect and releases what is left when the plugin stops.
|
||||
*/
|
||||
subscribe<InputSchema extends ZodType, EventSchema extends ZodType>(
|
||||
contract: PluginSubscriptionContract<InputSchema, EventSchema>,
|
||||
input: PluginWireInput<ZodInput<InputSchema>>,
|
||||
onEvent: (event: ZodOutput<EventSchema>) => void,
|
||||
options?: PluginSubscribeOptions,
|
||||
): PluginSubscriptionHandle;
|
||||
}
|
||||
|
||||
export type PluginClientContribution = (client: PluginClientContext) => PluginCleanup;
|
||||
@@ -295,7 +311,7 @@ export interface PluginCommandCapabilities {
|
||||
paseo: PaseoApi;
|
||||
rpc<InputSchema extends ZodType, OutputSchema extends ZodType>(
|
||||
contract: PluginRpcContract<InputSchema, OutputSchema>,
|
||||
input: ZodInput<InputSchema>,
|
||||
input: PluginWireInput<ZodInput<InputSchema>>,
|
||||
): Promise<ZodOutput<OutputSchema>>;
|
||||
openScreen(input: PluginOpenScreenInput): void;
|
||||
/** @deprecated Use `openScreen`. */
|
||||
|
||||
@@ -8,7 +8,7 @@ export {
|
||||
import type { PluginAttachmentSourceContribution } from "../contracts.js";
|
||||
import { PluginRpcProvider } from "./rpc-context.js";
|
||||
import { PaseoApiProvider } from "./paseo-context.js";
|
||||
import { callPluginRpc } from "../rpc.js";
|
||||
import { callPluginRpc, subscribePluginRpc, type PluginSubscriptionObserver } from "../rpc.js";
|
||||
|
||||
export async function searchPluginAttachments(
|
||||
source: PluginAttachmentSourceContribution<PluginText>,
|
||||
@@ -19,7 +19,9 @@ export async function searchPluginAttachments(
|
||||
return PluginAttachmentSearchPayloadSchema.parseAsync(output);
|
||||
}
|
||||
|
||||
export { callPluginRpc, PaseoApiProvider, PluginRpcProvider };
|
||||
export { callPluginRpc, subscribePluginRpc, PaseoApiProvider, PluginRpcProvider };
|
||||
export type { PluginSubscriptionObserver };
|
||||
export type { PluginSubscribe } from "./rpc-context.js";
|
||||
|
||||
export {
|
||||
PluginTranslationRuntime,
|
||||
|
||||
@@ -71,7 +71,12 @@ export {
|
||||
} from "./file.js";
|
||||
export { usePaseo } from "./paseo-context.js";
|
||||
export { useAgent, useWorkspace } from "./client-state.js";
|
||||
export { useRpc } from "./rpc-context.js";
|
||||
export { useRpc, useSubscription, type PluginSubscriptionState } from "./rpc-context.js";
|
||||
export type {
|
||||
PluginSubscribeOptions,
|
||||
PluginSubscriptionClosed,
|
||||
PluginSubscriptionHandle,
|
||||
} from "../rpc.js";
|
||||
import type { SettingsDefinition } from "../settings.js";
|
||||
import type { SettingsState } from "./contracts.js";
|
||||
import type { ZodType } from "zod";
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
import { createContext, useCallback, useContext, useMemo, type ReactNode } from "react";
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import type { ZodType, input as ZodInput, output as ZodOutput } from "zod";
|
||||
import { callPluginRpc, type PluginRpcContract } from "../rpc.js";
|
||||
import {
|
||||
callPluginRpc,
|
||||
subscribePluginRpc,
|
||||
type PluginRpcContract,
|
||||
type PluginSubscriptionContract,
|
||||
type PluginSubscriptionHandle,
|
||||
type PluginSubscriptionObserver,
|
||||
type PluginWireInput,
|
||||
} from "../rpc.js";
|
||||
import { shallowEqual } from "./shallow.js";
|
||||
|
||||
export type PluginSubscribe = (
|
||||
method: string,
|
||||
input: unknown,
|
||||
observer: PluginSubscriptionObserver,
|
||||
) => PluginSubscriptionHandle;
|
||||
|
||||
export interface PluginRpcContextValue {
|
||||
invoke(method: string, input: unknown): Promise<unknown>;
|
||||
subscribe: PluginSubscribe;
|
||||
}
|
||||
|
||||
const PluginRpcContext = createContext<PluginRpcContextValue | null>(null);
|
||||
@@ -15,22 +40,100 @@ export function usePluginRpcContextValue(): PluginRpcContextValue | null {
|
||||
export function PluginRpcProvider({
|
||||
children,
|
||||
invoke,
|
||||
subscribe,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
invoke(method: string, input: unknown): Promise<unknown>;
|
||||
subscribe: PluginSubscribe;
|
||||
}) {
|
||||
const value = useMemo(() => ({ invoke }), [invoke]);
|
||||
const value = useMemo(() => ({ invoke, subscribe }), [invoke, subscribe]);
|
||||
return <PluginRpcContext.Provider value={value}>{children}</PluginRpcContext.Provider>;
|
||||
}
|
||||
|
||||
export function useRpc<InputSchema extends ZodType, OutputSchema extends ZodType>(
|
||||
contract: PluginRpcContract<InputSchema, OutputSchema>,
|
||||
): (input: ZodInput<InputSchema>) => Promise<ZodOutput<OutputSchema>> {
|
||||
): (input: PluginWireInput<ZodInput<InputSchema>>) => Promise<ZodOutput<OutputSchema>> {
|
||||
const context = usePluginRpcContextValue();
|
||||
if (!context) throw new Error("useRpc must run inside a contributed plugin surface");
|
||||
|
||||
return useCallback(
|
||||
(input: ZodInput<InputSchema>) => callPluginRpc(contract, context.invoke, input),
|
||||
(input: PluginWireInput<ZodInput<InputSchema>>) =>
|
||||
callPluginRpc(contract, context.invoke, input),
|
||||
[context, contract],
|
||||
);
|
||||
}
|
||||
|
||||
export type PluginSubscriptionState =
|
||||
| { status: "connecting" }
|
||||
| { status: "live" }
|
||||
/** The plugin's handler finished. The hook subscribes again when `input` changes. */
|
||||
| { status: "closed" }
|
||||
| { status: "error"; error: Error };
|
||||
|
||||
/**
|
||||
* Subscribes while the component is mounted. Re-subscribes when `input` changes by shallow
|
||||
* comparison; `onEvent` always runs the latest callback.
|
||||
*/
|
||||
export function useSubscription<InputSchema extends ZodType, EventSchema extends ZodType>(
|
||||
contract: PluginSubscriptionContract<InputSchema, EventSchema>,
|
||||
input: PluginWireInput<ZodInput<InputSchema>>,
|
||||
onEvent: (event: ZodOutput<EventSchema>) => void,
|
||||
): PluginSubscriptionState {
|
||||
const context = usePluginRpcContextValue();
|
||||
if (!context) throw new Error("useSubscription must run inside a contributed plugin surface");
|
||||
const subscribe = context.subscribe;
|
||||
const onEventRef = useRef(onEvent);
|
||||
onEventRef.current = onEvent;
|
||||
const stableInput = useShallowStable(input);
|
||||
const [state, setState] = useState<PluginSubscriptionState>({ status: "connecting" });
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
setState({ status: "connecting" });
|
||||
let handle: PluginSubscriptionHandle;
|
||||
try {
|
||||
handle = subscribePluginRpc(
|
||||
contract,
|
||||
subscribe,
|
||||
stableInput,
|
||||
(event) => onEventRef.current(event),
|
||||
{
|
||||
onClosed: ({ error }) => {
|
||||
if (active) setState(error ? { status: "error", error } : CLOSED);
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
setState({ status: "error", error: toError(error) });
|
||||
return;
|
||||
}
|
||||
handle.ready.then(
|
||||
() => {
|
||||
if (active) setState((current) => (current.status === "connecting" ? LIVE : current));
|
||||
return undefined;
|
||||
},
|
||||
(error: unknown) => {
|
||||
if (active) setState({ status: "error", error: toError(error) });
|
||||
},
|
||||
);
|
||||
return () => {
|
||||
active = false;
|
||||
handle();
|
||||
};
|
||||
}, [contract, subscribe, stableInput]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
const LIVE: PluginSubscriptionState = { status: "live" };
|
||||
const CLOSED: PluginSubscriptionState = { status: "closed" };
|
||||
|
||||
function useShallowStable<Value>(value: Value): Value {
|
||||
const ref = useRef(value);
|
||||
if (!shallowEqual(ref.current, value)) ref.current = value;
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
function toError(error: unknown): Error {
|
||||
return error instanceof Error ? error : new Error(String(error));
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function usePluginRuntimeContextBridge(): PluginRuntimeContextBridge {
|
||||
);
|
||||
return (
|
||||
<PaseoApiProvider paseo={paseo}>
|
||||
<PluginRpcProvider invoke={rpc.invoke}>
|
||||
<PluginRpcProvider invoke={rpc.invoke} subscribe={rpc.subscribe}>
|
||||
<PluginTranslationProvider runtime={translations}>{content}</PluginTranslationProvider>
|
||||
</PluginRpcProvider>
|
||||
</PaseoApiProvider>
|
||||
|
||||
@@ -19,7 +19,16 @@ export {
|
||||
type PluginAttachmentItem,
|
||||
type PluginAttachmentSearchPayload,
|
||||
} from "./attachments.js";
|
||||
export { defineRpc, type PluginRpcContract, type RpcInput, type RpcOutput } from "./rpc.js";
|
||||
export {
|
||||
defineRpc,
|
||||
defineSubscription,
|
||||
type PluginRpcContract,
|
||||
type PluginSubscriptionContract,
|
||||
type RpcInput,
|
||||
type RpcOutput,
|
||||
type SubscriptionEvent,
|
||||
type SubscriptionInput,
|
||||
} from "./rpc.js";
|
||||
|
||||
export type { PluginText, PluginMessage } from "./text.js";
|
||||
export {
|
||||
|
||||
@@ -9,6 +9,12 @@ export interface PluginRpcContract<
|
||||
output: OutputSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* `z.instanceof(Uint8Array)` infers `Uint8Array<ArrayBuffer>`; values crossing the wire accept any
|
||||
* `Uint8Array`, including a `Buffer` or a view of a shared buffer.
|
||||
*/
|
||||
export type PluginWireInput<Value> = Value extends Uint8Array ? Uint8Array : Value;
|
||||
|
||||
export type RpcInput<Contract extends PluginRpcContract> = ZodOutput<Contract["input"]>;
|
||||
export type RpcOutput<Contract extends PluginRpcContract> = ZodInput<Contract["output"]>;
|
||||
|
||||
@@ -31,9 +37,99 @@ export function defineRpc<InputSchema extends ZodType, OutputSchema extends ZodT
|
||||
export async function callPluginRpc<InputSchema extends ZodType, OutputSchema extends ZodType>(
|
||||
contract: PluginRpcContract<InputSchema, OutputSchema>,
|
||||
invoke: (method: string, input: unknown) => Promise<unknown>,
|
||||
input: ZodInput<InputSchema>,
|
||||
input: PluginWireInput<ZodInput<InputSchema>>,
|
||||
): Promise<ZodOutput<OutputSchema>> {
|
||||
const parsedInput = await contract.input.parseAsync(input);
|
||||
const output = await invoke(contract.name, parsedInput);
|
||||
return contract.output.parseAsync(output);
|
||||
}
|
||||
|
||||
export interface PluginSubscriptionContract<
|
||||
InputSchema extends ZodType = ZodType,
|
||||
EventSchema extends ZodType = ZodType,
|
||||
> {
|
||||
name: string;
|
||||
input: InputSchema;
|
||||
event: EventSchema;
|
||||
}
|
||||
|
||||
export type SubscriptionInput<Contract extends PluginSubscriptionContract> = ZodOutput<
|
||||
Contract["input"]
|
||||
>;
|
||||
export type SubscriptionEvent<Contract extends PluginSubscriptionContract> = ZodOutput<
|
||||
Contract["event"]
|
||||
>;
|
||||
|
||||
interface SubscriptionDefinition<InputSchema extends ZodType, EventSchema extends ZodType> {
|
||||
name: string;
|
||||
input: InputSchema;
|
||||
event: EventSchema;
|
||||
}
|
||||
|
||||
export function defineSubscription<InputSchema extends ZodType, EventSchema extends ZodType>(
|
||||
definition: SubscriptionDefinition<InputSchema, EventSchema>,
|
||||
): PluginSubscriptionContract<InputSchema, EventSchema> {
|
||||
const name = definition.name.trim();
|
||||
if (!RPC_NAME.test(name)) throw new Error(`Invalid plugin subscription: ${definition.name}`);
|
||||
return { ...definition, name };
|
||||
}
|
||||
|
||||
/** One subscription from a client surface. Calling it or `release()` ends it. */
|
||||
export type PluginSubscriptionHandle = (() => void) & {
|
||||
/** Resolves once the plugin acknowledged the subscription; rejects if it refused. */
|
||||
readonly ready: Promise<void>;
|
||||
release(): Promise<void>;
|
||||
};
|
||||
|
||||
export interface PluginSubscriptionObserver {
|
||||
event(event: unknown): void;
|
||||
closed(error?: Error): void;
|
||||
}
|
||||
|
||||
export interface PluginSubscriptionClosed {
|
||||
/** Set when the plugin's handler failed, the plugin stopped, or an event failed validation. */
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export interface PluginSubscribeOptions {
|
||||
/** Aborting releases the subscription. */
|
||||
signal?: AbortSignal;
|
||||
/** Runs once when the daemon closes the subscription. Not called after a release. */
|
||||
onClosed?: (closed: PluginSubscriptionClosed) => void;
|
||||
}
|
||||
|
||||
export function subscribePluginRpc<InputSchema extends ZodType, EventSchema extends ZodType>(
|
||||
contract: PluginSubscriptionContract<InputSchema, EventSchema>,
|
||||
subscribe: (
|
||||
method: string,
|
||||
input: unknown,
|
||||
observer: PluginSubscriptionObserver,
|
||||
) => PluginSubscriptionHandle,
|
||||
input: PluginWireInput<ZodInput<InputSchema>>,
|
||||
onEvent: (event: ZodOutput<EventSchema>) => void,
|
||||
options: PluginSubscribeOptions = {},
|
||||
): PluginSubscriptionHandle {
|
||||
// The plugin parses the input into the schema's output, so it receives the input as given.
|
||||
contract.input.parse(input);
|
||||
let closed = false;
|
||||
function close(error?: Error) {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
options.onClosed?.(error ? { error } : {});
|
||||
}
|
||||
const handle = subscribe(contract.name, input, {
|
||||
event(event) {
|
||||
if (closed) return;
|
||||
const parsed = contract.event.safeParse(event);
|
||||
if (parsed.success) return onEvent(parsed.data);
|
||||
handle();
|
||||
close(new Error(`Plugin subscription ${contract.name} sent an invalid event`));
|
||||
},
|
||||
closed: close,
|
||||
});
|
||||
if (options.signal) {
|
||||
if (options.signal.aborted) handle();
|
||||
else options.signal.addEventListener("abort", () => handle(), { once: true });
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PaseoApi } from "@getpaseo/client";
|
||||
import type { ZodType, input as ZodInput, output as ZodOutput } from "zod";
|
||||
import type { PluginRpcContract } from "../rpc.js";
|
||||
import type { PluginRpcContract, PluginSubscriptionContract, PluginWireInput } from "../rpc.js";
|
||||
import type { PluginCleanup } from "../contracts.js";
|
||||
import type { ProviderRegistration } from "./provider.js";
|
||||
import type { UsageSourceRegistration } from "./usage.js";
|
||||
@@ -10,6 +10,18 @@ export interface PluginHandlerContext {
|
||||
paseo: PaseoApi;
|
||||
}
|
||||
|
||||
/** What a subscription handler receives beside its input. */
|
||||
export interface PluginSubscriptionContext<Event = unknown> extends PluginHandlerContext {
|
||||
/**
|
||||
* Sends one event to this subscriber. A `Uint8Array` event travels as a binary frame; any other
|
||||
* event is JSON and must not contain a `Uint8Array`. Resolves once the event left the plugin
|
||||
* process. Events pushed after `signal` aborts are dropped.
|
||||
*/
|
||||
push(event: Event): Promise<void>;
|
||||
/** Aborts when the subscriber releases, disconnects, or the plugin stops. */
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export type PluginSettingsState<Schema extends ZodType> =
|
||||
| {
|
||||
status: "ready";
|
||||
@@ -31,12 +43,24 @@ export interface PluginServerContext extends PluginLifecycleRegistration {
|
||||
registerSettings<Schema extends ZodType>(
|
||||
definition: import("../settings.js").SettingsDefinition<Schema>,
|
||||
): PluginSettings<Schema>;
|
||||
/**
|
||||
* Serves a subscription. The handler starts the work and returns; the subscription stays open
|
||||
* until `signal` aborts. A handler that throws refuses the subscription. When it returns a
|
||||
* promise, settling the promise closes the subscription, with the rejection as its error.
|
||||
*/
|
||||
handle<InputSchema extends ZodType, EventSchema extends ZodType>(
|
||||
contract: PluginSubscriptionContract<InputSchema, EventSchema>,
|
||||
handler: (
|
||||
input: ZodOutput<InputSchema>,
|
||||
context: PluginSubscriptionContext<PluginWireInput<ZodInput<EventSchema>>>,
|
||||
) => void | Promise<void>,
|
||||
): void;
|
||||
handle<InputSchema extends ZodType, OutputSchema extends ZodType>(
|
||||
contract: PluginRpcContract<InputSchema, OutputSchema>,
|
||||
handler: (
|
||||
input: ZodOutput<InputSchema>,
|
||||
context: PluginHandlerContext,
|
||||
) => ZodInput<OutputSchema> | Promise<ZodInput<OutputSchema>>,
|
||||
) => PluginWireInput<ZodInput<OutputSchema>> | Promise<PluginWireInput<ZodInput<OutputSchema>>>,
|
||||
): void;
|
||||
registerProvider(provider: ProviderRegistration): void;
|
||||
registerUsageSource(source: UsageSourceRegistration): void;
|
||||
|
||||
@@ -11,6 +11,7 @@ export type {
|
||||
PluginServerContribution,
|
||||
PluginSettings,
|
||||
PluginSettingsState,
|
||||
PluginSubscriptionContext,
|
||||
} from "./contracts.js";
|
||||
export type {
|
||||
PluginHooks,
|
||||
|
||||
Reference in New Issue
Block a user