TypeScript/Deno runtime bridge — Go gRPC server + Deno sidecar. The seed project that inspired the entire Core framework. - Module: forge.lthn.ai/core/ts - Package: ts (renamed from coredeno) - gRPC bridge: CoreService (Go→Deno) + DenoService (Deno→Go) - Deno runtime: worker isolation, module loading, permissions - Proto descriptor retains original path (regenerate with protoc later) Co-Authored-By: Virgil <virgil@lethean.io>
106 lines
3 KiB
TypeScript
106 lines
3 KiB
TypeScript
// CoreDeno Runtime Entry Point
|
|
// Connects to CoreGO via gRPC over Unix socket.
|
|
// Implements DenoService for module lifecycle management.
|
|
|
|
// Must be first import — patches http2 before @grpc/grpc-js loads.
|
|
import "./polyfill.ts";
|
|
|
|
import { createCoreClient, type CoreClient } from "./client.ts";
|
|
import { startDenoServer, type DenoServer } from "./server.ts";
|
|
import { ModuleRegistry } from "./modules.ts";
|
|
|
|
// Read required environment variables
|
|
const coreSocket = Deno.env.get("CORE_SOCKET");
|
|
if (!coreSocket) {
|
|
console.error("FATAL: CORE_SOCKET environment variable not set");
|
|
Deno.exit(1);
|
|
}
|
|
|
|
const denoSocket = Deno.env.get("DENO_SOCKET");
|
|
if (!denoSocket) {
|
|
console.error("FATAL: DENO_SOCKET environment variable not set");
|
|
Deno.exit(1);
|
|
}
|
|
|
|
console.error(`CoreDeno: CORE_SOCKET=${coreSocket}`);
|
|
console.error(`CoreDeno: DENO_SOCKET=${denoSocket}`);
|
|
|
|
// 1. Create module registry
|
|
const registry = new ModuleRegistry();
|
|
|
|
// 2. Start DenoService server (Go calls us here via JSON-RPC over Unix socket)
|
|
let denoServer: DenoServer;
|
|
try {
|
|
denoServer = await startDenoServer(denoSocket, registry);
|
|
console.error("CoreDeno: DenoService server started");
|
|
} catch (err) {
|
|
console.error(`FATAL: failed to start DenoService server: ${err}`);
|
|
Deno.exit(1);
|
|
}
|
|
|
|
// 3. Connect to CoreService (we call Go here) with retry
|
|
let coreClient: CoreClient;
|
|
{
|
|
coreClient = createCoreClient(coreSocket);
|
|
const maxRetries = 20;
|
|
let connected = false;
|
|
let lastErr: unknown;
|
|
for (let i = 0; i < maxRetries; i++) {
|
|
try {
|
|
const timeoutCall = <T>(p: Promise<T>): Promise<T> =>
|
|
Promise.race([
|
|
p,
|
|
new Promise<T>((_, reject) =>
|
|
setTimeout(() => reject(new Error("call timeout")), 2000),
|
|
),
|
|
]);
|
|
await timeoutCall(
|
|
coreClient.storeSet("_coredeno", "status", "connected"),
|
|
);
|
|
const resp = await timeoutCall(
|
|
coreClient.storeGet("_coredeno", "status"),
|
|
);
|
|
if (resp.found && resp.value === "connected") {
|
|
connected = true;
|
|
break;
|
|
}
|
|
} catch (err) {
|
|
lastErr = err;
|
|
if (i < 3 || i === 9 || i === 19) {
|
|
console.error(`CoreDeno: retry ${i}: ${err}`);
|
|
}
|
|
}
|
|
await new Promise((r) => setTimeout(r, 250));
|
|
}
|
|
if (!connected) {
|
|
console.error(
|
|
`FATAL: failed to connect to CoreService after retries, last error: ${lastErr}`,
|
|
);
|
|
denoServer.close();
|
|
Deno.exit(1);
|
|
}
|
|
console.error("CoreDeno: CoreService client connected");
|
|
}
|
|
|
|
// 4. Inject CoreClient into registry for I/O bridge
|
|
registry.setCoreClient(coreClient);
|
|
|
|
// 5. Signal readiness
|
|
console.error("CoreDeno: ready");
|
|
|
|
// 6. Keep alive until SIGTERM
|
|
const ac = new AbortController();
|
|
Deno.addSignalListener("SIGTERM", () => {
|
|
console.error("CoreDeno: shutting down");
|
|
ac.abort();
|
|
});
|
|
|
|
try {
|
|
await new Promise((_resolve, reject) => {
|
|
ac.signal.addEventListener("abort", () => reject(new Error("shutdown")));
|
|
});
|
|
} catch {
|
|
// Clean shutdown
|
|
coreClient.close();
|
|
denoServer.close();
|
|
}
|