@alchemy.run/fly 2.0.0-beta.85 → 2.0.0-beta.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/App.d.ts +26 -4
- package/lib/App.d.ts.map +1 -1
- package/lib/App.js +25 -2
- package/lib/App.js.map +1 -1
- package/lib/AuthProvider.d.ts +1 -1
- package/lib/AuthProvider.d.ts.map +1 -1
- package/lib/AuthProvider.js.map +1 -1
- package/lib/BindService.d.ts +7 -7
- package/lib/BindService.d.ts.map +1 -1
- package/lib/BindService.js +20 -27
- package/lib/BindService.js.map +1 -1
- package/lib/Bootstrap.d.ts.map +1 -1
- package/lib/Bootstrap.js +2 -1
- package/lib/Bootstrap.js.map +1 -1
- package/lib/Bucket.d.ts +36 -2
- package/lib/Bucket.d.ts.map +1 -1
- package/lib/Bucket.js +181 -28
- package/lib/Bucket.js.map +1 -1
- package/lib/BucketBinding.d.ts.map +1 -1
- package/lib/BucketBinding.js.map +1 -1
- package/lib/Catalog.d.ts.map +1 -1
- package/lib/Catalog.js.map +1 -1
- package/lib/Certificate.d.ts +25 -1
- package/lib/Certificate.d.ts.map +1 -1
- package/lib/Certificate.js +18 -0
- package/lib/Certificate.js.map +1 -1
- package/lib/Checkpoint.d.ts.map +1 -1
- package/lib/Checkpoint.js.map +1 -1
- package/lib/CheckpointHttp.d.ts.map +1 -1
- package/lib/CheckpointHttp.js.map +1 -1
- package/lib/ConnectPostgres.d.ts.map +1 -1
- package/lib/ConnectPostgres.js.map +1 -1
- package/lib/ConnectPostgresHttp.d.ts.map +1 -1
- package/lib/ConnectPostgresHttp.js.map +1 -1
- package/lib/Contributions.d.ts +1 -1
- package/lib/Contributions.d.ts.map +1 -1
- package/lib/Contributions.js.map +1 -1
- package/lib/Credentials.d.ts +4 -3
- package/lib/Credentials.d.ts.map +1 -1
- package/lib/Credentials.js +9 -5
- package/lib/Credentials.js.map +1 -1
- package/lib/Decrypt.d.ts.map +1 -1
- package/lib/Decrypt.js.map +1 -1
- package/lib/DecryptHttp.d.ts.map +1 -1
- package/lib/DecryptHttp.js.map +1 -1
- package/lib/DeleteObject.d.ts.map +1 -1
- package/lib/DeleteObject.js.map +1 -1
- package/lib/DeleteObjectHttp.d.ts.map +1 -1
- package/lib/DeleteObjectHttp.js.map +1 -1
- package/lib/Deployment.d.ts.map +1 -1
- package/lib/Deployment.js.map +1 -1
- package/lib/DeploymentImages.d.ts.map +1 -1
- package/lib/DeploymentImages.js.map +1 -1
- package/lib/DeploymentState.d.ts.map +1 -1
- package/lib/DeploymentState.js.map +1 -1
- package/lib/Encrypt.d.ts.map +1 -1
- package/lib/Encrypt.js.map +1 -1
- package/lib/EncryptHttp.d.ts.map +1 -1
- package/lib/EncryptHttp.js.map +1 -1
- package/lib/Environment.d.ts.map +1 -1
- package/lib/Environment.js.map +1 -1
- package/lib/Errors.d.ts.map +1 -1
- package/lib/Errors.js.map +1 -1
- package/lib/Exec.d.ts.map +1 -1
- package/lib/Exec.js.map +1 -1
- package/lib/ExecHttp.d.ts.map +1 -1
- package/lib/ExecHttp.js.map +1 -1
- package/lib/GetObject.d.ts.map +1 -1
- package/lib/GetObject.js.map +1 -1
- package/lib/GetObjectHttp.d.ts.map +1 -1
- package/lib/GetObjectHttp.js.map +1 -1
- package/lib/GetSecret.d.ts.map +1 -1
- package/lib/GetSecret.js.map +1 -1
- package/lib/GetSecretHttp.d.ts.map +1 -1
- package/lib/GetSecretHttp.js.map +1 -1
- package/lib/HeadObject.d.ts.map +1 -1
- package/lib/HeadObject.js.map +1 -1
- package/lib/HeadObjectHttp.d.ts.map +1 -1
- package/lib/HeadObjectHttp.js.map +1 -1
- package/lib/IpAssignment.d.ts +24 -1
- package/lib/IpAssignment.d.ts.map +1 -1
- package/lib/IpAssignment.js +17 -0
- package/lib/IpAssignment.js.map +1 -1
- package/lib/ListObjectsV2.d.ts.map +1 -1
- package/lib/ListObjectsV2.js.map +1 -1
- package/lib/ListObjectsV2Http.d.ts.map +1 -1
- package/lib/ListObjectsV2Http.js.map +1 -1
- package/lib/ListSecrets.d.ts.map +1 -1
- package/lib/ListSecrets.js.map +1 -1
- package/lib/ListSecretsHttp.d.ts.map +1 -1
- package/lib/ListSecretsHttp.js.map +1 -1
- package/lib/Machine.d.ts +26 -1
- package/lib/Machine.d.ts.map +1 -1
- package/lib/Machine.js +16 -1
- package/lib/Machine.js.map +1 -1
- package/lib/MachineContainers.d.ts.map +1 -1
- package/lib/MachineContainers.js.map +1 -1
- package/lib/Metadata.d.ts.map +1 -1
- package/lib/Metadata.js.map +1 -1
- package/lib/MountVolume.d.ts +7 -2
- package/lib/MountVolume.d.ts.map +1 -1
- package/lib/MountVolume.js.map +1 -1
- package/lib/Postgres.d.ts +33 -1
- package/lib/Postgres.d.ts.map +1 -1
- package/lib/Postgres.js +20 -0
- package/lib/Postgres.js.map +1 -1
- package/lib/PostgresMigrations.d.ts.map +1 -1
- package/lib/PostgresMigrations.js.map +1 -1
- package/lib/Providers.d.ts +1 -1
- package/lib/Providers.d.ts.map +1 -1
- package/lib/Providers.js.map +1 -1
- package/lib/PutObject.d.ts.map +1 -1
- package/lib/PutObject.js.map +1 -1
- package/lib/PutObjectHttp.d.ts.map +1 -1
- package/lib/PutObjectHttp.js.map +1 -1
- package/lib/ReadRedis.d.ts.map +1 -1
- package/lib/ReadRedis.js.map +1 -1
- package/lib/ReadRedisHttp.d.ts.map +1 -1
- package/lib/ReadRedisHttp.js.map +1 -1
- package/lib/ReadWriteRedis.d.ts.map +1 -1
- package/lib/ReadWriteRedis.js.map +1 -1
- package/lib/ReadWriteRedisHttp.d.ts.map +1 -1
- package/lib/ReadWriteRedisHttp.js.map +1 -1
- package/lib/Redis.d.ts +21 -1
- package/lib/Redis.d.ts.map +1 -1
- package/lib/Redis.js +28 -0
- package/lib/Redis.js.map +1 -1
- package/lib/RedisBinding.d.ts.map +1 -1
- package/lib/RedisBinding.js.map +1 -1
- package/lib/RedisHttp.d.ts.map +1 -1
- package/lib/RedisHttp.js.map +1 -1
- package/lib/Region.d.ts.map +1 -1
- package/lib/Region.js.map +1 -1
- package/lib/Secret.d.ts +22 -1
- package/lib/Secret.d.ts.map +1 -1
- package/lib/Secret.js +15 -0
- package/lib/Secret.js.map +1 -1
- package/lib/SecretHttp.d.ts.map +1 -1
- package/lib/SecretHttp.js.map +1 -1
- package/lib/SecretKey.d.ts +22 -1
- package/lib/SecretKey.d.ts.map +1 -1
- package/lib/SecretKey.js +15 -0
- package/lib/SecretKey.js.map +1 -1
- package/lib/SecretKeyHttp.d.ts.map +1 -1
- package/lib/SecretKeyHttp.js.map +1 -1
- package/lib/Service.d.ts +96 -21
- package/lib/Service.d.ts.map +1 -1
- package/lib/Service.js +251 -37
- package/lib/Service.js.map +1 -1
- package/lib/Sign.d.ts.map +1 -1
- package/lib/Sign.js.map +1 -1
- package/lib/SignHttp.d.ts.map +1 -1
- package/lib/SignHttp.js.map +1 -1
- package/lib/Sprite.d.ts +22 -3
- package/lib/Sprite.d.ts.map +1 -1
- package/lib/Sprite.js +15 -0
- package/lib/Sprite.js.map +1 -1
- package/lib/SpriteHttp.d.ts.map +1 -1
- package/lib/SpriteHttp.js.map +1 -1
- package/lib/Verify.d.ts.map +1 -1
- package/lib/Verify.js.map +1 -1
- package/lib/VerifyHttp.d.ts.map +1 -1
- package/lib/VerifyHttp.js.map +1 -1
- package/lib/Volume.d.ts +3 -3
- package/lib/Volume.d.ts.map +1 -1
- package/lib/Volume.js +17 -1
- package/lib/Volume.js.map +1 -1
- package/lib/VolumeSnapshot.d.ts +32 -1
- package/lib/VolumeSnapshot.d.ts.map +1 -1
- package/lib/VolumeSnapshot.js +23 -0
- package/lib/VolumeSnapshot.js.map +1 -1
- package/lib/Website/AssetDeployment.d.ts.map +1 -1
- package/lib/Website/AssetDeployment.js.map +1 -1
- package/lib/Website/Astro.d.ts.map +1 -1
- package/lib/Website/Astro.js.map +1 -1
- package/lib/Website/Foldkit.d.ts.map +1 -1
- package/lib/Website/Foldkit.js.map +1 -1
- package/lib/Website/FrameworkSite.d.ts.map +1 -1
- package/lib/Website/FrameworkSite.js.map +1 -1
- package/lib/Website/Nextjs.d.ts.map +1 -1
- package/lib/Website/Nextjs.js.map +1 -1
- package/lib/Website/Nuxt.d.ts.map +1 -1
- package/lib/Website/Nuxt.js.map +1 -1
- package/lib/Website/Octane.d.ts.map +1 -1
- package/lib/Website/Octane.js.map +1 -1
- package/lib/Website/ReactRouter.d.ts.map +1 -1
- package/lib/Website/ReactRouter.js.map +1 -1
- package/lib/Website/SolidStart.d.ts.map +1 -1
- package/lib/Website/SolidStart.js.map +1 -1
- package/lib/Website/StaticSite.d.ts.map +1 -1
- package/lib/Website/StaticSite.js.map +1 -1
- package/lib/Website/SvelteKit.d.ts.map +1 -1
- package/lib/Website/SvelteKit.js.map +1 -1
- package/lib/Website/TanStackStart.d.ts.map +1 -1
- package/lib/Website/TanStackStart.js.map +1 -1
- package/lib/Website/Vinext.d.ts.map +1 -1
- package/lib/Website/Vinext.js.map +1 -1
- package/lib/Website/Vite.d.ts.map +1 -1
- package/lib/Website/Vite.js.map +1 -1
- package/lib/Website/Vocs.d.ts.map +1 -1
- package/lib/Website/Vocs.js.map +1 -1
- package/lib/Website/Waku.d.ts.map +1 -1
- package/lib/Website/Waku.js.map +1 -1
- package/lib/Website/index.d.ts.map +1 -1
- package/lib/Website/index.js.map +1 -1
- package/lib/WriteCertificates.d.ts.map +1 -1
- package/lib/WriteCertificates.js.map +1 -1
- package/lib/WriteCertificatesHttp.d.ts.map +1 -1
- package/lib/WriteCertificatesHttp.js.map +1 -1
- package/lib/WriteRedis.d.ts.map +1 -1
- package/lib/WriteRedis.js.map +1 -1
- package/lib/WriteRedisHttp.d.ts.map +1 -1
- package/lib/WriteRedisHttp.js.map +1 -1
- package/lib/WriteSecret.d.ts.map +1 -1
- package/lib/WriteSecret.js.map +1 -1
- package/lib/WriteSecretHttp.d.ts.map +1 -1
- package/lib/WriteSecretHttp.js.map +1 -1
- package/lib/bluegreen.d.ts +5 -4
- package/lib/bluegreen.d.ts.map +1 -1
- package/lib/bluegreen.js +1 -1
- package/lib/bluegreen.js.map +1 -1
- package/lib/hosted.d.ts +16 -0
- package/lib/hosted.d.ts.map +1 -1
- package/lib/hosted.js +101 -11
- package/lib/hosted.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/internal/Oidc.d.ts +15 -0
- package/lib/internal/Oidc.d.ts.map +1 -0
- package/lib/internal/Oidc.js +73 -0
- package/lib/internal/Oidc.js.map +1 -0
- package/lib/internal/RemoteBuilder.d.ts.map +1 -1
- package/lib/internal/RemoteBuilder.js.map +1 -1
- package/lib/leases.d.ts +6 -2
- package/lib/leases.d.ts.map +1 -1
- package/lib/leases.js +25 -5
- package/lib/leases.js.map +1 -1
- package/lib/ports.d.ts.map +1 -1
- package/lib/ports.js.map +1 -1
- package/lib/replicas.d.ts +38 -26
- package/lib/replicas.d.ts.map +1 -1
- package/lib/replicas.js +57 -2
- package/lib/replicas.js.map +1 -1
- package/lib/rpc.d.ts +14 -12
- package/lib/rpc.d.ts.map +1 -1
- package/lib/rpc.js +38 -40
- package/lib/rpc.js.map +1 -1
- package/package.json +15 -15
- package/src/App.ts +36 -3
- package/src/BindService.ts +27 -34
- package/src/Bootstrap.ts +2 -0
- package/src/Bucket.ts +264 -33
- package/src/Certificate.ts +32 -1
- package/src/Credentials.ts +9 -5
- package/src/IpAssignment.ts +31 -1
- package/src/Machine.ts +34 -1
- package/src/MountVolume.ts +5 -2
- package/src/Postgres.ts +43 -1
- package/src/Redis.ts +43 -1
- package/src/Secret.ts +29 -1
- package/src/SecretKey.ts +29 -1
- package/src/Service.ts +389 -70
- package/src/Sprite.ts +34 -8
- package/src/Volume.ts +25 -1
- package/src/VolumeSnapshot.ts +39 -1
- package/src/bluegreen.ts +4 -1
- package/src/hosted.ts +118 -13
- package/src/internal/Oidc.ts +99 -0
- package/src/leases.ts +35 -7
- package/src/replicas.ts +63 -2
- package/src/rpc.ts +37 -40
package/src/Sprite.ts
CHANGED
|
@@ -5,7 +5,7 @@ import * as Bundle from "alchemy/Bundle/Bundle";
|
|
|
5
5
|
import { isResolved } from "alchemy/Diff";
|
|
6
6
|
import { Platform, type Main, type PlatformProps } from "alchemy/Platform";
|
|
7
7
|
import * as Provider from "alchemy/Provider";
|
|
8
|
-
import type { Resource, ResourceBinding } from "alchemy/Resource";
|
|
8
|
+
import type { LookupMethod, Resource, ResourceBinding } from "alchemy/Resource";
|
|
9
9
|
import type { ServerHost } from "alchemy/Runtime/Process";
|
|
10
10
|
import { Stack } from "alchemy/Stack";
|
|
11
11
|
import { createInternalTags } from "alchemy/Tags";
|
|
@@ -119,9 +119,19 @@ export type Sprite = Resource<
|
|
|
119
119
|
};
|
|
120
120
|
},
|
|
121
121
|
SpriteBinding,
|
|
122
|
-
Providers
|
|
122
|
+
Providers,
|
|
123
|
+
SpriteLookup
|
|
123
124
|
>;
|
|
124
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Props for {@link Sprite.lookup}: find a Sprite this stack doesn't
|
|
128
|
+
* manage by its name.
|
|
129
|
+
*/
|
|
130
|
+
export interface SpriteLookup {
|
|
131
|
+
/** Sprite name (unique per org). */
|
|
132
|
+
name: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
125
135
|
export const isSprite = (value: unknown): value is Sprite =>
|
|
126
136
|
typeof value === "object" && value !== null && (value as { Type?: string }).Type === "Fly.Sprite";
|
|
127
137
|
|
|
@@ -307,15 +317,24 @@ export type SpriteRuntimeContext = FlyHostRuntimeContext;
|
|
|
307
317
|
* ) {}
|
|
308
318
|
* ```
|
|
309
319
|
*
|
|
320
|
+
* ### Looking up an existing Sprite
|
|
321
|
+
* `Sprite.lookup` references a Sprite Alchemy doesn't manage (created with
|
|
322
|
+
* `sprite create` or by another stack). It is read during plan/deploy,
|
|
323
|
+
* never created, updated or deleted. `code.hash` is empty: the Sprite's
|
|
324
|
+
* program isn't Alchemy's.
|
|
325
|
+
*
|
|
326
|
+
* **Example:** Reference an existing Sprite
|
|
327
|
+
* ```typescript
|
|
328
|
+
* const box = Fly.Sprite.lookup({ name: "my-box" });
|
|
329
|
+
* ```
|
|
330
|
+
*
|
|
310
331
|
* @resource
|
|
311
332
|
* @product Sprite
|
|
312
333
|
*/
|
|
313
|
-
export const Sprite: Platform<Sprite, SpriteServices, SpriteShape, SpriteRuntimeContext>
|
|
314
|
-
"Fly.Sprite",
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
},
|
|
318
|
-
);
|
|
334
|
+
export const Sprite: Platform<Sprite, SpriteServices, SpriteShape, SpriteRuntimeContext> &
|
|
335
|
+
LookupMethod<Sprite> = Platform("Fly.Sprite", {
|
|
336
|
+
createRuntimeContext: createFlyHostRuntimeContext("Fly.Sprite"),
|
|
337
|
+
});
|
|
319
338
|
|
|
320
339
|
export class SpriteNotCreated extends Data.TaggedError("Fly.SpriteNotCreated")<{
|
|
321
340
|
resourceName: string;
|
|
@@ -575,6 +594,13 @@ export const SpriteProvider = () =>
|
|
|
575
594
|
return undefined;
|
|
576
595
|
}),
|
|
577
596
|
|
|
597
|
+
lookup: Effect.fn(function* ({ name }) {
|
|
598
|
+
const found = yield* getByName(name).pipe(
|
|
599
|
+
Effect.catchTag("SpritesNotEnabled", () => Effect.undefined),
|
|
600
|
+
);
|
|
601
|
+
return found === undefined ? undefined : toAttrs(found, name, "");
|
|
602
|
+
}),
|
|
603
|
+
|
|
578
604
|
read: Effect.fn(function* ({ id, olds, output }) {
|
|
579
605
|
const name = yield* resolveSpriteName(id, olds?.name, output?.name);
|
|
580
606
|
const found = yield* (
|
package/src/Volume.ts
CHANGED
|
@@ -116,6 +116,11 @@ export const createVolume = Effect.fn(function* (input: {
|
|
|
116
116
|
disk: DiskSpec;
|
|
117
117
|
}) {
|
|
118
118
|
const sizeGb = Math.max(input.disk.sizeGb, MIN_SIZE_GB);
|
|
119
|
+
// The volumes this group held before the create: a storage host that fails
|
|
120
|
+
// with an unknown outcome may still have made one.
|
|
121
|
+
const before = new Set(
|
|
122
|
+
(yield* listVolumeGroup(input.appName, input.name, input.region)).map((volume) => volume.id),
|
|
123
|
+
);
|
|
119
124
|
const created = yield* machines
|
|
120
125
|
.createVolume({
|
|
121
126
|
app_name: input.appName,
|
|
@@ -130,7 +135,26 @@ export const createVolume = Effect.fn(function* (input: {
|
|
|
130
135
|
source_volume_id: input.disk.sourceVolumeId,
|
|
131
136
|
require_unique_zone: input.disk.requireUniqueZone,
|
|
132
137
|
})
|
|
133
|
-
.pipe(
|
|
138
|
+
.pipe(
|
|
139
|
+
Effect.catchTags({
|
|
140
|
+
Conflict: () => Effect.undefined,
|
|
141
|
+
StorageHostCapacity: (error) =>
|
|
142
|
+
Effect.gen(function* () {
|
|
143
|
+
// Its outcome is unknown: a single new volume in the group is the
|
|
144
|
+
// one this call made. Otherwise create again after a pause.
|
|
145
|
+
yield* Effect.sleep(Duration.seconds(5));
|
|
146
|
+
const fresh = (yield* listVolumeGroup(input.appName, input.name, input.region)).filter(
|
|
147
|
+
(volume) => volume.id !== undefined && !before.has(volume.id),
|
|
148
|
+
);
|
|
149
|
+
return fresh.length === 1 ? fresh[0] : yield* error;
|
|
150
|
+
}),
|
|
151
|
+
}),
|
|
152
|
+
Effect.retry({
|
|
153
|
+
while: (error) => error._tag === "StorageHostCapacity",
|
|
154
|
+
schedule: Schedule.spaced(Duration.seconds(5)),
|
|
155
|
+
times: 6,
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
134
158
|
if (created?.id !== undefined && created.id.length > 0) {
|
|
135
159
|
return (yield* waitUntilVolumeReady(input.appName, created.id)) ?? created;
|
|
136
160
|
}
|
package/src/VolumeSnapshot.ts
CHANGED
|
@@ -56,9 +56,23 @@ export type VolumeSnapshot = Resource<
|
|
|
56
56
|
createdAt: string | undefined;
|
|
57
57
|
},
|
|
58
58
|
never,
|
|
59
|
-
Providers
|
|
59
|
+
Providers,
|
|
60
|
+
VolumeSnapshotLookup
|
|
60
61
|
>;
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Props for {@link VolumeSnapshot.lookup}: find a snapshot this stack
|
|
65
|
+
* doesn't manage by its App, Volume and id.
|
|
66
|
+
*/
|
|
67
|
+
export interface VolumeSnapshotLookup {
|
|
68
|
+
/** Fly App name the Volume belongs to. */
|
|
69
|
+
appName: string;
|
|
70
|
+
/** Fly Volume id (`vol_…`). */
|
|
71
|
+
volumeId: string;
|
|
72
|
+
/** Fly snapshot id (`vs_…`). */
|
|
73
|
+
snapshotId: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
/**
|
|
63
77
|
* A Fly.VolumeSnapshot is an on-demand snapshot of a mounted disk.
|
|
64
78
|
*
|
|
@@ -127,6 +141,25 @@ export type VolumeSnapshot = Resource<
|
|
|
127
141
|
* ) {}
|
|
128
142
|
* ```
|
|
129
143
|
*
|
|
144
|
+
* ### Looking up an existing snapshot
|
|
145
|
+
* `VolumeSnapshot.lookup` references a snapshot Alchemy doesn't manage
|
|
146
|
+
* (taken by Fly's daily schedule or by another stack). It is read during
|
|
147
|
+
* plan/deploy and never created or deleted.
|
|
148
|
+
*
|
|
149
|
+
* **Example:** Restore a disk from an existing snapshot
|
|
150
|
+
* ```typescript
|
|
151
|
+
* const snapshot = Fly.VolumeSnapshot.lookup({
|
|
152
|
+
* appName: "my-app",
|
|
153
|
+
* volumeId: "vol_123",
|
|
154
|
+
* snapshotId: "vs_456",
|
|
155
|
+
* });
|
|
156
|
+
* const disk = yield* Fly.MountVolume({
|
|
157
|
+
* path: "/data",
|
|
158
|
+
* sizeGb: 1,
|
|
159
|
+
* snapshotId: snapshot.snapshotId,
|
|
160
|
+
* });
|
|
161
|
+
* ```
|
|
162
|
+
*
|
|
130
163
|
* @resource
|
|
131
164
|
* @product Volume
|
|
132
165
|
*/
|
|
@@ -235,6 +268,11 @@ export const VolumeSnapshotProvider = () =>
|
|
|
235
268
|
return undefined;
|
|
236
269
|
}),
|
|
237
270
|
|
|
271
|
+
lookup: Effect.fn(function* ({ appName, volumeId, snapshotId }) {
|
|
272
|
+
const found = yield* findById(appName, volumeId, snapshotId);
|
|
273
|
+
return found === undefined ? undefined : toAttrs(appName, volumeId, found);
|
|
274
|
+
}),
|
|
275
|
+
|
|
238
276
|
read: Effect.fn(function* ({ olds, output }) {
|
|
239
277
|
const appName = output?.appName ?? appNameOf(olds?.app);
|
|
240
278
|
const volumeId = output?.volumeId ?? volumeIdOf(olds?.volumeId);
|
package/src/bluegreen.ts
CHANGED
|
@@ -444,7 +444,10 @@ export const reconcileBlueGreen = Effect.fn(function* (
|
|
|
444
444
|
.pipe(
|
|
445
445
|
Retry.none,
|
|
446
446
|
Effect.timeout("30 seconds"),
|
|
447
|
-
Effect.catchTag(
|
|
447
|
+
Effect.catchTag(
|
|
448
|
+
["Conflict", "GatewayTimeout", "TimeoutError", "StorageHostCapacity"],
|
|
449
|
+
() => readback,
|
|
450
|
+
),
|
|
448
451
|
Effect.catchReason("HttpClientError", "TransportError", () => readback),
|
|
449
452
|
);
|
|
450
453
|
}
|
package/src/hosted.ts
CHANGED
|
@@ -9,9 +9,12 @@ import {
|
|
|
9
9
|
type PackageInstall,
|
|
10
10
|
} from "alchemy/Bundle/InstalledPackages";
|
|
11
11
|
import { findCwdForBundle, getStableContextDir, resolveMainPath } from "alchemy/Bundle/TempRoot";
|
|
12
|
+
import { hashDirectory } from "alchemy/Command/Memo";
|
|
12
13
|
import { safeHttpEffect } from "alchemy/Http";
|
|
14
|
+
import * as Output from "alchemy/Output";
|
|
13
15
|
import type { ResourceBinding } from "alchemy/Resource";
|
|
14
16
|
import { createContainerRuntimeContext, type HostRuntimeContext } from "alchemy/Runtime/Process";
|
|
17
|
+
import { packEnvValueKeepRedacted } from "alchemy/RuntimeContext";
|
|
15
18
|
import {
|
|
16
19
|
copyExtraFiles,
|
|
17
20
|
contextRootOf,
|
|
@@ -30,6 +33,7 @@ import * as Path from "effect/Path";
|
|
|
30
33
|
import * as Redacted from "effect/Redacted";
|
|
31
34
|
import * as Schedule from "effect/Schedule";
|
|
32
35
|
import type * as rolldown from "rolldown";
|
|
36
|
+
import { flyHostBundle, flyOidcToken } from "./internal/Oidc.ts";
|
|
33
37
|
import { buildOnRemoteBuilder } from "./internal/RemoteBuilder.ts";
|
|
34
38
|
import type { BoundTarget, DiskSpec, ServiceBinding } from "./MountVolume.ts";
|
|
35
39
|
import { rpcMethodsOf, serveFlyRpc } from "./rpc.ts";
|
|
@@ -59,7 +63,20 @@ export const createFlyHostRuntimeContext =
|
|
|
59
63
|
boots ? { shape: { ...shape, fetch: shape?.fetch ?? wrapped } } : options,
|
|
60
64
|
) as Effect.Effect<void, never, any>;
|
|
61
65
|
};
|
|
62
|
-
|
|
66
|
+
// Keep the `Redacted` wrapper outside the packed string so the Service
|
|
67
|
+
// reconcile can store those values as App secrets instead of Machine env.
|
|
68
|
+
const set: HostRuntimeContext["set"] = (bindingId, output) =>
|
|
69
|
+
Effect.sync(() => {
|
|
70
|
+
const key = bindingId.replaceAll(/[^a-zA-Z0-9]/g, "_");
|
|
71
|
+
base.env[key] = output.pipe(Output.map(packEnvValueKeepRedacted));
|
|
72
|
+
return key;
|
|
73
|
+
});
|
|
74
|
+
// A Service runs on Fly Machines, which mint OIDC tokens for their
|
|
75
|
+
// bindings to exchange with AWS and GCP (see `internal/Oidc.ts`).
|
|
76
|
+
return Object.assign(
|
|
77
|
+
base,
|
|
78
|
+
type === "Fly.Service" ? { serve, set, oidcToken: flyOidcToken } : { serve, set },
|
|
79
|
+
);
|
|
63
80
|
};
|
|
64
81
|
|
|
65
82
|
export const FLY_REGISTRY = "registry.fly.io";
|
|
@@ -189,10 +206,14 @@ export const collectBindingState = (bindings: readonly ResourceBinding<ServiceBi
|
|
|
189
206
|
const active = bindings.filter(
|
|
190
207
|
(binding: ResourceBinding<ServiceBinding> & { action?: string }) => binding.action !== "delete",
|
|
191
208
|
);
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
209
|
+
const merged = active
|
|
210
|
+
.map((binding) => binding?.data?.env)
|
|
211
|
+
.reduce<Record<string, any>>((acc, value) => ({ ...acc, ...value }), {});
|
|
212
|
+
const env = toEnvRecord(merged);
|
|
213
|
+
// Names of bound `Config` values and Redacted outputs. Only the names:
|
|
214
|
+
// the values stay in `env` until the Service moves them into App secrets.
|
|
215
|
+
const secretNames = Object.keys(toEnvRecord(merged)).filter((key) =>
|
|
216
|
+
Redacted.isRedacted(merged[key]),
|
|
196
217
|
);
|
|
197
218
|
const mounts: DiskSpec[] = [];
|
|
198
219
|
const seen = new Set<string>();
|
|
@@ -200,9 +221,12 @@ export const collectBindingState = (bindings: readonly ResourceBinding<ServiceBi
|
|
|
200
221
|
const buckets: { name: string; id?: string; values?: Record<string, string> }[] = [];
|
|
201
222
|
const postgres: { clusterId: string; variableName?: string }[] = [];
|
|
202
223
|
const targets: BoundTarget[] = [];
|
|
224
|
+
const callers = new Set<string>();
|
|
203
225
|
for (const binding of active) {
|
|
204
226
|
const target = binding?.data?.target;
|
|
205
227
|
if (target !== undefined) targets.push(target);
|
|
228
|
+
const caller = coerceBindingName(binding?.data?.caller?.appName);
|
|
229
|
+
if (caller !== undefined) callers.add(caller);
|
|
206
230
|
for (const mount of binding?.data?.mounts ?? []) {
|
|
207
231
|
if (seen.has(mount.path)) continue;
|
|
208
232
|
seen.add(mount.path);
|
|
@@ -256,7 +280,16 @@ export const collectBindingState = (bindings: readonly ResourceBinding<ServiceBi
|
|
|
256
280
|
});
|
|
257
281
|
}
|
|
258
282
|
}
|
|
259
|
-
return {
|
|
283
|
+
return {
|
|
284
|
+
env,
|
|
285
|
+
secretNames,
|
|
286
|
+
mounts,
|
|
287
|
+
redis,
|
|
288
|
+
buckets,
|
|
289
|
+
postgres,
|
|
290
|
+
targets,
|
|
291
|
+
callers: [...callers].sort(),
|
|
292
|
+
};
|
|
260
293
|
};
|
|
261
294
|
|
|
262
295
|
/**
|
|
@@ -377,7 +410,7 @@ export const createFlyHostedSupport = ({
|
|
|
377
410
|
plugins?: rolldown.RolldownPluginOption,
|
|
378
411
|
) {
|
|
379
412
|
return yield* Bundle.build(
|
|
380
|
-
{
|
|
413
|
+
flyHostBundle({
|
|
381
414
|
...props.build?.input,
|
|
382
415
|
input: entry,
|
|
383
416
|
cwd,
|
|
@@ -394,7 +427,7 @@ export const createFlyHostedSupport = ({
|
|
|
394
427
|
...props.build?.input?.resolve,
|
|
395
428
|
},
|
|
396
429
|
plugins: [props.build?.input?.plugins, plugins],
|
|
397
|
-
},
|
|
430
|
+
}),
|
|
398
431
|
{
|
|
399
432
|
...props.build?.output,
|
|
400
433
|
format: "esm",
|
|
@@ -539,10 +572,10 @@ export const createFlyHostedSupport = ({
|
|
|
539
572
|
);
|
|
540
573
|
const entryRel = posixRelUnder(root, realMain, path) ?? path.basename(realMain);
|
|
541
574
|
const dest = path.join(contextDir, entryRel);
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
575
|
+
// The context directory is reused across deploys: always copy the
|
|
576
|
+
// entry, or a changed entry ships the previous deploy's copy.
|
|
577
|
+
yield* fs.makeDirectory(path.dirname(dest), { recursive: true });
|
|
578
|
+
yield* fs.copy(realMain, dest, { overwrite: true });
|
|
546
579
|
}
|
|
547
580
|
return contextDir;
|
|
548
581
|
});
|
|
@@ -583,7 +616,79 @@ export const createFlyHostedSupport = ({
|
|
|
583
616
|
return codeHash;
|
|
584
617
|
});
|
|
585
618
|
|
|
586
|
-
|
|
619
|
+
/** A Docker build context's content hash: its image changes when any file in it does. */
|
|
620
|
+
const contextHash = Effect.fn(function* (context: string) {
|
|
621
|
+
return (yield* hashDirectory({ cwd: context })).slice(0, 16);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Build the image a `context` directory's `Dockerfile` describes, as it
|
|
626
|
+
* is, and push it to `registry.fly.io/{appName}:{logicalId}-{hash}`.
|
|
627
|
+
* When `previousHash` matches, skip build and push.
|
|
628
|
+
*/
|
|
629
|
+
const resolveContextImage = Effect.fn(function* (input: {
|
|
630
|
+
id: string;
|
|
631
|
+
appName: string;
|
|
632
|
+
context: string;
|
|
633
|
+
remote: boolean | undefined;
|
|
634
|
+
previousHash?: string;
|
|
635
|
+
session?: { note: (message: string) => Effect.Effect<void> };
|
|
636
|
+
}) {
|
|
637
|
+
const note = input.session?.note ?? ((_message: string) => Effect.void);
|
|
638
|
+
const codeHash = yield* contextHash(input.context);
|
|
639
|
+
const imageRef = `${FLY_REGISTRY}/${input.appName}:${sanitizeImageRepo(input.id)}-${codeHash}`;
|
|
640
|
+
if (input.previousHash === codeHash) return { imageRef, codeHash };
|
|
641
|
+
|
|
642
|
+
const minted = yield* machines.createAppDeployToken({ app_name: input.appName });
|
|
643
|
+
const token =
|
|
644
|
+
minted.token === undefined || Redacted.isRedacted(minted.token)
|
|
645
|
+
? minted.token
|
|
646
|
+
: Redacted.make(minted.token);
|
|
647
|
+
if (token === undefined || Redacted.value(token).length === 0) {
|
|
648
|
+
return yield* new DeployTokenMissing({ appName: input.appName });
|
|
649
|
+
}
|
|
650
|
+
const registry = { server: FLY_REGISTRY, username: "x", password: token };
|
|
651
|
+
|
|
652
|
+
if (input.remote !== false) {
|
|
653
|
+
yield* note(`Building ${imageRef} on a Fly remote builder...`);
|
|
654
|
+
yield* buildOnRemoteBuilder({
|
|
655
|
+
docker,
|
|
656
|
+
appName: input.appName,
|
|
657
|
+
contextDir: input.context,
|
|
658
|
+
imageRef,
|
|
659
|
+
platform: MACHINE_PLATFORM,
|
|
660
|
+
registry,
|
|
661
|
+
note,
|
|
662
|
+
});
|
|
663
|
+
yield* note(`Pushed ${imageRef}`);
|
|
664
|
+
return { imageRef, codeHash };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (!(yield* imageExists(imageRef))) {
|
|
668
|
+
yield* note(`Building container image ${imageRef}...`);
|
|
669
|
+
yield* docker.image.build({
|
|
670
|
+
context: input.context,
|
|
671
|
+
tag: imageRef,
|
|
672
|
+
platform: MACHINE_PLATFORM,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
yield* note(`Pushing ${imageRef}...`);
|
|
676
|
+
yield* docker.image
|
|
677
|
+
.push(imageRef, registry)
|
|
678
|
+
.pipe(Effect.retry({ times: 3, schedule: pushBackoff }));
|
|
679
|
+
yield* note(`Pushed ${imageRef}`);
|
|
680
|
+
return { imageRef, codeHash };
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
alchemyEnv,
|
|
685
|
+
bundleProgram,
|
|
686
|
+
computeCodeHash,
|
|
687
|
+
resolveImage,
|
|
688
|
+
hash,
|
|
689
|
+
contextHash,
|
|
690
|
+
resolveContextImage,
|
|
691
|
+
};
|
|
587
692
|
};
|
|
588
693
|
|
|
589
694
|
/**
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as http from "node:http";
|
|
2
|
+
import { OidcTokenError } from "alchemy/RuntimeContext";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Redacted from "effect/Redacted";
|
|
5
|
+
import type * as rolldown from "rolldown";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fly Machine OIDC tokens. Every Machine serves part of the Machines API on
|
|
9
|
+
* the `/.fly/api` socket, authenticated as that Machine, so a process can
|
|
10
|
+
* mint a token without holding one. A token is signed by
|
|
11
|
+
* `https://oidc.fly.io/<org>`, lives 10 minutes, and names its caller as
|
|
12
|
+
* `sub = <org>:<app>:<machine>`. AWS and GCP trust it through an OIDC
|
|
13
|
+
* provider, so a Fly host reaches them without a key Alchemy created.
|
|
14
|
+
*
|
|
15
|
+
* See https://fly.io/docs/security/openid-connect/.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** The socket a Machine serves its Machines API on. */
|
|
19
|
+
const FLY_API_SOCKET = "/.fly/api";
|
|
20
|
+
|
|
21
|
+
/** Fly's OIDC issuer for an organization. */
|
|
22
|
+
export const flyOidcIssuer = (orgSlug: string) => `https://oidc.fly.io/${orgSlug}`;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Bundle options for a Fly Service: defines `__ALCHEMY_HOST_IDENTITY__` as
|
|
26
|
+
* `"Fly.OIDC"` so bindings keep only the code that exchanges the Machine's
|
|
27
|
+
* OIDC token, and drop the created-key path.
|
|
28
|
+
*/
|
|
29
|
+
export const flyHostBundle = (input: rolldown.InputOptions): rolldown.InputOptions => ({
|
|
30
|
+
...input,
|
|
31
|
+
transform: {
|
|
32
|
+
...input.transform,
|
|
33
|
+
define: {
|
|
34
|
+
...input.transform?.define,
|
|
35
|
+
"globalThis.__ALCHEMY_HOST_IDENTITY__": JSON.stringify("Fly.OIDC"),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** Mint an OIDC token for `audience` from the Machine's `/.fly/api` socket. */
|
|
41
|
+
export const flyOidcToken = (
|
|
42
|
+
audience: string,
|
|
43
|
+
): Effect.Effect<Redacted.Redacted<string>, OidcTokenError> =>
|
|
44
|
+
Effect.callback<Redacted.Redacted<string>, OidcTokenError>((resume) => {
|
|
45
|
+
const body = JSON.stringify({ aud: audience });
|
|
46
|
+
const request = http.request(
|
|
47
|
+
{
|
|
48
|
+
socketPath: FLY_API_SOCKET,
|
|
49
|
+
path: "/v1/tokens/oidc",
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: {
|
|
52
|
+
"content-type": "application/json",
|
|
53
|
+
"content-length": Buffer.byteLength(body),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
(response) => {
|
|
57
|
+
let text = "";
|
|
58
|
+
response.setEncoding("utf8");
|
|
59
|
+
response.on("data", (chunk: string) => {
|
|
60
|
+
text += chunk;
|
|
61
|
+
});
|
|
62
|
+
response.on("end", () => {
|
|
63
|
+
if (response.statusCode === 200 && text.trim().length > 0) {
|
|
64
|
+
resume(Effect.succeed(Redacted.make(text.trim())));
|
|
65
|
+
} else {
|
|
66
|
+
resume(
|
|
67
|
+
Effect.fail(
|
|
68
|
+
new OidcTokenError({
|
|
69
|
+
message: `${FLY_API_SOCKET} returned HTTP ${response.statusCode} for an OIDC token: ${text.slice(0, 200)}`,
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
response.on("error", (cause) =>
|
|
76
|
+
resume(
|
|
77
|
+
Effect.fail(
|
|
78
|
+
new OidcTokenError({
|
|
79
|
+
message: `Reading the OIDC token from ${FLY_API_SOCKET}`,
|
|
80
|
+
cause,
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
request.on("error", (cause) =>
|
|
88
|
+
resume(
|
|
89
|
+
Effect.fail(
|
|
90
|
+
new OidcTokenError({
|
|
91
|
+
message: `Cannot reach ${FLY_API_SOCKET} for an OIDC token. It is served only on Fly Machines, to root.`,
|
|
92
|
+
cause,
|
|
93
|
+
}),
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
request.end(body);
|
|
98
|
+
return Effect.sync(() => request.destroy());
|
|
99
|
+
});
|
package/src/leases.ts
CHANGED
|
@@ -17,6 +17,7 @@ import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
|
17
17
|
import * as HttpClient from "effect/http/HttpClient";
|
|
18
18
|
import * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
19
19
|
import * as Option from "effect/Option";
|
|
20
|
+
import * as Random from "effect/Random";
|
|
20
21
|
import * as Schedule from "effect/Schedule";
|
|
21
22
|
|
|
22
23
|
const TTL_SECONDS = 120;
|
|
@@ -37,14 +38,20 @@ const retrySchedule = <E>(): Schedule.Schedule<Duration.Duration, E> =>
|
|
|
37
38
|
),
|
|
38
39
|
),
|
|
39
40
|
Schedule.addDelay<Duration.Duration, E>(() =>
|
|
40
|
-
Effect.
|
|
41
|
+
Effect.map(Random.next, (random) => Duration.millis(random * 50)),
|
|
41
42
|
),
|
|
42
43
|
);
|
|
43
44
|
|
|
44
45
|
export class MachineLeaseBusy extends Data.TaggedError("Fly.MachineLeaseBusy")<{
|
|
45
46
|
appName: string;
|
|
46
47
|
machineId: string;
|
|
47
|
-
|
|
48
|
+
/** Fly's conflict message: the lease holder and its expiry. */
|
|
49
|
+
reason?: string;
|
|
50
|
+
}> {
|
|
51
|
+
get message() {
|
|
52
|
+
return `Machine lease for ${this.appName}/${this.machineId} is held by another operation${this.reason ? `: ${this.reason}` : ""}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
48
55
|
|
|
49
56
|
export class MachineLeaseLost extends Data.TaggedError("Fly.MachineLeaseLost")<{
|
|
50
57
|
appName: string;
|
|
@@ -71,6 +78,9 @@ export const makeMachineLeases = Effect.fn(function* (appName: string) {
|
|
|
71
78
|
const scope = yield* Effect.scope;
|
|
72
79
|
const held = new Map<string, HeldLease>();
|
|
73
80
|
const deleting = new Set<string>();
|
|
81
|
+
// Machines this lease set deleted; a later snapshot of the same operation
|
|
82
|
+
// must not wait for them to accept a lease.
|
|
83
|
+
const removed = new Set<string>();
|
|
74
84
|
const renewals = new Set<Fiber.Fiber<void, never>>();
|
|
75
85
|
const uncertain = new Set<string>();
|
|
76
86
|
const lost = yield* Deferred.make<never, MachineLeaseLost>();
|
|
@@ -131,8 +141,10 @@ export const makeMachineLeases = Effect.fn(function* (appName: string) {
|
|
|
131
141
|
while: (error) => error._tag === "Conflict" || error._tag === "TooManyRequests",
|
|
132
142
|
}),
|
|
133
143
|
Effect.timeout("30 seconds"),
|
|
134
|
-
Effect.catchTag("Conflict", () =>
|
|
135
|
-
Effect.fail(
|
|
144
|
+
Effect.catchTag("Conflict", (error) =>
|
|
145
|
+
Effect.fail(
|
|
146
|
+
new MachineLeaseBusy({ appName, machineId, reason: error.message }),
|
|
147
|
+
),
|
|
136
148
|
),
|
|
137
149
|
Effect.catchTag(["HttpClientError", "GatewayTimeout", "TimeoutError"], (error) =>
|
|
138
150
|
lose(machineId, `acquisition uncertain: ${error._tag}`),
|
|
@@ -274,7 +286,10 @@ export const makeMachineLeases = Effect.fn(function* (appName: string) {
|
|
|
274
286
|
Effect.asVoid,
|
|
275
287
|
Effect.catch((error) =>
|
|
276
288
|
Effect.gen(function* () {
|
|
277
|
-
if (error._tag === "NotFound")
|
|
289
|
+
if (error._tag === "NotFound") {
|
|
290
|
+
removed.add(machineId);
|
|
291
|
+
return yield* forget(machineId);
|
|
292
|
+
}
|
|
278
293
|
if (
|
|
279
294
|
error._tag === "Fly.MachineMutationUncertain" ||
|
|
280
295
|
error._tag === "InternalServerError" ||
|
|
@@ -282,12 +297,16 @@ export const makeMachineLeases = Effect.fn(function* (appName: string) {
|
|
|
282
297
|
error._tag === "ServiceUnavailable"
|
|
283
298
|
) {
|
|
284
299
|
// Keep renewal aware of the removal until its exact target has been observed.
|
|
285
|
-
if (yield* confirmRemoved(machineId))
|
|
300
|
+
if (yield* confirmRemoved(machineId)) {
|
|
301
|
+
removed.add(machineId);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
286
304
|
}
|
|
287
305
|
return yield* Effect.fail(error);
|
|
288
306
|
}),
|
|
289
307
|
),
|
|
290
308
|
);
|
|
309
|
+
removed.add(machineId);
|
|
291
310
|
yield* forget(machineId);
|
|
292
311
|
}).pipe(
|
|
293
312
|
Effect.ensuring(
|
|
@@ -360,7 +379,14 @@ export const makeMachineLeases = Effect.fn(function* (appName: string) {
|
|
|
360
379
|
Effect.retry({
|
|
361
380
|
times: 8,
|
|
362
381
|
schedule: retrySchedule<machines.CreateMachineLeaseError | MachineLeaseLost>(),
|
|
363
|
-
|
|
382
|
+
// A renewal re-sends the nonce we hold, so replaying it after a
|
|
383
|
+
// transient 5xx extends the same lease instead of taking a new one.
|
|
384
|
+
while: (error) =>
|
|
385
|
+
error._tag === "TooManyRequests" ||
|
|
386
|
+
error._tag === "InternalServerError" ||
|
|
387
|
+
error._tag === "BadGateway" ||
|
|
388
|
+
error._tag === "ServiceUnavailable" ||
|
|
389
|
+
error._tag === "GatewayTimeout",
|
|
364
390
|
}),
|
|
365
391
|
Effect.timeout("75 seconds"),
|
|
366
392
|
Effect.catch((error) =>
|
|
@@ -407,6 +433,7 @@ export const makeMachineLeases = Effect.fn(function* (appName: string) {
|
|
|
407
433
|
Effect.forkScoped,
|
|
408
434
|
);
|
|
409
435
|
const checkTarget = (machineId: string) => nonce(machineId).pipe(Effect.asVoid);
|
|
436
|
+
const wasRemoved = (machineId: string) => removed.has(machineId);
|
|
410
437
|
return {
|
|
411
438
|
acquire,
|
|
412
439
|
check,
|
|
@@ -416,6 +443,7 @@ export const makeMachineLeases = Effect.fn(function* (appName: string) {
|
|
|
416
443
|
remove,
|
|
417
444
|
forget,
|
|
418
445
|
nonceIfHeld,
|
|
446
|
+
wasRemoved,
|
|
419
447
|
};
|
|
420
448
|
});
|
|
421
449
|
|