1import { encodeFqn } from "@/FQN";
2import { localState } from "@/State/LocalState";
3import * as Deferred from "effect/Deferred";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Layer from "effect/Layer";
7import * as Path from "effect/Path";
8import * as PlatformError from "effect/PlatformError";
9import * as Schema from "effect/Schema";
10
11/** Only nonsecret row identity and lifecycle fields leave the filesystem barrier. */
12export const ResourceRow = Schema.Struct({
13 resourceType: Schema.String,
14 fqn: Schema.String,
15 instanceId: Schema.String,
16 status: Schema.String,
17 attr: Schema.optional(
18 Schema.Struct({
19 appName: Schema.String,
20 machineId: Schema.String,
21 machineIds: Schema.Array(Schema.String),
22 rolloutPending: Schema.optional(Schema.Boolean),
23 }),
24 ),
25});
26
27type ResourceRow = typeof ResourceRow.Type;
28
29/** Delay one final resource-row rename; all filesystem operations remain real. */
30export const delayedResourceWrite = (target: {
31 stack: string;
32 stage: string;
33 fqn: string;
34}) =>
35 Effect.gen(function* () {
36 const fs = yield* FileSystem.FileSystem;
37 const path = yield* Path.Path;
38 const held = yield* Deferred.make<ResourceRow>();
39 const released = yield* Deferred.make<void>();
40 const written = yield* Deferred.make<ResourceRow>();
41 const suffix = path.join(
42 ".alchemy",
43 "state",
44 target.stack,
45 target.stage,
46 `${encodeFqn(target.fqn)}.json`,
47 );
48 let intercepted = false;
49 const forwarded: FileSystem.FileSystem = {
50 ...fs,
51 rename: (from, to) =>
52 Effect.gen(function* () {
53 if (
54 !intercepted &&
55 to.endsWith(`${path.sep}${suffix}`) &&
56 from.startsWith(`${to}.`) &&
57 from.endsWith(".tmp")
58 ) {
59 const row = yield* fs.readFileString(from).pipe(
60 Effect.flatMap(
61 Schema.decodeUnknownEffect(Schema.fromJsonString(ResourceRow)),
62 ),
63 Effect.catchTag("SchemaError", () =>
64 Effect.fail(
65 PlatformError.badArgument({
66 module: "FileSystem",
67 method: "rename",
68 description: "Invalid delayed resource-row identity",
69 }),
70 ),
71 ),
72 );
73 if (
74 row.resourceType === "Fly.Machine" &&
75 row.fqn === target.fqn &&
76 row.status === "updated" &&
77 row.attr !== undefined
78 ) {
79 yield* Effect.sync(() => {
80 intercepted = true;
81 });
82 yield* Deferred.succeed(held, row);
83 yield* Deferred.await(released).pipe(
84 Effect.timeout("600 seconds"),
85 Effect.catchTag("TimeoutError", () =>
86 Effect.fail(
87 PlatformError.systemError({
88 _tag: "TimedOut",
89 module: "FileSystem",
90 method: "rename",
91 description:
92 "Delayed resource-row release exceeded 600 seconds",
93 }),
94 ),
95 ),
96 );
97 yield* fs.rename(from, to);
98 yield* Deferred.succeed(written, row);
99 return;
100 }
101 }
102 yield* fs.rename(from, to);
103 }),
104 };
105 return {
106 state: localState().pipe(
107 Layer.provide(Layer.succeed(FileSystem.FileSystem, forwarded)),
108 ),
109 wait: Deferred.await(held).pipe(Effect.timeout("600 seconds")),
110 release: Deferred.succeed(released, undefined).pipe(Effect.asVoid),
111 written: Deferred.await(written).pipe(Effect.timeout("600 seconds")),
112 };
113 });