@alchemy.run/fly 2.0.0-beta.84 → 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 +27 -5
- package/lib/App.d.ts.map +1 -1
- package/lib/App.js +29 -8
- 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 +21 -28
- 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 +38 -4
- package/lib/Bucket.d.ts.map +1 -1
- package/lib/Bucket.js +190 -37
- package/lib/Bucket.js.map +1 -1
- package/lib/BucketBinding.d.ts.map +1 -1
- package/lib/BucketBinding.js +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 +22 -4
- 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 +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 +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 +5 -4
- package/lib/Credentials.d.ts.map +1 -1
- package/lib/Credentials.js +18 -18
- 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 +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 +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 +21 -4
- 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 +27 -2
- package/lib/Machine.d.ts.map +1 -1
- package/lib/Machine.js +18 -3
- 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 +34 -2
- package/lib/Postgres.d.ts.map +1 -1
- package/lib/Postgres.js +26 -6
- package/lib/Postgres.js.map +1 -1
- package/lib/PostgresMigrations.d.ts +7 -22
- package/lib/PostgresMigrations.d.ts.map +1 -1
- package/lib/PostgresMigrations.js +37 -89
- 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 +23 -3
- package/lib/Redis.d.ts.map +1 -1
- package/lib/Redis.js +37 -6
- package/lib/Redis.js.map +1 -1
- package/lib/RedisBinding.d.ts.map +1 -1
- package/lib/RedisBinding.js +1 -1
- package/lib/RedisBinding.js.map +1 -1
- package/lib/RedisHttp.d.ts +1 -1
- package/lib/RedisHttp.d.ts.map +1 -1
- package/lib/RedisHttp.js +4 -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 +23 -2
- package/lib/Secret.d.ts.map +1 -1
- package/lib/Secret.js +17 -2
- 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 +23 -2
- package/lib/SecretKey.d.ts.map +1 -1
- package/lib/SecretKey.js +17 -2
- 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 +104 -34
- package/lib/Service.d.ts.map +1 -1
- package/lib/Service.js +301 -54
- 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 +24 -5
- package/lib/Sprite.d.ts.map +1 -1
- package/lib/Sprite.js +22 -5
- 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 +4 -4
- package/lib/Volume.d.ts.map +1 -1
- package/lib/Volume.js +21 -5
- 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 +24 -1
- package/lib/VolumeSnapshot.js.map +1 -1
- package/lib/Website/AssetDeployment.d.ts.map +1 -1
- package/lib/Website/AssetDeployment.js +9 -9
- package/lib/Website/AssetDeployment.js.map +1 -1
- package/lib/Website/Astro.d.ts +8 -6
- package/lib/Website/Astro.d.ts.map +1 -1
- package/lib/Website/Astro.js +12 -13
- 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 +39 -2
- package/lib/Website/FrameworkSite.d.ts.map +1 -1
- package/lib/Website/FrameworkSite.js +46 -3
- 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 +10 -3
- package/lib/Website/StaticSite.d.ts.map +1 -1
- package/lib/Website/StaticSite.js +16 -4
- 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 +9 -0
- package/lib/Website/Vite.d.ts.map +1 -1
- package/lib/Website/Vite.js +9 -0
- 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 +2 -2
- 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 +7 -5
- package/lib/bluegreen.js.map +1 -1
- package/lib/hosted.d.ts +19 -4
- 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 +21 -8
- 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 +39 -27
- package/lib/replicas.d.ts.map +1 -1
- package/lib/replicas.js +63 -8
- 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 +41 -10
- package/src/BindService.ts +28 -36
- package/src/Bootstrap.ts +2 -0
- package/src/Bucket.ts +278 -45
- package/src/BucketBinding.ts +1 -1
- package/src/Certificate.ts +36 -5
- package/src/ConnectPostgres.ts +1 -1
- package/src/ConnectPostgresHttp.ts +1 -1
- package/src/Credentials.ts +18 -41
- package/src/DeploymentImages.ts +1 -1
- package/src/Errors.ts +1 -1
- package/src/IpAssignment.ts +35 -5
- package/src/Machine.ts +39 -4
- package/src/MountVolume.ts +5 -2
- package/src/Postgres.ts +50 -8
- package/src/PostgresMigrations.ts +39 -99
- package/src/Redis.ts +54 -9
- package/src/RedisBinding.ts +1 -1
- package/src/RedisHttp.ts +4 -1
- package/src/Secret.ts +32 -4
- package/src/SecretKey.ts +32 -4
- package/src/Service.ts +441 -88
- package/src/Sprite.ts +45 -17
- package/src/Volume.ts +30 -6
- package/src/VolumeSnapshot.ts +40 -2
- package/src/Website/AssetDeployment.ts +13 -15
- package/src/Website/Astro.ts +20 -15
- package/src/Website/FrameworkSite.ts +92 -6
- package/src/Website/StaticSite.ts +27 -7
- package/src/Website/Vite.ts +9 -0
- package/src/WriteCertificatesHttp.ts +2 -2
- package/src/bluegreen.ts +11 -5
- package/src/hosted.ts +121 -17
- package/src/internal/Oidc.ts +99 -0
- package/src/internal/RemoteBuilder.ts +37 -19
- package/src/leases.ts +35 -7
- package/src/replicas.ts +70 -9
- package/src/rpc.ts +37 -40
package/src/bluegreen.ts
CHANGED
|
@@ -102,7 +102,7 @@ export const setRouting = (
|
|
|
102
102
|
yield* getMachineById(appName, machineId).pipe(
|
|
103
103
|
Effect.filterOrFail(
|
|
104
104
|
(machine) => machine !== undefined && machine.cordoned === cordoned,
|
|
105
|
-
() => new ReplicaNotCreated({ appName,
|
|
105
|
+
() => new ReplicaNotCreated({ appName, resourceName: machineId }),
|
|
106
106
|
),
|
|
107
107
|
Effect.retry({
|
|
108
108
|
times: 8,
|
|
@@ -293,7 +293,9 @@ export const reconcileBlueGreen = Effect.fn(function* (
|
|
|
293
293
|
reusable?.[0] ??
|
|
294
294
|
(yield* sha256Object({
|
|
295
295
|
workload,
|
|
296
|
-
predecessors: owned
|
|
296
|
+
predecessors: owned
|
|
297
|
+
.map((machine) => machine.id)
|
|
298
|
+
.sort((a, b) => ((a ?? "") < (b ?? "") ? -1 : (a ?? "") > (b ?? "") ? 1 : 0)),
|
|
297
299
|
})).slice(0, 20);
|
|
298
300
|
const desired = reusable?.[1] ?? [];
|
|
299
301
|
if (
|
|
@@ -417,7 +419,7 @@ export const reconcileBlueGreen = Effect.fn(function* (
|
|
|
417
419
|
),
|
|
418
420
|
Effect.filterOrFail(
|
|
419
421
|
(machine) => machine !== undefined,
|
|
420
|
-
() => new ReplicaNotCreated({ appName: input.appName, name }),
|
|
422
|
+
() => new ReplicaNotCreated({ appName: input.appName, resourceName: name }),
|
|
421
423
|
),
|
|
422
424
|
Effect.retry({
|
|
423
425
|
times: 8,
|
|
@@ -442,11 +444,15 @@ export const reconcileBlueGreen = Effect.fn(function* (
|
|
|
442
444
|
.pipe(
|
|
443
445
|
Retry.none,
|
|
444
446
|
Effect.timeout("30 seconds"),
|
|
445
|
-
Effect.catchTag(
|
|
447
|
+
Effect.catchTag(
|
|
448
|
+
["Conflict", "GatewayTimeout", "TimeoutError", "StorageHostCapacity"],
|
|
449
|
+
() => readback,
|
|
450
|
+
),
|
|
446
451
|
Effect.catchReason("HttpClientError", "TransportError", () => readback),
|
|
447
452
|
);
|
|
448
453
|
}
|
|
449
|
-
if (!current?.id)
|
|
454
|
+
if (!current?.id)
|
|
455
|
+
return yield* new ReplicaNotCreated({ appName: input.appName, resourceName: name });
|
|
450
456
|
const leased = (yield* leaseSnapshot(input.appName, [current], leases))[0];
|
|
451
457
|
if (!leased?.id)
|
|
452
458
|
return yield* ambiguous(
|
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";
|
|
@@ -70,13 +87,12 @@ export const MACHINE_PLATFORM = "linux/amd64";
|
|
|
70
87
|
export interface FlyBuildOptions extends Bundle.BundleConfig {
|
|
71
88
|
/**
|
|
72
89
|
* Native or Node-only packages to install into the Machine image with
|
|
73
|
-
* `npm install` instead of bundling them. `
|
|
74
|
-
*
|
|
75
|
-
* constructor`). Same `build.install` shape as Lambda.
|
|
90
|
+
* `npm install` instead of bundling them. Same `build.install` shape as
|
|
91
|
+
* Lambda.
|
|
76
92
|
*
|
|
77
93
|
* @example
|
|
78
94
|
* ```typescript
|
|
79
|
-
* build: { install: ["
|
|
95
|
+
* build: { install: ["sharp"] }
|
|
80
96
|
* ```
|
|
81
97
|
*/
|
|
82
98
|
readonly install?: PackageInstall;
|
|
@@ -190,10 +206,14 @@ export const collectBindingState = (bindings: readonly ResourceBinding<ServiceBi
|
|
|
190
206
|
const active = bindings.filter(
|
|
191
207
|
(binding: ResourceBinding<ServiceBinding> & { action?: string }) => binding.action !== "delete",
|
|
192
208
|
);
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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]),
|
|
197
217
|
);
|
|
198
218
|
const mounts: DiskSpec[] = [];
|
|
199
219
|
const seen = new Set<string>();
|
|
@@ -201,9 +221,12 @@ export const collectBindingState = (bindings: readonly ResourceBinding<ServiceBi
|
|
|
201
221
|
const buckets: { name: string; id?: string; values?: Record<string, string> }[] = [];
|
|
202
222
|
const postgres: { clusterId: string; variableName?: string }[] = [];
|
|
203
223
|
const targets: BoundTarget[] = [];
|
|
224
|
+
const callers = new Set<string>();
|
|
204
225
|
for (const binding of active) {
|
|
205
226
|
const target = binding?.data?.target;
|
|
206
227
|
if (target !== undefined) targets.push(target);
|
|
228
|
+
const caller = coerceBindingName(binding?.data?.caller?.appName);
|
|
229
|
+
if (caller !== undefined) callers.add(caller);
|
|
207
230
|
for (const mount of binding?.data?.mounts ?? []) {
|
|
208
231
|
if (seen.has(mount.path)) continue;
|
|
209
232
|
seen.add(mount.path);
|
|
@@ -257,7 +280,16 @@ export const collectBindingState = (bindings: readonly ResourceBinding<ServiceBi
|
|
|
257
280
|
});
|
|
258
281
|
}
|
|
259
282
|
}
|
|
260
|
-
return {
|
|
283
|
+
return {
|
|
284
|
+
env,
|
|
285
|
+
secretNames,
|
|
286
|
+
mounts,
|
|
287
|
+
redis,
|
|
288
|
+
buckets,
|
|
289
|
+
postgres,
|
|
290
|
+
targets,
|
|
291
|
+
callers: [...callers].sort(),
|
|
292
|
+
};
|
|
261
293
|
};
|
|
262
294
|
|
|
263
295
|
/**
|
|
@@ -378,7 +410,7 @@ export const createFlyHostedSupport = ({
|
|
|
378
410
|
plugins?: rolldown.RolldownPluginOption,
|
|
379
411
|
) {
|
|
380
412
|
return yield* Bundle.build(
|
|
381
|
-
{
|
|
413
|
+
flyHostBundle({
|
|
382
414
|
...props.build?.input,
|
|
383
415
|
input: entry,
|
|
384
416
|
cwd,
|
|
@@ -395,7 +427,7 @@ export const createFlyHostedSupport = ({
|
|
|
395
427
|
...props.build?.input?.resolve,
|
|
396
428
|
},
|
|
397
429
|
plugins: [props.build?.input?.plugins, plugins],
|
|
398
|
-
},
|
|
430
|
+
}),
|
|
399
431
|
{
|
|
400
432
|
...props.build?.output,
|
|
401
433
|
format: "esm",
|
|
@@ -540,10 +572,10 @@ export const createFlyHostedSupport = ({
|
|
|
540
572
|
);
|
|
541
573
|
const entryRel = posixRelUnder(root, realMain, path) ?? path.basename(realMain);
|
|
542
574
|
const dest = path.join(contextDir, entryRel);
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
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 });
|
|
547
579
|
}
|
|
548
580
|
return contextDir;
|
|
549
581
|
});
|
|
@@ -584,7 +616,79 @@ export const createFlyHostedSupport = ({
|
|
|
584
616
|
return codeHash;
|
|
585
617
|
});
|
|
586
618
|
|
|
587
|
-
|
|
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
|
+
};
|
|
588
692
|
};
|
|
589
693
|
|
|
590
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
|
+
});
|
|
@@ -37,7 +37,9 @@ const HEALTH_INTERVAL = Duration.seconds(5);
|
|
|
37
37
|
const MAX_PENDING_POLLS = 240;
|
|
38
38
|
/** How often, and how long, to wait for the builder's push to reach `registry.fly.io`. */
|
|
39
39
|
const UPSTREAM_POLL = Duration.seconds(2);
|
|
40
|
-
const MAX_UPSTREAM_POLLS =
|
|
40
|
+
const MAX_UPSTREAM_POLLS = 30;
|
|
41
|
+
/** Pushes of one image before giving up on it reaching the registry. */
|
|
42
|
+
const MAX_PUSHES = 3;
|
|
41
43
|
|
|
42
44
|
export class RemoteBuilderError extends Data.TaggedError("Fly.RemoteBuilderError")<{
|
|
43
45
|
operation: string;
|
|
@@ -170,7 +172,13 @@ export const buildOnRemoteBuilder = Effect.fn(function* (input: {
|
|
|
170
172
|
() => input.docker.run(["buildx", "rm", builder]).pipe(Effect.ignore),
|
|
171
173
|
);
|
|
172
174
|
|
|
173
|
-
|
|
175
|
+
// The builder pushes through its host's registry mirror, which forwards
|
|
176
|
+
// to registry.fly.io. Machines pull from there, so confirm the tag landed
|
|
177
|
+
// (usually within seconds) while the builder is still held. The mirror
|
|
178
|
+
// occasionally drops a push without an error, so a tag that hasn't
|
|
179
|
+
// landed is pushed again: the builder has every layer cached, so a
|
|
180
|
+
// repeat build only re-sends the image.
|
|
181
|
+
const build = input.docker.image
|
|
174
182
|
.build(
|
|
175
183
|
{
|
|
176
184
|
context: input.contextDir,
|
|
@@ -193,25 +201,35 @@ export const buildOnRemoteBuilder = Effect.fn(function* (input: {
|
|
|
193
201
|
}),
|
|
194
202
|
}),
|
|
195
203
|
);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
Effect.provideService(DockerEnvironment, {
|
|
203
|
-
DOCKER_AUTH_CONFIG: JSON.stringify({
|
|
204
|
-
auths: { [input.registry.server]: { auth: basicAuth(input.registry) } },
|
|
205
|
-
}),
|
|
206
|
-
}),
|
|
207
|
-
Effect.retry({ schedule: Schedule.spaced(UPSTREAM_POLL), times: MAX_UPSTREAM_POLLS }),
|
|
208
|
-
Effect.mapError(
|
|
209
|
-
() =>
|
|
210
|
-
new RemoteBuilderError({
|
|
211
|
-
operation: "upload",
|
|
212
|
-
message: `${input.imageRef} did not reach ${input.registry.server}`,
|
|
204
|
+
const landed = input.docker
|
|
205
|
+
.run(["buildx", "imagetools", "inspect", "--raw", input.imageRef])
|
|
206
|
+
.pipe(
|
|
207
|
+
Effect.provideService(DockerEnvironment, {
|
|
208
|
+
DOCKER_AUTH_CONFIG: JSON.stringify({
|
|
209
|
+
auths: { [input.registry.server]: { auth: basicAuth(input.registry) } },
|
|
213
210
|
}),
|
|
211
|
+
}),
|
|
212
|
+
Effect.retry({ schedule: Schedule.spaced(UPSTREAM_POLL), times: MAX_UPSTREAM_POLLS }),
|
|
213
|
+
Effect.mapError(
|
|
214
|
+
() =>
|
|
215
|
+
new RemoteBuilderError({
|
|
216
|
+
operation: "upload",
|
|
217
|
+
message: `${input.imageRef} did not reach ${input.registry.server} after ${MAX_PUSHES} pushes`,
|
|
218
|
+
}),
|
|
219
|
+
),
|
|
220
|
+
);
|
|
221
|
+
const started = yield* Clock.currentTimeMillis;
|
|
222
|
+
yield* build.pipe(
|
|
223
|
+
Effect.andThen(landed),
|
|
224
|
+
Effect.tapError((error) =>
|
|
225
|
+
error._tag === "Fly.RemoteBuilderError" && error.operation === "upload"
|
|
226
|
+
? note(`${input.imageRef} hasn't reached ${input.registry.server} yet`)
|
|
227
|
+
: Effect.void,
|
|
214
228
|
),
|
|
229
|
+
Effect.retry({
|
|
230
|
+
while: (error) => error._tag === "Fly.RemoteBuilderError" && error.operation === "upload",
|
|
231
|
+
times: MAX_PUSHES - 1,
|
|
232
|
+
}),
|
|
215
233
|
);
|
|
216
234
|
const waited = (yield* Clock.currentTimeMillis) - started;
|
|
217
235
|
yield* note(`${input.imageRef} available on ${input.registry.server} after ${waited}ms`);
|
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
|
|