go/pkg/coredeno/runtime/client.ts
Claude af98accc03
feat(coredeno): Tier 2 bidirectional bridge — Go↔Deno module lifecycle
Wire the CoreDeno sidecar into a fully bidirectional bridge:

- Deno→Go (gRPC): Deno connects as CoreService client via polyfilled
  @grpc/grpc-js over Unix socket. Polyfill patches Deno 2.x http2 gaps
  (getDefaultSettings, pre-connected socket handling, remoteSettings).
- Go→Deno (JSON-RPC): Go connects to Deno's newline-delimited JSON-RPC
  server for module lifecycle (LoadModule, UnloadModule, ModuleStatus).
  gRPC server direction avoided due to Deno http2.createServer limitations.
- ProcessStart/ProcessStop: gRPC handlers delegate to process.Service
  with manifest permission gating (run permissions).
- Deno runtime: main.ts boots DenoService server, connects CoreService
  client with retry + health-check round-trip, handles SIGTERM shutdown.

40 unit tests + 2 integration tests (Tier 1 boot + Tier 2 bidirectional).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 22:43:12 +00:00

95 lines
3.1 KiB
TypeScript

// CoreService gRPC client — Deno calls Go for I/O operations.
// All filesystem, store, and process operations route through this client.
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const PROTO_PATH = join(__dirname, "..", "proto", "coredeno.proto");
let packageDef: protoLoader.PackageDefinition | null = null;
function getProto(): any {
if (!packageDef) {
packageDef = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
}
return grpc.loadPackageDefinition(packageDef).coredeno as any;
}
export interface CoreClient {
raw: any;
storeGet(group: string, key: string): Promise<{ value: string; found: boolean }>;
storeSet(group: string, key: string, value: string): Promise<{ ok: boolean }>;
fileRead(path: string, moduleCode: string): Promise<{ content: string }>;
fileWrite(path: string, content: string, moduleCode: string): Promise<{ ok: boolean }>;
fileList(path: string, moduleCode: string): Promise<{ entries: Array<{ name: string; is_dir: boolean; size: number }> }>;
fileDelete(path: string, moduleCode: string): Promise<{ ok: boolean }>;
processStart(command: string, args: string[], moduleCode: string): Promise<{ process_id: string }>;
processStop(processId: string): Promise<{ ok: boolean }>;
close(): void;
}
function promisify<T>(client: any, method: string, request: any): Promise<T> {
return new Promise((resolve, reject) => {
client[method](request, (err: Error | null, response: T) => {
if (err) reject(err);
else resolve(response);
});
});
}
export function createCoreClient(socketPath: string): CoreClient {
const proto = getProto();
const client = new proto.CoreService(
`unix:${socketPath}`,
grpc.credentials.createInsecure(),
);
return {
raw: client,
storeGet(group: string, key: string) {
return promisify(client, "StoreGet", { group, key });
},
storeSet(group: string, key: string, value: string) {
return promisify(client, "StoreSet", { group, key, value });
},
fileRead(path: string, moduleCode: string) {
return promisify(client, "FileRead", { path, module_code: moduleCode });
},
fileWrite(path: string, content: string, moduleCode: string) {
return promisify(client, "FileWrite", { path, content, module_code: moduleCode });
},
fileList(path: string, moduleCode: string) {
return promisify(client, "FileList", { path, module_code: moduleCode });
},
fileDelete(path: string, moduleCode: string) {
return promisify(client, "FileDelete", { path, module_code: moduleCode });
},
processStart(command: string, args: string[], moduleCode: string) {
return promisify(client, "ProcessStart", { command, args, module_code: moduleCode });
},
processStop(processId: string) {
return promisify(client, "ProcessStop", { process_id: processId });
},
close() {
client.close();
},
};
}