Compare commits
No commits in common. "59b99d2042a162964b8d8ac6e01bf0a81cd4b4cf" and "23ac09ff21f296ccb6ec7d903256ea3155ff70ee" have entirely different histories.
59b99d2042
...
23ac09ff21
|
|
@ -1,95 +0,0 @@
|
||||||
import { createMiddlewareChain } from "../middleware";
|
|
||||||
import { PromptValidator } from "./command-runner";
|
|
||||||
import { CommandSchema } from "./types";
|
|
||||||
|
|
||||||
export interface PromptDef<TArgs extends any[]> {
|
|
||||||
schema: CommandSchema;
|
|
||||||
hint?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PromptCall<TArgs extends any[] = unknown[], TRes = unknown> {
|
|
||||||
def: PromptDef<TArgs>;
|
|
||||||
player?: string;
|
|
||||||
validator: PromptValidator<TRes, TArgs>;
|
|
||||||
|
|
||||||
resolve(res: TRes): void;
|
|
||||||
reject(reason: string): void;
|
|
||||||
promise: Promise<TRes>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PromptTryResult =
|
|
||||||
| {
|
|
||||||
ok: false;
|
|
||||||
reason: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
ok: true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createPromptContext() {
|
|
||||||
const map = new Map<string, PromptCall>();
|
|
||||||
const handleCall = createMiddlewareChain(async (call: PromptCall) => {
|
|
||||||
const key = call.player ?? "global";
|
|
||||||
const existing = map.get(key);
|
|
||||||
if (existing) {
|
|
||||||
existing.reject("Prompt overriden");
|
|
||||||
}
|
|
||||||
|
|
||||||
map.set(key, call);
|
|
||||||
try {
|
|
||||||
return await call.promise;
|
|
||||||
} finally {
|
|
||||||
if (map.get(key) === call) map.delete(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function tryCommit<TArgs extends any[]>(
|
|
||||||
player: string,
|
|
||||||
...args: TArgs
|
|
||||||
): PromptTryResult {
|
|
||||||
const call = map.get(player);
|
|
||||||
if (!call) return { ok: false, reason: "No Prompt" };
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = call.validator(...args);
|
|
||||||
call.resolve(res);
|
|
||||||
return { ok: true };
|
|
||||||
} catch (reason) {
|
|
||||||
if (typeof reason === "string") return { ok: false, reason };
|
|
||||||
throw reason;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancel(player: string, reason = "Prompt Cancelled") {
|
|
||||||
map.get(player)?.reject(reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function prompt<TArgs extends any[], TRes>(
|
|
||||||
def: PromptDef<TArgs>,
|
|
||||||
validator: PromptValidator<TRes, TArgs>,
|
|
||||||
player?: string,
|
|
||||||
) {
|
|
||||||
let resolve: (res: TRes) => void;
|
|
||||||
let reject: (reason: string) => void;
|
|
||||||
const promise = new Promise<TRes>((res, rej) => {
|
|
||||||
resolve = res;
|
|
||||||
reject = rej;
|
|
||||||
});
|
|
||||||
const call = {
|
|
||||||
def,
|
|
||||||
player,
|
|
||||||
validator,
|
|
||||||
resolve: resolve!,
|
|
||||||
reject: reject!,
|
|
||||||
promise,
|
|
||||||
} as PromptCall<unknown[], unknown>;
|
|
||||||
return await handleCall.execute(call);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
prompt,
|
|
||||||
tryCommit,
|
|
||||||
cancel,
|
|
||||||
handleCall,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +1,22 @@
|
||||||
export { parseCommand } from "./command-parse";
|
export { parseCommand } from './command-parse';
|
||||||
export { parseCommandSchema } from "./schema-parse";
|
export { parseCommandSchema } from './schema-parse';
|
||||||
|
export { validateCommand, parseCommandWithSchema, applyCommandSchema } from './command-validate';
|
||||||
export {
|
export {
|
||||||
validateCommand,
|
createCommandRegistry,
|
||||||
parseCommandWithSchema,
|
registerCommand,
|
||||||
applyCommandSchema,
|
unregisterCommand,
|
||||||
} from "./command-validate";
|
hasCommand,
|
||||||
export {
|
getCommand,
|
||||||
createCommandRegistry,
|
runCommand,
|
||||||
registerCommand,
|
runCommandParsed,
|
||||||
unregisterCommand,
|
createCommandRunnerContext,
|
||||||
hasCommand,
|
} from './command-registry';
|
||||||
getCommand,
|
|
||||||
runCommand,
|
|
||||||
runCommandParsed,
|
|
||||||
createCommandRunnerContext,
|
|
||||||
} from "./command-registry";
|
|
||||||
export type {
|
export type {
|
||||||
Command,
|
Command,
|
||||||
CommandParamSchema,
|
CommandParamSchema,
|
||||||
CommandOptionSchema,
|
CommandOptionSchema,
|
||||||
CommandFlagSchema,
|
CommandFlagSchema,
|
||||||
CommandSchema,
|
CommandSchema,
|
||||||
} from "./types";
|
} from './types';
|
||||||
export type {
|
export type { CommandRunner, CommandDef, CommandResult, CommandRunnerHandler, CommandRunnerContext, PromptEvent, CommandRunnerEvents } from './command-runner';
|
||||||
CommandRunner,
|
export type { CommandRegistry, CommandRunnerContextExport } from './command-registry';
|
||||||
CommandDef,
|
|
||||||
CommandResult,
|
|
||||||
CommandRunnerHandler,
|
|
||||||
CommandRunnerContext,
|
|
||||||
PromptEvent,
|
|
||||||
CommandRunnerEvents,
|
|
||||||
} from "./command-runner";
|
|
||||||
export type {
|
|
||||||
CommandRegistry,
|
|
||||||
CommandRunnerContextExport,
|
|
||||||
} from "./command-registry";
|
|
||||||
|
|
||||||
export type { PromptDef, PromptTryResult, PromptCall } from "./command-prompt";
|
|
||||||
|
|
||||||
export { createPromptContext } from "./command-prompt";
|
|
||||||
|
|
|
||||||
|
|
@ -1,219 +0,0 @@
|
||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import { createPromptContext } from "@/utils/command/command-prompt";
|
|
||||||
import type { CommandSchema } from "@/utils/command/types";
|
|
||||||
|
|
||||||
const mockSchema: CommandSchema = {
|
|
||||||
name: "test",
|
|
||||||
params: [{ name: "value", required: true, variadic: false }],
|
|
||||||
options: {},
|
|
||||||
flags: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("createPromptContext", () => {
|
|
||||||
describe("prompt", () => {
|
|
||||||
it("should store prompt and return promise", async () => {
|
|
||||||
const ctx = createPromptContext();
|
|
||||||
const validator = (value: string) => value;
|
|
||||||
|
|
||||||
const promise = ctx.prompt({ schema: mockSchema }, validator);
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
const result = ctx.tryCommit("global", "hello");
|
|
||||||
expect(result).toEqual({ ok: true });
|
|
||||||
await expect(promise).resolves.toBe("hello");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should associate prompt with specific player", async () => {
|
|
||||||
const ctx = createPromptContext();
|
|
||||||
const validator = (value: string) => value;
|
|
||||||
|
|
||||||
const promise = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
const result = ctx.tryCommit("player1", "hello");
|
|
||||||
expect(result).toEqual({ ok: true });
|
|
||||||
await expect(promise).resolves.toBe("hello");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should cancel existing prompt when new prompt starts for same player", async () => {
|
|
||||||
const ctx = createPromptContext();
|
|
||||||
const validator = (value: string) => value;
|
|
||||||
|
|
||||||
const promise1 = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
const promise2 = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
||||||
|
|
||||||
// Resolve promise2 first, then both will settle
|
|
||||||
ctx.tryCommit("player1", "test");
|
|
||||||
const [result1, result2] = await Promise.allSettled([promise1, promise2]);
|
|
||||||
|
|
||||||
expect(result1.status).toBe("rejected");
|
|
||||||
expect((result1 as PromiseRejectedResult).reason).toBe(
|
|
||||||
"Prompt overriden",
|
|
||||||
);
|
|
||||||
expect(result2.status).toBe("fulfilled");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should allow different players to have independent prompts", async () => {
|
|
||||||
const ctx = createPromptContext();
|
|
||||||
const validator = (value: string) => value;
|
|
||||||
|
|
||||||
const promise1 = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
||||||
const promise2 = ctx.prompt({ schema: mockSchema }, validator, "player2");
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
ctx.tryCommit("player1", "hello");
|
|
||||||
ctx.tryCommit("player2", "world");
|
|
||||||
|
|
||||||
await expect(promise1).resolves.toBe("hello");
|
|
||||||
await expect(promise2).resolves.toBe("world");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("tryCommit", () => {
|
|
||||||
it("should return ok:true when prompt exists and validation passes", () => {
|
|
||||||
const ctx = createPromptContext();
|
|
||||||
const validator = (value: string) => value.toUpperCase();
|
|
||||||
|
|
||||||
ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
||||||
const result = ctx.tryCommit("player1", "hello");
|
|
||||||
|
|
||||||
expect(result).toEqual({ ok: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return ok:false with reason when no prompt exists", () => {
|
|
||||||
const ctx = createPromptContext();
|
|
||||||
const result = ctx.tryCommit("nonexistent", "hello");
|
|
||||||
|
|
||||||
expect(result).toEqual({ ok: false, reason: "No Prompt" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return ok:false when validator throws string", () => {
|
|
||||||
const ctx = createPromptContext();
|
|
||||||
const validator = (value: string) => {
|
|
||||||
if (value.length < 3) throw "Value too short";
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
||||||
const result = ctx.tryCommit("player1", "ab");
|
|
||||||
|
|
||||||
expect(result).toEqual({ ok: false, reason: "Value too short" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw when validator throws non-string error", () => {
|
|
||||||
const ctx = createPromptContext();
|
|
||||||
const validator = () => {
|
|
||||||
throw new Error("unexpected");
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
||||||
expect(() => ctx.tryCommit("player1", "test")).toThrow("unexpected");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should pass validator return value to promise resolver", async () => {
|
|
||||||
const ctx = createPromptContext();
|
|
||||||
const validator = (a: number, b: number) => a + b;
|
|
||||||
|
|
||||||
const promise = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
ctx.tryCommit("player1", 2, 3);
|
|
||||||
await expect(promise).resolves.toBe(5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("cancel", () => {
|
|
||||||
it("should reject promise with default message", async () => {
|
|
||||||
const ctx = createPromptContext();
|
|
||||||
const validator = (value: string) => value;
|
|
||||||
|
|
||||||
const promise = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
ctx.cancel("player1");
|
|
||||||
|
|
||||||
const result = await Promise.allSettled([promise]);
|
|
||||||
expect(result[0].status).toBe("rejected");
|
|
||||||
expect((result[0] as PromiseRejectedResult).reason).toBe(
|
|
||||||
"Prompt Cancelled",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reject promise with custom reason", async () => {
|
|
||||||
const ctx = createPromptContext();
|
|
||||||
const validator = (value: string) => value;
|
|
||||||
|
|
||||||
const promise = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
ctx.cancel("player1", "Custom cancel reason");
|
|
||||||
|
|
||||||
const result = await Promise.allSettled([promise]);
|
|
||||||
expect(result[0].status).toBe("rejected");
|
|
||||||
expect((result[0] as PromiseRejectedResult).reason).toBe(
|
|
||||||
"Custom cancel reason",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should do nothing when no prompt exists for player", () => {
|
|
||||||
const ctx = createPromptContext();
|
|
||||||
ctx.cancel("nonexistent");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should only cancel prompt for specific player", async () => {
|
|
||||||
const ctx = createPromptContext();
|
|
||||||
const validator = (value: string) => value;
|
|
||||||
|
|
||||||
const promise1 = ctx.prompt({ schema: mockSchema }, validator, "player1");
|
|
||||||
const promise2 = ctx.prompt({ schema: mockSchema }, validator, "player2");
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
ctx.cancel("player1");
|
|
||||||
ctx.tryCommit("player2", "test");
|
|
||||||
|
|
||||||
const [result1, result2] = await Promise.allSettled([promise1, promise2]);
|
|
||||||
expect(result1.status).toBe("rejected");
|
|
||||||
expect((result1 as PromiseRejectedResult).reason).toBe(
|
|
||||||
"Prompt Cancelled",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result2.status).toBe("fulfilled");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("handleCall", () => {
|
|
||||||
it("should be a middleware chain that can be extended", async () => {
|
|
||||||
const ctx = createPromptContext();
|
|
||||||
const validator = (value: string) => value;
|
|
||||||
|
|
||||||
let middlewareCalled = false;
|
|
||||||
ctx.handleCall.use(async (call, next) => {
|
|
||||||
middlewareCalled = true;
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
|
|
||||||
const promise = ctx.prompt({ schema: mockSchema }, validator);
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
expect(middlewareCalled).toBe(true);
|
|
||||||
ctx.tryCommit("global", "test");
|
|
||||||
await expect(promise).resolves.toBe("test");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove prompt from map after resolution", async () => {
|
|
||||||
const ctx = createPromptContext();
|
|
||||||
const validator = (value: string) => value;
|
|
||||||
|
|
||||||
const promise = ctx.prompt({ schema: mockSchema }, validator);
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
ctx.tryCommit("global", "test");
|
|
||||||
await promise;
|
|
||||||
|
|
||||||
const result = ctx.tryCommit("global", "another");
|
|
||||||
expect(result).toEqual({ ok: false, reason: "No Prompt" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue