feat: add capability playground controller
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
decodeCapabilityCallResult,
|
||||
type CapabilityCallResult,
|
||||
} from "./capability-models.js";
|
||||
import {
|
||||
callCapability,
|
||||
type CapabilityCallRequest,
|
||||
} from "./capability-call-client.js";
|
||||
import type { ConsoleExecutor } from "./executor-protocol.js";
|
||||
|
||||
const result: CapabilityCallResult = {
|
||||
qualifiedName: "local.docs.read_documents",
|
||||
sourceId: "local.docs",
|
||||
kind: "node_spec",
|
||||
deploymentId: null,
|
||||
outcome: "ok",
|
||||
output: { documents: [] },
|
||||
diagnostics: [],
|
||||
};
|
||||
|
||||
describe("callCapability", () => {
|
||||
it("lowers the request to the executor operation payload", async () => {
|
||||
const executor = {
|
||||
run: vi.fn().mockResolvedValue(result),
|
||||
} as unknown as ConsoleExecutor;
|
||||
const request: CapabilityCallRequest = {
|
||||
qualifiedName: "local.docs.read_documents",
|
||||
payload: { names: ["README.md"] },
|
||||
deploymentId: "docs.default",
|
||||
};
|
||||
|
||||
await callCapability(executor, request);
|
||||
|
||||
expect(executor.run).toHaveBeenCalledWith(
|
||||
"workflow.capabilities.call",
|
||||
{
|
||||
qualified_name: "local.docs.read_documents",
|
||||
payload: { names: ["README.md"] },
|
||||
deployment_id: "docs.default",
|
||||
},
|
||||
decodeCapabilityCallResult,
|
||||
);
|
||||
});
|
||||
|
||||
it("omits a blank deployment ID", async () => {
|
||||
const executor = {
|
||||
run: vi.fn().mockResolvedValue(result),
|
||||
} as unknown as ConsoleExecutor;
|
||||
|
||||
await callCapability(executor, {
|
||||
qualifiedName: "local.docs.read_documents",
|
||||
payload: {},
|
||||
deploymentId: " ",
|
||||
});
|
||||
|
||||
expect(executor.run).toHaveBeenCalledWith(
|
||||
"workflow.capabilities.call",
|
||||
{
|
||||
qualified_name: "local.docs.read_documents",
|
||||
payload: {},
|
||||
},
|
||||
decodeCapabilityCallResult,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { ConsoleExecutor } from "./executor-protocol.js";
|
||||
import {
|
||||
decodeCapabilityCallResult,
|
||||
type CapabilityCallResult,
|
||||
} from "./capability-models.js";
|
||||
|
||||
export type CapabilityCallRequest = {
|
||||
readonly qualifiedName: string;
|
||||
readonly payload: Record<string, unknown>;
|
||||
readonly deploymentId?: string;
|
||||
};
|
||||
|
||||
export const callCapability = (
|
||||
executor: ConsoleExecutor,
|
||||
request: CapabilityCallRequest,
|
||||
): Promise<CapabilityCallResult> => {
|
||||
const deploymentId = request.deploymentId?.trim();
|
||||
return executor.run(
|
||||
"workflow.capabilities.call",
|
||||
{
|
||||
qualified_name: request.qualifiedName,
|
||||
payload: request.payload,
|
||||
...(deploymentId ? { deployment_id: deploymentId } : {}),
|
||||
},
|
||||
decodeCapabilityCallResult,
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
decodeCapabilityCallResult,
|
||||
decodeCapabilityDetail,
|
||||
decodeCapabilityPage,
|
||||
} from "./capability-models.js";
|
||||
@@ -81,4 +82,52 @@ describe("capability models", () => {
|
||||
expect(wrapper.version).toBe(2);
|
||||
}
|
||||
});
|
||||
|
||||
it("decodes a successful capability call with dependency diagnostics", () => {
|
||||
const result = decodeCapabilityCallResult({
|
||||
qualifiedName: "local.docs.read_documents",
|
||||
sourceId: "local.docs",
|
||||
kind: "node_spec",
|
||||
deploymentId: null,
|
||||
outcome: "ok",
|
||||
output: { documents: [{ name: "README.md" }] },
|
||||
diagnostics: [
|
||||
{
|
||||
boundSource: "local.docs",
|
||||
code: "source_missing",
|
||||
logicalRef: "local.docs",
|
||||
message: "The source is not configured.",
|
||||
repairHint: "Configure the source before calling.",
|
||||
severity: "error",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.qualifiedName).toBe("local.docs.read_documents");
|
||||
expect(result.output).toEqual({ documents: [{ name: "README.md" }] });
|
||||
expect(result.diagnostics[0]?.repairHint).toBe(
|
||||
"Configure the source before calling.",
|
||||
);
|
||||
});
|
||||
|
||||
it("decodes runtime_error as a normal completed capability result", () => {
|
||||
const result = decodeCapabilityCallResult({
|
||||
qualifiedName: "local.docs.read_documents",
|
||||
sourceId: "local.docs",
|
||||
kind: "wrapper_artifact",
|
||||
deploymentId: "docs.default",
|
||||
outcome: "runtime_error",
|
||||
output: null,
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
expect(result.outcome).toBe("runtime_error");
|
||||
expect(result.output).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects malformed capability call results", () => {
|
||||
expect(() => decodeCapabilityCallResult({ outcome: "ok" })).toThrow(
|
||||
"CapabilityCallResult is malformed",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,25 @@ const decode = <T>(
|
||||
const JsonObjectSchema = v.record(v.string(), v.unknown());
|
||||
const WrapperHintsSchema = v.record(v.string(), v.unknown());
|
||||
|
||||
const DependencyDiagnosticSchema = v.object({
|
||||
boundSource: v.nullable(v.string()),
|
||||
code: v.string(),
|
||||
logicalRef: v.string(),
|
||||
message: v.string(),
|
||||
repairHint: v.nullable(v.string()),
|
||||
severity: v.string(),
|
||||
});
|
||||
|
||||
const CapabilityCallResultSchema = v.object({
|
||||
qualifiedName: v.string(),
|
||||
sourceId: v.string(),
|
||||
kind: v.union([v.literal("node_spec"), v.literal("wrapper_artifact")]),
|
||||
deploymentId: v.nullable(v.string()),
|
||||
outcome: v.string(),
|
||||
output: v.nullable(JsonObjectSchema),
|
||||
diagnostics: v.array(DependencyDiagnosticSchema),
|
||||
});
|
||||
|
||||
const CapabilitySummarySchema = v.variant("kind", [
|
||||
v.object({
|
||||
kind: v.literal("node_spec"),
|
||||
@@ -75,9 +94,17 @@ const CapabilityDetailSchema = v.variant("kind", [
|
||||
export type CapabilitySummary = v.InferOutput<typeof CapabilitySummarySchema>;
|
||||
export type CapabilityPage = v.InferOutput<typeof CapabilityPageSchema>;
|
||||
export type CapabilityDetail = v.InferOutput<typeof CapabilityDetailSchema>;
|
||||
export type CapabilityCallResult = v.InferOutput<
|
||||
typeof CapabilityCallResultSchema
|
||||
>;
|
||||
|
||||
export const decodeCapabilityPage = (value: unknown): CapabilityPage =>
|
||||
decode("CapabilityPage", CapabilityPageSchema, value);
|
||||
|
||||
export const decodeCapabilityDetail = (value: unknown): CapabilityDetail =>
|
||||
decode("CapabilityDetail", CapabilityDetailSchema, value);
|
||||
|
||||
export const decodeCapabilityCallResult = (
|
||||
value: unknown,
|
||||
): CapabilityCallResult =>
|
||||
decode("CapabilityCallResult", CapabilityCallResultSchema, value);
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ConnectionState } from "../../app/state.js";
|
||||
import { useConsoleWorkspace } from "../context.js";
|
||||
import {
|
||||
callCapability,
|
||||
type CapabilityCallRequest,
|
||||
} from "../domain/capability-call-client.js";
|
||||
import type { CapabilityCallResult } from "../domain/capability-models.js";
|
||||
import type { ConsoleWriteExecutor } from "../domain/write-executor.js";
|
||||
import { useCapabilityPlayground } from "./useCapabilityPlayground.js";
|
||||
|
||||
vi.mock("../context.js", () => ({
|
||||
useConsoleWorkspace: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../domain/capability-call-client.js", async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import("../domain/capability-call-client.js")
|
||||
>("../domain/capability-call-client.js");
|
||||
return { ...actual, callCapability: vi.fn() };
|
||||
});
|
||||
|
||||
const mockedUseConsoleWorkspace = vi.mocked(useConsoleWorkspace);
|
||||
const mockedCallCapability = vi.mocked(callCapability);
|
||||
|
||||
const connectedState = {
|
||||
phase: "connected",
|
||||
connectedTarget: "http://workflow.example/rpc",
|
||||
} as ConnectionState;
|
||||
|
||||
const disconnectedState = {
|
||||
phase: "not_configured",
|
||||
connectedTarget: null,
|
||||
} as ConnectionState;
|
||||
|
||||
const writeExecutor = {} as ConsoleWriteExecutor;
|
||||
|
||||
const result = (outcome = "ok"): CapabilityCallResult => ({
|
||||
qualifiedName: "local.docs.read_documents",
|
||||
sourceId: "local.docs",
|
||||
kind: "node_spec",
|
||||
deploymentId: null,
|
||||
outcome,
|
||||
output: outcome === "runtime_error" ? null : { documents: [] },
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const deferred = <T,>() => {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise;
|
||||
reject = rejectPromise;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockedCallCapability.mockReset();
|
||||
mockedUseConsoleWorkspace.mockReturnValue({
|
||||
connection: connectedState,
|
||||
connectedTarget: connectedState.connectedTarget,
|
||||
recordEvidence: vi.fn(),
|
||||
readExecutor: null,
|
||||
writeExecutor,
|
||||
});
|
||||
});
|
||||
|
||||
describe("useCapabilityPlayground", () => {
|
||||
it("is disconnected without a connected write target", () => {
|
||||
mockedUseConsoleWorkspace.mockReturnValue({
|
||||
connection: disconnectedState,
|
||||
connectedTarget: null,
|
||||
recordEvidence: vi.fn(),
|
||||
readExecutor: null,
|
||||
writeExecutor: null,
|
||||
});
|
||||
|
||||
const { result: hook } = renderHook(() =>
|
||||
useCapabilityPlayground("local.docs.read_documents"),
|
||||
);
|
||||
|
||||
expect(hook.current.phase).toBe("disconnected");
|
||||
expect(hook.current.result).toBeNull();
|
||||
});
|
||||
|
||||
it("starts connected in the idle phase", async () => {
|
||||
const { result: hook } = renderHook(() =>
|
||||
useCapabilityPlayground("local.docs.read_documents"),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(hook.current.phase).toBe("idle"));
|
||||
expect(hook.current.acknowledged).toBe(false);
|
||||
expect(hook.current.deploymentId).toBe("");
|
||||
});
|
||||
|
||||
it("reports calling and then a successful result", async () => {
|
||||
mockedCallCapability.mockResolvedValue(result());
|
||||
const { result: hook } = renderHook(() =>
|
||||
useCapabilityPlayground("local.docs.read_documents"),
|
||||
);
|
||||
|
||||
act(() => hook.current.call({ names: ["README.md"] }));
|
||||
expect(hook.current.phase).toBe("calling");
|
||||
await waitFor(() => expect(hook.current.phase).toBe("result"));
|
||||
expect(hook.current.result?.outcome).toBe("ok");
|
||||
});
|
||||
|
||||
it("keeps runtime_error in the completed result phase", async () => {
|
||||
mockedCallCapability.mockResolvedValue(result("runtime_error"));
|
||||
const { result: hook } = renderHook(() =>
|
||||
useCapabilityPlayground("local.docs.read_documents"),
|
||||
);
|
||||
|
||||
act(() => hook.current.call({}));
|
||||
|
||||
await waitFor(() => expect(hook.current.phase).toBe("result"));
|
||||
expect(hook.current.result?.outcome).toBe("runtime_error");
|
||||
expect(hook.current.message).toBeNull();
|
||||
});
|
||||
|
||||
it("reports transport and protocol failures as an error phase", async () => {
|
||||
mockedCallCapability.mockRejectedValue(new Error("upstream unavailable"));
|
||||
const { result: hook } = renderHook(() =>
|
||||
useCapabilityPlayground("local.docs.read_documents"),
|
||||
);
|
||||
|
||||
act(() => hook.current.call({}));
|
||||
|
||||
await waitFor(() => expect(hook.current.phase).toBe("error"));
|
||||
expect(hook.current.message).toBe("upstream unavailable");
|
||||
expect(hook.current.result).toBeNull();
|
||||
});
|
||||
|
||||
it("suppresses a second submit while a call is pending", () => {
|
||||
const pending = deferred<CapabilityCallResult>();
|
||||
mockedCallCapability.mockReturnValue(pending.promise);
|
||||
const { result: hook } = renderHook(() =>
|
||||
useCapabilityPlayground("local.docs.read_documents"),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
hook.current.call({ first: true });
|
||||
hook.current.call({ second: true });
|
||||
});
|
||||
|
||||
expect(mockedCallCapability).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("resets acknowledgement, deployment, and result when capability changes", async () => {
|
||||
mockedCallCapability.mockResolvedValue(result());
|
||||
const { result: hook, rerender } = renderHook(
|
||||
({ qualifiedName }) => useCapabilityPlayground(qualifiedName),
|
||||
{ initialProps: { qualifiedName: "local.docs.read_documents" } },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
hook.current.setAcknowledged(true);
|
||||
hook.current.setDeploymentId("docs.default");
|
||||
hook.current.call({});
|
||||
});
|
||||
await waitFor(() => expect(hook.current.phase).toBe("result"));
|
||||
|
||||
rerender({ qualifiedName: "local.docs.write_documents" });
|
||||
|
||||
await waitFor(() => expect(hook.current.phase).toBe("idle"));
|
||||
expect(hook.current.acknowledged).toBe(false);
|
||||
expect(hook.current.deploymentId).toBe("");
|
||||
expect(hook.current.result).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores a stale completion after the selected capability changes", async () => {
|
||||
const first = deferred<CapabilityCallResult>();
|
||||
const second = deferred<CapabilityCallResult>();
|
||||
mockedCallCapability
|
||||
.mockReturnValueOnce(first.promise)
|
||||
.mockReturnValueOnce(second.promise);
|
||||
const { result: hook, rerender } = renderHook(
|
||||
({ qualifiedName }) => useCapabilityPlayground(qualifiedName),
|
||||
{ initialProps: { qualifiedName: "local.docs.first" } },
|
||||
);
|
||||
|
||||
act(() => hook.current.call({ value: "first" }));
|
||||
rerender({ qualifiedName: "local.docs.second" });
|
||||
act(() => hook.current.call({ value: "second" }));
|
||||
|
||||
first.resolve({ ...result(), qualifiedName: "local.docs.first" });
|
||||
await waitFor(() => expect(hook.current.phase).toBe("calling"));
|
||||
second.resolve({ ...result(), qualifiedName: "local.docs.second" });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(hook.current.result?.qualifiedName).toBe("local.docs.second"),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes the selected capability, payload, and deployment to the client", async () => {
|
||||
mockedCallCapability.mockResolvedValue(result());
|
||||
const { result: hook } = renderHook(() =>
|
||||
useCapabilityPlayground("local.docs.read_documents"),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
hook.current.setDeploymentId("docs.default");
|
||||
hook.current.call({ names: ["README.md"] });
|
||||
});
|
||||
|
||||
await waitFor(() => expect(hook.current.phase).toBe("result"));
|
||||
expect(mockedCallCapability).toHaveBeenCalledWith(writeExecutor, {
|
||||
qualifiedName: "local.docs.read_documents",
|
||||
payload: { names: ["README.md"] },
|
||||
deploymentId: "docs.default",
|
||||
} satisfies CapabilityCallRequest);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useConsoleWorkspace } from "../context.js";
|
||||
import {
|
||||
callCapability,
|
||||
type CapabilityCallRequest,
|
||||
} from "../domain/capability-call-client.js";
|
||||
import type { CapabilityCallResult } from "../domain/capability-models.js";
|
||||
import type { ConsoleWriteExecutor } from "../domain/write-executor.js";
|
||||
|
||||
export type CapabilityPlaygroundPhase =
|
||||
| "disconnected"
|
||||
| "idle"
|
||||
| "calling"
|
||||
| "result"
|
||||
| "error";
|
||||
|
||||
export type CapabilityPlaygroundController = {
|
||||
readonly phase: CapabilityPlaygroundPhase;
|
||||
readonly result: CapabilityCallResult | null;
|
||||
readonly message: string | null;
|
||||
readonly acknowledged: boolean;
|
||||
readonly deploymentId: string;
|
||||
readonly setAcknowledged: (value: boolean) => void;
|
||||
readonly setDeploymentId: (value: string) => void;
|
||||
readonly call: (payload: Record<string, unknown>) => void;
|
||||
readonly reset: () => void;
|
||||
};
|
||||
|
||||
type PlaygroundState = Omit<
|
||||
CapabilityPlaygroundController,
|
||||
"setAcknowledged" | "setDeploymentId" | "call" | "reset"
|
||||
>;
|
||||
|
||||
type SelectionIdentity = {
|
||||
readonly writeExecutor: ConsoleWriteExecutor | null;
|
||||
readonly connectedTarget: string | null;
|
||||
readonly qualifiedName: string | null;
|
||||
};
|
||||
|
||||
const errorMessage = (error: unknown): string =>
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
const isSameSelection = (
|
||||
left: SelectionIdentity | null,
|
||||
right: SelectionIdentity,
|
||||
): boolean =>
|
||||
left !== null &&
|
||||
left.writeExecutor === right.writeExecutor &&
|
||||
left.connectedTarget === right.connectedTarget &&
|
||||
left.qualifiedName === right.qualifiedName;
|
||||
|
||||
export const useCapabilityPlayground = (
|
||||
qualifiedName: string | null,
|
||||
): CapabilityPlaygroundController => {
|
||||
const { connectedTarget, writeExecutor } = useConsoleWorkspace();
|
||||
const isConnected = writeExecutor !== null && connectedTarget !== null;
|
||||
const selection: SelectionIdentity = {
|
||||
writeExecutor,
|
||||
connectedTarget,
|
||||
qualifiedName,
|
||||
};
|
||||
const selectionRef = useRef<SelectionIdentity>(selection);
|
||||
const committedSelectionRef = useRef<SelectionIdentity | null>(null);
|
||||
const generationRef = useRef(0);
|
||||
const pendingRef = useRef(false);
|
||||
const deploymentIdRef = useRef("");
|
||||
const [state, setState] = useState<PlaygroundState>(() => ({
|
||||
phase: isConnected ? "idle" : "disconnected",
|
||||
result: null,
|
||||
message: null,
|
||||
acknowledged: false,
|
||||
deploymentId: "",
|
||||
}));
|
||||
|
||||
if (!isSameSelection(selectionRef.current, selection)) {
|
||||
// Invalidate an in-flight call during render so a promise resolving before
|
||||
// the reset effect cannot publish data for the previous selection.
|
||||
selectionRef.current = selection;
|
||||
generationRef.current += 1;
|
||||
pendingRef.current = false;
|
||||
}
|
||||
|
||||
const resetState = useCallback((): void => {
|
||||
deploymentIdRef.current = "";
|
||||
setState({
|
||||
phase: isConnected ? "idle" : "disconnected",
|
||||
result: null,
|
||||
message: null,
|
||||
acknowledged: false,
|
||||
deploymentId: "",
|
||||
});
|
||||
}, [isConnected]);
|
||||
|
||||
useEffect(() => {
|
||||
committedSelectionRef.current = selectionRef.current;
|
||||
resetState();
|
||||
}, [connectedTarget, qualifiedName, resetState, writeExecutor]);
|
||||
|
||||
const setAcknowledged = useCallback((value: boolean): void => {
|
||||
setState((current) => ({ ...current, acknowledged: value }));
|
||||
}, []);
|
||||
|
||||
const setDeploymentId = useCallback((value: string): void => {
|
||||
deploymentIdRef.current = value;
|
||||
setState((current) => ({ ...current, deploymentId: value }));
|
||||
}, []);
|
||||
|
||||
const reset = useCallback((): void => {
|
||||
generationRef.current += 1;
|
||||
pendingRef.current = false;
|
||||
resetState();
|
||||
}, [resetState]);
|
||||
|
||||
const call = useCallback(
|
||||
(payload: Record<string, unknown>): void => {
|
||||
if (
|
||||
writeExecutor === null ||
|
||||
connectedTarget === null ||
|
||||
qualifiedName === null ||
|
||||
!qualifiedName.trim() ||
|
||||
pendingRef.current ||
|
||||
!isSameSelection(committedSelectionRef.current, selectionRef.current)
|
||||
) return;
|
||||
|
||||
const generation = ++generationRef.current;
|
||||
const requestSelection = selectionRef.current;
|
||||
pendingRef.current = true;
|
||||
setState((current) => ({
|
||||
...current,
|
||||
phase: "calling",
|
||||
result: null,
|
||||
message: null,
|
||||
}));
|
||||
|
||||
const request: CapabilityCallRequest = {
|
||||
qualifiedName,
|
||||
payload,
|
||||
deploymentId: deploymentIdRef.current,
|
||||
};
|
||||
void callCapability(writeExecutor, request).then(
|
||||
(result) => {
|
||||
if (
|
||||
generation !== generationRef.current ||
|
||||
!isSameSelection(committedSelectionRef.current, requestSelection)
|
||||
) return;
|
||||
pendingRef.current = false;
|
||||
setState((current) => ({
|
||||
...current,
|
||||
phase: "result",
|
||||
result,
|
||||
message: null,
|
||||
}));
|
||||
},
|
||||
(error: unknown) => {
|
||||
if (
|
||||
generation !== generationRef.current ||
|
||||
!isSameSelection(committedSelectionRef.current, requestSelection)
|
||||
) return;
|
||||
pendingRef.current = false;
|
||||
setState((current) => ({
|
||||
...current,
|
||||
phase: "error",
|
||||
result: null,
|
||||
message: errorMessage(error),
|
||||
}));
|
||||
},
|
||||
);
|
||||
},
|
||||
[connectedTarget, qualifiedName, writeExecutor],
|
||||
);
|
||||
|
||||
const hasCurrentSelection = isSameSelection(
|
||||
committedSelectionRef.current,
|
||||
selection,
|
||||
);
|
||||
const visibleState: PlaygroundState = hasCurrentSelection
|
||||
? state
|
||||
: {
|
||||
phase: isConnected ? "idle" : "disconnected",
|
||||
result: null,
|
||||
message: null,
|
||||
acknowledged: false,
|
||||
deploymentId: "",
|
||||
};
|
||||
|
||||
return {
|
||||
...visibleState,
|
||||
setAcknowledged,
|
||||
setDeploymentId,
|
||||
call,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user