import { Effect, FileSystem, Option, Schema } from "effect"; import { writeFileStringAtomically } from "./atomicWrite.ts";import { type ServerConfigShape } from "./config.ts";import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts"; export const PersistedServerRuntimeState = Schema.Struct({ version: Schema.Literal(1), pid: Schema.Int, host: Schema.optional(Schema.String), port: Schema.Int, origin: Schema.String, startedAt: Schema.String,});export type PersistedServerRuntimeState = typeof PersistedServerRuntimeState.Type; const decodePersistedServerRuntimeState = Schema.decodeUnknownEffect( Schema.fromJsonString(PersistedServerRuntimeState),); const runtimeOriginForConfig = ( config: Pick<ServerConfigShape, "host">, port: number,): PersistedServerRuntimeState["origin"] => { const hostname = config.host && !isWildcardHost(config.host) ? formatHostForUrl(config.host) : "127.0.0.1"; return `http://${hostname}:${port}`;}; export const makePersistedServerRuntimeState = (input: { readonly config: Pick<ServerConfigShape, "host">; readonly port: number;}): PersistedServerRuntimeState => ({ version: 1, pid: process.pid, ...(input.config.host ? { host: input.config.host } : {}), port: input.port, origin: runtimeOriginForConfig(input.config, input.port), startedAt: new Date().toISOString(),}); export const persistServerRuntimeState = (input: { readonly path: string; readonly state: PersistedServerRuntimeState;}) => writeFileStringAtomically({ filePath: input.path, contents: `${JSON.stringify(input.state)}\n`, }); export const clearPersistedServerRuntimeState = (path: string) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; yield* fs.remove(path, { force: true }).pipe(Effect.ignore({ log: true })); }); export const readPersistedServerRuntimeState = (path: string) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const exists = yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false)); if (!exists) { return Option.none<PersistedServerRuntimeState>(); } const raw = yield* fs.readFileString(path).pipe(Effect.orElseSucceed(() => "")); const trimmed = raw.trim(); if (trimmed.length === 0) { return Option.none<PersistedServerRuntimeState>(); } return yield* decodePersistedServerRuntimeState(trimmed).pipe(Effect.option); });
import { Effect, FileSystem, Option, Schema } from "effect";
import { writeFileStringAtomically } from "./atomicWrite.ts";
import { type ServerConfigShape } from "./config.ts";
import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts";
export const PersistedServerRuntimeState = Schema.Struct({
version: Schema.Literal(1),
pid: Schema.Int,
host: Schema.optional(Schema.String),
port: Schema.Int,
origin: Schema.String,
startedAt: Schema.String,
});
export type PersistedServerRuntimeState = typeof PersistedServerRuntimeState.Type;
const decodePersistedServerRuntimeState = Schema.decodeUnknownEffect(
Schema.fromJsonString(PersistedServerRuntimeState),
);
const runtimeOriginForConfig = (
config: Pick<ServerConfigShape, "host">,
port: number,
): PersistedServerRuntimeState["origin"] => {
const hostname =
config.host && !isWildcardHost(config.host) ? formatHostForUrl(config.host) : "127.0.0.1";
return `http://${hostname}:${port}`;
};
export const makePersistedServerRuntimeState = (input: {
readonly config: Pick<ServerConfigShape, "host">;
readonly port: number;
}): PersistedServerRuntimeState => ({
version: 1,
pid: process.pid,
...(input.config.host ? { host: input.config.host } : {}),
port: input.port,
origin: runtimeOriginForConfig(input.config, input.port),
startedAt: new Date().toISOString(),
});
export const persistServerRuntimeState = (input: {
readonly path: string;
readonly state: PersistedServerRuntimeState;
}) =>
writeFileStringAtomically({
filePath: input.path,
contents: `${JSON.stringify(input.state)}\n`,
});
export const clearPersistedServerRuntimeState = (path: string) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.remove(path, { force: true }).pipe(Effect.ignore({ log: true }));
});
export const readPersistedServerRuntimeState = (path: string) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const exists = yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false));
if (!exists) {
return Option.none<PersistedServerRuntimeState>();
}
const raw = yield* fs.readFileString(path).pipe(Effect.orElseSucceed(() => ""));
const trimmed = raw.trim();
if (trimmed.length === 0) {
return Option.none<PersistedServerRuntimeState>();
}
return yield* decodePersistedServerRuntimeState(trimmed).pipe(Effect.option);
});