23b43be952
* WIP realtime streams * Handle realtime with large payloads or outputs #1451 * feat: optimize Redis stream handling with batching Add STREAM_ORIGIN to environment schema. Improve performance in RealtimeStreams by using TextDecoderStream for simpler text decoding and implementing batching of XADD commands for Redis streams. Limit stream size using MAXLEN option. Update environment variable repository with new variable type. Adjust import statements for Redis key and value types. * 🔧 chore: add dev dependencies for bundle analysis * add metadata tests and a few more utilties * Add stream tests and improve streaming * Added AI tool tasks, descriptions to tasks * Use the config file path to determine the workingDir, then the package.json path * Remove stream test files * useTaskTrigger react hook that allows triggering a task from the client * Add streaming support for the realtime react hooks * Add ability to stream results after useTaskTrigger * Improve the stream throttling * Use the runId as the ID key to bust the cache after triggering * Upgrade to to the latest electric sql client and server * Make realtime server backwards compat with 3.1.2 release * Pass the runId into useRealtimeRun * Fix scopes when specifiying reading all runs * WIP @trigger.dev/rsc package * Various fixes and accepted recommendations by CodeRabbit * Regenerate pnpm lock file * A couple tweaks to rsc and give up on rendering react in tasks for now * Add changeset * Remove triggerRequest from the useEffect deps * Improve realtime & frontend authentication errors * Fixed authorization tests * Remove unnecessary log * Add metadata.stream limits and improve the metadata streams structure * Streams can now have up to 2500 entries * Various coderabbit fixes * additional react-hooks jsdocs
170 lines
5.4 KiB
TypeScript
170 lines
5.4 KiB
TypeScript
import { createRequestHandler } from "@remix-run/express";
|
|
import { broadcastDevReady, logDevReady } from "@remix-run/server-runtime";
|
|
import compression from "compression";
|
|
import type { Server as EngineServer } from "engine.io";
|
|
import express from "express";
|
|
import morgan from "morgan";
|
|
import { nanoid } from "nanoid";
|
|
import path from "path";
|
|
import type { Server as IoServer } from "socket.io";
|
|
import { WebSocketServer } from "ws";
|
|
import { RateLimitMiddleware } from "~/services/apiRateLimit.server";
|
|
import { type RunWithHttpContextFunction } from "~/services/httpAsyncStorage.server";
|
|
import { RegistryProxy } from "~/v3/registryProxy.server";
|
|
|
|
const app = express();
|
|
|
|
if (process.env.DISABLE_COMPRESSION !== "1") {
|
|
app.use(compression());
|
|
}
|
|
|
|
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
|
|
app.disable("x-powered-by");
|
|
|
|
// Remix fingerprints its assets so we can cache forever.
|
|
app.use("/build", express.static("public/build", { immutable: true, maxAge: "1y" }));
|
|
|
|
// Everything else (like favicon.ico) is cached for an hour. You may want to be
|
|
// more aggressive with this caching.
|
|
app.use(express.static("public", { maxAge: "1h" }));
|
|
|
|
app.use(morgan("tiny"));
|
|
|
|
process.title = "node webapp-server";
|
|
|
|
const MODE = process.env.NODE_ENV;
|
|
const BUILD_DIR = path.join(process.cwd(), "build");
|
|
const build = require(BUILD_DIR);
|
|
|
|
const port = process.env.REMIX_APP_PORT || process.env.PORT || 3000;
|
|
|
|
if (process.env.HTTP_SERVER_DISABLED !== "true") {
|
|
const socketIo: { io: IoServer } | undefined = build.entry.module.socketIo;
|
|
const wss: WebSocketServer | undefined = build.entry.module.wss;
|
|
const registryProxy: RegistryProxy | undefined = build.entry.module.registryProxy;
|
|
const apiRateLimiter: RateLimitMiddleware = build.entry.module.apiRateLimiter;
|
|
const runWithHttpContext: RunWithHttpContextFunction = build.entry.module.runWithHttpContext;
|
|
|
|
if (registryProxy && process.env.ENABLE_REGISTRY_PROXY === "true") {
|
|
console.log(`🐳 Enabling container registry proxy to ${registryProxy.origin}`);
|
|
|
|
// Adjusted to match /v2 and any subpath under /v2
|
|
app.all("/v2/*", async (req, res) => {
|
|
await registryProxy.call(req, res);
|
|
});
|
|
|
|
// This might also be necessary if you need to explicitly match /v2 as well
|
|
app.all("/v2", async (req, res) => {
|
|
await registryProxy.call(req, res);
|
|
});
|
|
}
|
|
|
|
app.use((req, res, next) => {
|
|
// helpful headers:
|
|
res.set("Strict-Transport-Security", `max-age=${60 * 60 * 24 * 365 * 100}`);
|
|
|
|
// /clean-urls/ -> /clean-urls
|
|
if (req.path.endsWith("/") && req.path.length > 1) {
|
|
const query = req.url.slice(req.path.length);
|
|
const safepath = req.path.slice(0, -1).replace(/\/+/g, "/");
|
|
res.redirect(301, safepath + query);
|
|
return;
|
|
}
|
|
next();
|
|
});
|
|
|
|
app.use((req, res, next) => {
|
|
// Generate a unique request ID for each request
|
|
const requestId = nanoid();
|
|
|
|
runWithHttpContext({ requestId, path: req.url, host: req.hostname, method: req.method }, next);
|
|
});
|
|
|
|
if (process.env.DASHBOARD_AND_API_DISABLED !== "true") {
|
|
app.use(apiRateLimiter);
|
|
|
|
app.all(
|
|
"*",
|
|
// @ts-ignore
|
|
createRequestHandler({
|
|
build,
|
|
mode: MODE,
|
|
})
|
|
);
|
|
} else {
|
|
// we need to do the health check here at /healthcheck
|
|
app.get("/healthcheck", (req, res) => {
|
|
res.status(200).send("OK");
|
|
});
|
|
}
|
|
|
|
const server = app.listen(port, () => {
|
|
console.log(`✅ server ready: http://localhost:${port} [NODE_ENV: ${MODE}]`);
|
|
|
|
if (MODE === "development") {
|
|
broadcastDevReady(build)
|
|
.then(() => logDevReady(build))
|
|
.catch(console.error);
|
|
}
|
|
});
|
|
|
|
server.keepAliveTimeout = 65 * 1000;
|
|
|
|
process.on("SIGTERM", () => {
|
|
server.close((err) => {
|
|
if (err) {
|
|
console.error("Error closing express server:", err);
|
|
} else {
|
|
console.log("Express server closed gracefully.");
|
|
}
|
|
});
|
|
});
|
|
|
|
socketIo?.io.attach(server);
|
|
server.removeAllListeners("upgrade"); // prevent duplicate upgrades from listeners created by io.attach()
|
|
|
|
server.on("upgrade", async (req, socket, head) => {
|
|
console.log(
|
|
`Attemping to upgrade connection at url ${req.url} with headers: ${JSON.stringify(
|
|
req.headers
|
|
)}`
|
|
);
|
|
|
|
socket.on("error", (err) => {
|
|
console.error("Connection upgrade error:", err);
|
|
});
|
|
|
|
const url = new URL(req.url ?? "", "http://localhost");
|
|
|
|
// Upgrade socket.io connection
|
|
if (url.pathname.startsWith("/socket.io/")) {
|
|
console.log(`Socket.io client connected, upgrading their connection...`);
|
|
|
|
// https://github.com/socketio/socket.io/issues/4693
|
|
(socketIo?.io.engine as EngineServer).handleUpgrade(req, socket, head);
|
|
return;
|
|
}
|
|
|
|
// Only upgrade the connecting if the path is `/ws`
|
|
if (url.pathname !== "/ws") {
|
|
// Setting the socket.destroy() error param causes an error event to be emitted which needs to be handled with socket.on("error") to prevent uncaught exceptions.
|
|
socket.destroy(
|
|
new Error(
|
|
"Cannot connect because of invalid path: Please include `/ws` in the path of your upgrade request."
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
console.log(`Client connected, upgrading their connection...`);
|
|
|
|
// Handle the WebSocket connection
|
|
wss?.handleUpgrade(req, socket, head, (ws) => {
|
|
wss?.emit("connection", ws, req);
|
|
});
|
|
});
|
|
} else {
|
|
require(BUILD_DIR);
|
|
console.log(`✅ app ready (skipping http server)`);
|
|
}
|