Initial package implementation: operations registry, call protocol, and adapters

Extracted from alkhub_ts packages/core/operations/ and packages/core/mcp/.
- Runtime-agnostic (injected fs/env deps, no Deno globals)
- Direct @logtape/logtape import instead of logger wrapper
- PendingRequestMap with pubsub-wired call protocol
- Peer-dep isolation for MCP adapter (sub-path export)
- Schema const naming convention (XSchema + X type alias)
- 68 tests passing, build + lint + test all green
This commit is contained in:
2026-04-30 12:34:26 +00:00
parent 9c41f683ee
commit 29f0dd7af0
37 changed files with 9287 additions and 0 deletions

249
src/call.ts Normal file
View File

@@ -0,0 +1,249 @@
import { Type, type Static } from "@alkdev/typebox";
import { createPubSub, type PubSub } from "@alkdev/pubsub";
import { getLogger } from "@logtape/logtape";
import { OperationRegistry } from "./registry.js";
import { CallError, InfrastructureErrorCode, mapError } from "./error.js";
import { validateOrThrow } from "./validation.js";
import type { IOperationDefinition, Identity, OperationContext, AccessControl } from "./types.js";
const logger = getLogger("operations:call");
export const CallEventSchema = {
"call.requested": Type.Object({
requestId: Type.String(),
operationId: Type.String(),
input: Type.Unknown(),
parentRequestId: Type.Optional(Type.String()),
deadline: Type.Optional(Type.Number()),
identity: Type.Optional(Type.Object({
id: Type.String(),
scopes: Type.Array(Type.String()),
resources: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String()))),
})),
}),
"call.responded": Type.Object({
requestId: Type.String(),
output: Type.Unknown(),
}),
"call.aborted": Type.Object({
requestId: Type.String(),
}),
"call.error": Type.Object({
requestId: Type.String(),
code: Type.String(),
message: Type.String(),
details: Type.Optional(Type.Unknown()),
}),
} as const;
export type CallRequestedEvent = Static<typeof CallEventSchema["call.requested"]>;
export type CallRespondedEvent = Static<typeof CallEventSchema["call.responded"]>;
export type CallAbortedEvent = Static<typeof CallEventSchema["call.aborted"]>;
export type CallErrorEvent = Static<typeof CallEventSchema["call.error"]>;
export type CallEventMapValue = CallRequestedEvent | CallRespondedEvent | CallAbortedEvent | CallErrorEvent;
export const CallEventMap = CallEventSchema;
type CallPubSubMap = {
"call.requested": [CallRequestedEvent];
"call.responded": [CallRespondedEvent];
"call.aborted": [CallAbortedEvent];
"call.error": [CallErrorEvent];
};
interface PendingRequest {
resolve: (value: unknown) => void;
reject: (reason: unknown) => void;
deadline?: number;
timer?: ReturnType<typeof setTimeout>;
}
export interface CallHandlerConfig {
registry: OperationRegistry;
eventTarget?: EventTarget;
}
export type CallHandler = (event: CallRequestedEvent) => Promise<void>;
export class PendingRequestMap {
private requests = new Map<string, PendingRequest>();
private pubsub: PubSub<CallPubSubMap>;
constructor(eventTarget?: EventTarget) {
this.pubsub = createPubSub<CallPubSubMap>(
eventTarget ? { eventTarget: eventTarget as any } : undefined
);
this.setupSubscriptions();
}
private setupSubscriptions(): void {
const respondedIter = this.pubsub.subscribe("call.responded");
(async () => {
for await (const event of respondedIter) {
const responded = event as CallRespondedEvent;
const pending = this.requests.get(responded.requestId);
if (pending) {
if (pending.timer) clearTimeout(pending.timer);
this.requests.delete(responded.requestId);
pending.resolve(responded.output);
}
}
})();
const errorIter = this.pubsub.subscribe("call.error");
(async () => {
for await (const event of errorIter) {
const err = event as CallErrorEvent;
const pending = this.requests.get(err.requestId);
if (pending) {
if (pending.timer) clearTimeout(pending.timer);
this.requests.delete(err.requestId);
pending.reject(new CallError(err.code, err.message, err.details));
}
}
})();
const abortedIter = this.pubsub.subscribe("call.aborted");
(async () => {
for await (const event of abortedIter) {
const aborted = event as CallAbortedEvent;
const pending = this.requests.get(aborted.requestId);
if (pending) {
if (pending.timer) clearTimeout(pending.timer);
this.requests.delete(aborted.requestId);
pending.reject(new CallError(InfrastructureErrorCode.ABORTED, `Request ${aborted.requestId} was aborted`));
}
}
})();
}
async call(
operationId: string,
input: unknown,
options?: { parentRequestId?: string; deadline?: number; identity?: Identity },
): Promise<unknown> {
const requestId = crypto.randomUUID();
return new Promise((resolve, reject) => {
const pending: PendingRequest = { resolve, reject };
if (options?.deadline) {
pending.deadline = options.deadline;
pending.timer = setTimeout(() => {
this.requests.delete(requestId);
reject(new CallError(InfrastructureErrorCode.TIMEOUT, `Request ${requestId} timed out`, { deadline: options.deadline }));
}, options.deadline - Date.now());
}
this.requests.set(requestId, pending);
this.pubsub.publish("call.requested", {
requestId,
operationId,
input,
parentRequestId: options?.parentRequestId,
deadline: options?.deadline,
identity: options?.identity,
});
});
}
respond(requestId: string, output: unknown): void {
this.pubsub.publish("call.responded", {
requestId,
output,
});
}
emitError(requestId: string, code: string, message: string, details?: unknown): void {
this.pubsub.publish("call.error", {
requestId,
code,
message,
details,
});
}
abort(requestId: string): void {
const pending = this.requests.get(requestId);
if (pending) {
if (pending.timer) clearTimeout(pending.timer);
this.requests.delete(requestId);
this.pubsub.publish("call.aborted", { requestId });
pending.reject(new CallError(InfrastructureErrorCode.ABORTED, `Request ${requestId} was aborted`));
}
}
getPendingCount(): number {
return this.requests.size;
}
}
export function buildCallHandler(config: CallHandlerConfig): CallHandler {
const { registry } = config;
return async (event: CallRequestedEvent): Promise<void> => {
const { requestId, operationId, input, identity } = event;
try {
const operation = registry.get(operationId);
if (!operation) {
throw new CallError(
InfrastructureErrorCode.OPERATION_NOT_FOUND,
`Operation not found: ${operationId}`,
{ operationId },
);
}
const accessControl: AccessControl = operation.accessControl as AccessControl;
if (identity && !checkAccess(accessControl, identity)) {
throw new CallError(
InfrastructureErrorCode.ACCESS_DENIED,
`Access denied for operation: ${operationId}`,
{ requiredScopes: accessControl.requiredScopes },
);
}
const context: OperationContext = {
requestId,
parentRequestId: event.parentRequestId,
identity,
};
validateOrThrow(operation.inputSchema, input, `Input validation for ${operationId}`);
await operation.handler(input, context);
} catch (error) {
const callError = mapError(error);
throw callError;
}
};
}
function checkAccess(accessControl: AccessControl, identity: Identity): boolean {
const { requiredScopes, requiredScopesAny, resourceType, resourceAction } = accessControl;
if (requiredScopes.length > 0) {
const hasAll = requiredScopes.every((scope: string) => identity.scopes.includes(scope));
if (!hasAll) return false;
}
if (requiredScopesAny && requiredScopesAny.length > 0) {
const hasAny = requiredScopesAny.some((scope: string) => identity.scopes.includes(scope));
if (!hasAny) return false;
}
if (resourceType && resourceAction && identity.resources) {
for (const [key, actions] of Object.entries(identity.resources)) {
if (key.startsWith(`${resourceType}:`) && actions.includes(resourceAction)) {
return true;
}
}
return false;
}
return true;
}