authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 15:15:05-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 15:15:05-07:00
log90f1936fade32015253aadff6d43f2271580f4c3
treedebe287cdac676711f28f43c94bbf67bcd4cd981
parent42ff43915a2ed5c0f190019c0c1a6a1cb68658c6
signature Signed by PGP key B5690EEEBB952194

[codex] Structure server runtime-state failures (#3319)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 251 insertions(+), 8 deletions(-)

apps/server/src/serverRuntimeState.test.ts created+167
......@@ -0,0 +1,167 @@
1import { assert, describe, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Logger from "effect/Logger";
7import * as Option from "effect/Option";
8import * as Path from "effect/Path";
9import * as References from "effect/References";
10import * as Schema from "effect/Schema";
11
12import * as ServerRuntimeState from "./serverRuntimeState.ts";
13
14const isServerRuntimeStateError = Schema.is(ServerRuntimeState.ServerRuntimeStateError);
15
16interface CapturedLog {
17 readonly message: unknown;
18 readonly annotations: Readonly<Record<string, unknown>>;
19}
20
21describe("serverRuntimeState", () => {
22 it.effect("persists and reads the runtime state", () =>
23 Effect.gen(function* () {
24 const fileSystem = yield* FileSystem.FileSystem;
25 const path = yield* Path.Path;
26 const root = yield* fileSystem.makeTempDirectoryScoped({
27 prefix: "t3-server-runtime-state-test-",
28 });
29 const statePath = path.join(root, "runtime", "server.json");
30 const state: ServerRuntimeState.PersistedServerRuntimeState = {
31 version: 1,
32 pid: 123,
33 host: "127.0.0.1",
34 port: 4_971,
35 origin: "http://127.0.0.1:4971",
36 startedAt: "2026-06-20T00:00:00.000Z",
37 };
38
39 yield* ServerRuntimeState.persistServerRuntimeState({ path: statePath, state });
40 const restored = yield* ServerRuntimeState.readPersistedServerRuntimeState(statePath);
41
42 assert.deepEqual(Option.getOrThrow(restored), state);
43 }).pipe(Effect.provide(NodeServices.layer)),
44 );
45
46 it.effect("treats a missing runtime state file as absent", () =>
47 Effect.gen(function* () {
48 const fileSystem = yield* FileSystem.FileSystem;
49 const path = yield* Path.Path;
50 const root = yield* fileSystem.makeTempDirectoryScoped({
51 prefix: "t3-server-runtime-state-test-",
52 });
53
54 const restored = yield* ServerRuntimeState.readPersistedServerRuntimeState(
55 path.join(root, "missing.json"),
56 );
57
58 assert.isTrue(Option.isNone(restored));
59 }).pipe(Effect.provide(NodeServices.layer)),
60 );
61
62 it.effect("preserves malformed state decode failures", () => {
63 const logs: CapturedLog[] = [];
64 const logger = Logger.make(({ fiber, message }) => {
65 logs.push({
66 message,
67 annotations: fiber.getRef(References.CurrentLogAnnotations),
68 });
69 });
70
71 return Effect.gen(function* () {
72 const fileSystem = yield* FileSystem.FileSystem;
73 const path = yield* Path.Path;
74 const root = yield* fileSystem.makeTempDirectoryScoped({
75 prefix: "t3-server-runtime-state-test-",
76 });
77 const statePath = path.join(root, "server.json");
78 yield* fileSystem.writeFileString(statePath, "{not json");
79
80 const restored = yield* ServerRuntimeState.readPersistedServerRuntimeState(statePath);
81
82 assert.isTrue(Option.isNone(restored));
83 assert.equal(logs[0]?.message, `Failed to decode server runtime state at ${statePath}.`);
84 const error = logs[0]?.annotations.cause;
85 assert.isTrue(isServerRuntimeStateError(error));
86 if (isServerRuntimeStateError(error)) {
87 assert.equal(error.operation, "decode");
88 assert.equal(error.statePath, statePath);
89 assert.equal(error.message, `Failed to decode server runtime state at ${statePath}.`);
90 assert.deepInclude(error.cause, { _tag: "SchemaError" });
91 }
92 }).pipe(
93 Effect.provide(
94 Layer.merge(NodeServices.layer, Logger.layer([logger], { mergeWithExisting: false })),
95 ),
96 );
97 });
98
99 it.effect("preserves runtime state read failures", () => {
100 const logs: CapturedLog[] = [];
101 const logger = Logger.make(({ fiber, message }) => {
102 logs.push({
103 message,
104 annotations: fiber.getRef(References.CurrentLogAnnotations),
105 });
106 });
107
108 return Effect.gen(function* () {
109 const fileSystem = yield* FileSystem.FileSystem;
110 const path = yield* Path.Path;
111 const root = yield* fileSystem.makeTempDirectoryScoped({
112 prefix: "t3-server-runtime-state-test-",
113 });
114 const statePath = path.join(root, "server.json");
115 yield* fileSystem.makeDirectory(statePath);
116
117 const restored = yield* ServerRuntimeState.readPersistedServerRuntimeState(statePath);
118
119 assert.isTrue(Option.isNone(restored));
120 assert.equal(logs[0]?.message, `Failed to read server runtime state at ${statePath}.`);
121 const error = logs[0]?.annotations.cause;
122 assert.isTrue(isServerRuntimeStateError(error));
123 if (isServerRuntimeStateError(error)) {
124 assert.equal(error.operation, "read");
125 assert.equal(error.statePath, statePath);
126 assert.equal(error.message, `Failed to read server runtime state at ${statePath}.`);
127 assert.deepInclude(error.cause, { _tag: "PlatformError" });
128 }
129 }).pipe(
130 Effect.provide(
131 Layer.merge(NodeServices.layer, Logger.layer([logger], { mergeWithExisting: false })),
132 ),
133 );
134 });
135
136 it.effect("preserves runtime state persistence failures", () =>
137 Effect.gen(function* () {
138 const fileSystem = yield* FileSystem.FileSystem;
139 const path = yield* Path.Path;
140 const root = yield* fileSystem.makeTempDirectoryScoped({
141 prefix: "t3-server-runtime-state-test-",
142 });
143 const blockedDirectory = path.join(root, "not-a-directory");
144 const statePath = path.join(blockedDirectory, "server.json");
145 yield* fileSystem.writeFileString(blockedDirectory, "blocked");
146
147 const error = yield* ServerRuntimeState.persistServerRuntimeState({
148 path: statePath,
149 state: {
150 version: 1,
151 pid: 123,
152 port: 4_971,
153 origin: "http://127.0.0.1:4971",
154 startedAt: "2026-06-20T00:00:00.000Z",
155 },
156 }).pipe(Effect.flip);
157
158 assert.isTrue(isServerRuntimeStateError(error));
159 if (isServerRuntimeStateError(error)) {
160 assert.equal(error.operation, "persist");
161 assert.equal(error.statePath, statePath);
162 assert.equal(error.message, `Failed to persist server runtime state at ${statePath}.`);
163 assert.deepInclude(error.cause, { _tag: "PlatformError" });
164 }
165 }).pipe(Effect.provide(NodeServices.layer)),
166 );
167});
apps/server/src/serverRuntimeState.ts+84-8
......@@ -18,6 +18,19 @@ export const PersistedServerRuntimeState = Schema.Struct({
1818});
1919export type PersistedServerRuntimeState = typeof PersistedServerRuntimeState.Type;
2020
21export class ServerRuntimeStateError extends Schema.TaggedErrorClass<ServerRuntimeStateError>()(
22 "ServerRuntimeStateError",
23 {
24 operation: Schema.Literals(["persist", "read", "decode", "clear"]),
25 statePath: Schema.String,
26 cause: Schema.Defect(),
27 },
28) {
29 override get message(): string {
30 return `Failed to ${this.operation} server runtime state at ${this.statePath}.`;
31 }
32}
33
2134const decodePersistedServerRuntimeState = Schema.decodeUnknownEffect(
2235 Schema.fromJsonString(PersistedServerRuntimeState),
2336);
......@@ -51,27 +64,90 @@ export const persistServerRuntimeState = (input: {
5164 writeFileStringAtomically({
5265 filePath: input.path,
5366 contents: `${JSON.stringify(input.state)}\n`,
54 });
67 }).pipe(
68 Effect.mapError(
69 (cause) =>
70 new ServerRuntimeStateError({
71 operation: "persist",
72 statePath: input.path,
73 cause,
74 }),
75 ),
76 );
5577
5678export const clearPersistedServerRuntimeState = (path: string) =>
5779 Effect.gen(function* () {
5880 const fs = yield* FileSystem.FileSystem;
59 yield* fs.remove(path, { force: true }).pipe(Effect.ignore({ log: true }));
81 yield* fs.remove(path, { force: true }).pipe(
82 Effect.mapError(
83 (cause) =>
84 new ServerRuntimeStateError({
85 operation: "clear",
86 statePath: path,
87 cause,
88 }),
89 ),
90 Effect.catchTags({
91 ServerRuntimeStateError: (error) =>
92 Effect.logWarning(error.message).pipe(
93 Effect.annotateLogs({
94 operation: error.operation,
95 statePath: error.statePath,
96 cause: error,
97 }),
98 ),
99 }),
100 );
60101 });
61102
62103export const readPersistedServerRuntimeState = (path: string) =>
63104 Effect.gen(function* () {
64105 const fs = yield* FileSystem.FileSystem;
65 const exists = yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false));
66 if (!exists) {
106 const raw = yield* fs.readFileString(path).pipe(
107 Effect.matchEffect({
108 onFailure: (cause) =>
109 cause.reason._tag === "NotFound"
110 ? Effect.succeed(Option.none<string>())
111 : Effect.fail(
112 new ServerRuntimeStateError({
113 operation: "read",
114 statePath: path,
115 cause,
116 }),
117 ),
118 onSuccess: (contents) => Effect.succeed(Option.some(contents)),
119 }),
120 );
121 if (Option.isNone(raw)) {
67122 return Option.none<PersistedServerRuntimeState>();
68123 }
69124
70 const raw = yield* fs.readFileString(path).pipe(Effect.orElseSucceed(() => ""));
71 const trimmed = raw.trim();
125 const trimmed = raw.value.trim();
72126 if (trimmed.length === 0) {
73127 return Option.none<PersistedServerRuntimeState>();
74128 }
75129
76 return yield* decodePersistedServerRuntimeState(trimmed).pipe(Effect.option);
77 });
130 return yield* decodePersistedServerRuntimeState(trimmed).pipe(
131 Effect.map(Option.some),
132 Effect.mapError(
133 (cause) =>
134 new ServerRuntimeStateError({
135 operation: "decode",
136 statePath: path,
137 cause,
138 }),
139 ),
140 );
141 }).pipe(
142 Effect.catchTags({
143 ServerRuntimeStateError: (error) =>
144 Effect.logWarning(error.message).pipe(
145 Effect.annotateLogs({
146 operation: error.operation,
147 statePath: error.statePath,
148 cause: error,
149 }),
150 Effect.as(Option.none<PersistedServerRuntimeState>()),
151 ),
152 }),
153 );