| 1 | /** @effect-diagnostics anyUnknownInErrorContext:off */ |
| 2 | |
| 3 | /** |
| 4 | * INTERNAL — the "image source changed" trigger stream shared by the floci |
| 5 | * dev providers that rebuild a Docker image on file change |
| 6 | * ([ECS FlociTaskProvider](../ECS/FlociTaskProvider.ts), |
| 7 | * [ECS FlociServiceProvider](../ECS/FlociServiceProvider.ts), |
| 8 | * [Lambda FlociMicrovmImageProvider](../Lambda/FlociMicrovmImageProvider.ts)). |
| 9 | * NOT exported from any service `index.ts`. |
| 10 | */ |
| 11 | |
| 12 | import * as Effect from "effect/Effect"; |
| 13 | import * as FileSystem from "effect/FileSystem"; |
| 14 | import * as Path from "effect/Path"; |
| 15 | import * as Stream from "effect/Stream"; |
| 16 | import * as Bundle from "../../Bundle/Bundle.ts"; |
| 17 | import { isInlineDockerfile } from "../../Docker/Dockerfile.ts"; |
| 18 | import { |
| 19 | imageSourceKind, |
| 20 | makeBunBootstrap, |
| 21 | makeImageSource, |
| 22 | type BundledImageSource, |
| 23 | type ImageSourceLike, |
| 24 | } from "../ECR/ImageSource.ts"; |
| 25 | |
| 26 | /** |
| 27 | * A stream of "the image source changed" triggers for a props bag: |
| 28 | * |
| 29 | * - `main` — a `Bundle.watch` over the exact module graph the deploy |
| 30 | * bundles (rebuild successes are the trigger; rebuild errors are logged |
| 31 | * and swallowed); |
| 32 | * - `context` — a debounced recursive `fs.watch` of the build context (and |
| 33 | * the Dockerfile's directory when it lives outside the context); |
| 34 | * - `image` (registry ref) / inline-dockerfile-only — nothing local to |
| 35 | * watch: an empty stream. |
| 36 | */ |
| 37 | export const imageSourceTrigger = Effect.fn(function* (options: { |
| 38 | /** Logical id, for log prefixes. */ |
| 39 | id: string; |
| 40 | source: ImageSourceLike; |
| 41 | isExternal: boolean | undefined; |
| 42 | }) { |
| 43 | const { id, source } = options; |
| 44 | const kind = imageSourceKind(source); |
| 45 | |
| 46 | if (kind === "main") { |
| 47 | const imageSource = yield* makeImageSource; |
| 48 | const plan = yield* imageSource.watchMain({ |
| 49 | source: source as BundledImageSource, |
| 50 | isExternal: options.isExternal, |
| 51 | bootstrap: makeBunBootstrap(source.handler ?? "default"), |
| 52 | }); |
| 53 | return Bundle.watch(plan.inputOptions, plan.outputOptions, plan.extra).pipe( |
| 54 | Stream.tap((event) => |
| 55 | event._tag === "Error" |
| 56 | ? Effect.logWarning( |
| 57 | `[alchemy dev] ${id}: rebuild failed: ${event.error.message}`, |
| 58 | ) |
| 59 | : Effect.void, |
| 60 | ), |
| 61 | Stream.filter((event) => event._tag === "Success"), |
| 62 | Stream.debounce("200 millis"), |
| 63 | Stream.map(() => undefined), |
| 64 | ) as Stream.Stream<void>; |
| 65 | } |
| 66 | |
| 67 | if (kind === "context") { |
| 68 | if (source.context === undefined) { |
| 69 | // Inline-dockerfile-only builds have no local files to watch — the |
| 70 | // content is a prop, and prop changes flow through the dev diff. |
| 71 | return Stream.empty as Stream.Stream<void>; |
| 72 | } |
| 73 | const fs = yield* FileSystem.FileSystem; |
| 74 | const path = yield* Path.Path; |
| 75 | const context = path.resolve(source.context); |
| 76 | const streams: Stream.Stream<unknown, any>[] = [ |
| 77 | fs.watch(context, { recursive: true }), |
| 78 | ]; |
| 79 | // A path Dockerfile living OUTSIDE the context dir is watched too. |
| 80 | if ( |
| 81 | source.dockerfile !== undefined && |
| 82 | !isInlineDockerfile(source.dockerfile) |
| 83 | ) { |
| 84 | const dockerfile = path.resolve(source.dockerfile); |
| 85 | if (!dockerfile.startsWith(`${context}/`)) { |
| 86 | streams.push(fs.watch(path.dirname(dockerfile))); |
| 87 | } |
| 88 | } |
| 89 | return Stream.mergeAll(streams, { concurrency: streams.length }).pipe( |
| 90 | Stream.debounce("300 millis"), |
| 91 | Stream.map(() => undefined), |
| 92 | Stream.catchCause((cause) => |
| 93 | Stream.fromEffect( |
| 94 | Effect.logWarning(`[alchemy dev] ${id}: context watch failed`, cause), |
| 95 | ).pipe(Stream.drain), |
| 96 | ), |
| 97 | ) as Stream.Stream<void>; |
| 98 | } |
| 99 | |
| 100 | // Registry `image` refs have nothing local to watch. |
| 101 | return Stream.empty as Stream.Stream<void>; |
| 102 | }); |