alchemy 2.0.0-beta.13 → 2.0.0-beta.15
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/bin/alchemy.js +9477 -9290
- package/bin/alchemy.js.map +1 -1
- package/bin/alchemy.ts +6 -2
- package/bin/cli.js +61 -0
- package/bin/commands/deploy.ts +6 -4
- package/bin/commands/login.ts +1 -1
- package/bin/commands/logs.ts +1 -1
- package/bin/commands/state.ts +233 -0
- package/bin/commands/tail.ts +1 -1
- package/lib/AWS/AutoScaling/LaunchTemplate.d.ts +1 -1
- package/lib/AWS/EC2/Instance.d.ts +1 -1
- package/lib/AWS/EC2/hosted.d.ts +2 -2
- package/lib/AWS/ECS/Task.d.ts +4 -4
- package/lib/AWS/ECS/Task.d.ts.map +1 -1
- package/lib/AWS/ECS/Task.js +2 -2
- package/lib/AWS/ECS/Task.js.map +1 -1
- package/lib/AWS/Environment.d.ts +2 -2
- package/lib/AWS/Lambda/BucketEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/BucketEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/EventBridgeEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/EventBridgeEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/Function.d.ts +2 -2
- package/lib/AWS/Lambda/Function.d.ts.map +1 -1
- package/lib/AWS/Lambda/Function.js +1 -2
- package/lib/AWS/Lambda/Function.js.map +1 -1
- package/lib/AWS/Lambda/QueueEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/QueueEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/StreamEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/StreamEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/TableEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/TableEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/TopicEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/TopicEventSource.d.ts.map +1 -1
- package/lib/AWS/SQS/QueueSink.d.ts.map +1 -1
- package/lib/AWS/SQS/QueueSink.js +1 -2
- package/lib/AWS/SQS/QueueSink.js.map +1 -1
- package/lib/Agent/coding/bash.d.ts +2 -6
- package/lib/Agent/coding/bash.d.ts.map +1 -1
- package/lib/Agent/coding/bash.js +2 -3
- package/lib/Agent/coding/bash.js.map +1 -1
- package/lib/Agent/coding/edit.d.ts +1 -1
- package/lib/Agent/coding/glob.d.ts +2 -4
- package/lib/Agent/coding/glob.d.ts.map +1 -1
- package/lib/Agent/coding/glob.js +1 -2
- package/lib/Agent/coding/glob.js.map +1 -1
- package/lib/Agent/coding/grep.d.ts +2 -4
- package/lib/Agent/coding/grep.d.ts.map +1 -1
- package/lib/Agent/coding/grep.js +1 -2
- package/lib/Agent/coding/grep.js.map +1 -1
- package/lib/Agent/coding/read.d.ts +1 -1
- package/lib/Agent/coding/write.d.ts +1 -1
- package/lib/Agent/lsp/client.d.ts +2 -2
- package/lib/Agent/util/ripgrep.d.ts +2 -2
- package/lib/AlchemyContext.d.ts +13 -0
- package/lib/AlchemyContext.d.ts.map +1 -0
- package/lib/AlchemyContext.js +18 -0
- package/lib/AlchemyContext.js.map +1 -0
- package/lib/Auth/AuthProvider.js +1 -1
- package/lib/Auth/AuthProvider.js.map +1 -1
- package/lib/Auth/Profile.d.ts +1 -1
- package/lib/Auth/index.d.ts +3 -1
- package/lib/Auth/index.d.ts.map +1 -1
- package/lib/Auth/index.js +3 -0
- package/lib/Auth/index.js.map +1 -1
- package/lib/Build/Command.d.ts +1 -1
- package/lib/Build/Memo.d.ts +1 -1
- package/lib/Bundle/Docker.d.ts +3 -3
- package/lib/Bundle/TempRoot.d.ts +3 -3
- package/lib/Cli/LoggingCli.d.ts +4 -0
- package/lib/Cli/LoggingCli.d.ts.map +1 -0
- package/lib/{Test/TestCli.js → Cli/LoggingCli.js} +3 -3
- package/lib/Cli/LoggingCli.js.map +1 -0
- package/lib/Cli/index.d.ts +0 -1
- package/lib/Cli/index.d.ts.map +1 -1
- package/lib/Cli/index.js +4 -1
- package/lib/Cli/index.js.map +1 -1
- package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts +58 -0
- package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts.map +1 -0
- package/lib/Cloudflare/ApiToken/AccountApiToken.js +137 -0
- package/lib/Cloudflare/ApiToken/AccountApiToken.js.map +1 -0
- package/lib/Cloudflare/ApiToken/Common.d.ts +91 -0
- package/lib/Cloudflare/ApiToken/Common.d.ts.map +1 -0
- package/lib/Cloudflare/ApiToken/Common.js +61 -0
- package/lib/Cloudflare/ApiToken/Common.js.map +1 -0
- package/lib/Cloudflare/ApiToken/PermissionGroups.d.ts +1549 -0
- package/lib/Cloudflare/ApiToken/PermissionGroups.d.ts.map +1 -0
- package/lib/Cloudflare/ApiToken/PermissionGroups.js +1856 -0
- package/lib/Cloudflare/ApiToken/PermissionGroups.js.map +1 -0
- package/lib/Cloudflare/ApiToken/UserApiToken.d.ts +49 -0
- package/lib/Cloudflare/ApiToken/UserApiToken.d.ts.map +1 -0
- package/lib/Cloudflare/ApiToken/UserApiToken.js +113 -0
- package/lib/Cloudflare/ApiToken/UserApiToken.js.map +1 -0
- package/lib/Cloudflare/ApiToken/index.d.ts +5 -0
- package/lib/Cloudflare/ApiToken/index.d.ts.map +1 -0
- package/lib/Cloudflare/ApiToken/index.js +5 -0
- package/lib/Cloudflare/ApiToken/index.js.map +1 -0
- package/lib/Cloudflare/Auth/AuthProvider.js +2 -2
- package/lib/Cloudflare/Auth/AuthProvider.js.map +1 -1
- package/lib/Cloudflare/CloudflareEnvironment.d.ts +1 -1
- package/lib/Cloudflare/Container/ContainerApplication.d.ts +2 -2
- package/lib/Cloudflare/Container/ContainerApplication.d.ts.map +1 -1
- package/lib/Cloudflare/Container/ContainerApplication.js +7 -2
- package/lib/Cloudflare/Container/ContainerApplication.js.map +1 -1
- package/lib/Cloudflare/EdgeSession.d.ts +53 -0
- package/lib/Cloudflare/EdgeSession.d.ts.map +1 -0
- package/lib/Cloudflare/EdgeSession.js +78 -0
- package/lib/Cloudflare/EdgeSession.js.map +1 -0
- package/lib/Cloudflare/Providers.d.ts +1 -1
- package/lib/Cloudflare/Providers.d.ts.map +1 -1
- package/lib/Cloudflare/Providers.js +4 -1
- package/lib/Cloudflare/Providers.js.map +1 -1
- package/lib/Cloudflare/StateStore/Api.d.ts +61 -0
- package/lib/Cloudflare/StateStore/Api.d.ts.map +1 -0
- package/lib/Cloudflare/StateStore/Api.js +103 -0
- package/lib/Cloudflare/StateStore/Api.js.map +1 -0
- package/lib/Cloudflare/StateStore/State.d.ts +39 -0
- package/lib/Cloudflare/StateStore/State.d.ts.map +1 -0
- package/lib/Cloudflare/StateStore/State.js +217 -0
- package/lib/Cloudflare/StateStore/State.js.map +1 -0
- package/lib/Cloudflare/StateStore/Store.d.ts +82 -0
- package/lib/Cloudflare/StateStore/Store.d.ts.map +1 -0
- package/lib/Cloudflare/StateStore/Store.js +191 -0
- package/lib/Cloudflare/StateStore/Store.js.map +1 -0
- package/lib/Cloudflare/StateStore/Token.d.ts +43 -0
- package/lib/Cloudflare/StateStore/Token.d.ts.map +1 -0
- package/lib/Cloudflare/StateStore/Token.js +61 -0
- package/lib/Cloudflare/StateStore/Token.js.map +1 -0
- package/lib/Cloudflare/StateStore/index.d.ts +2 -0
- package/lib/Cloudflare/StateStore/index.d.ts.map +1 -0
- package/lib/Cloudflare/StateStore/index.js +2 -0
- package/lib/Cloudflare/StateStore/index.js.map +1 -0
- package/lib/Cloudflare/Workers/Assets.d.ts +2 -2
- package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts +2 -2
- package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/DurableObjectNamespace.js +0 -1
- package/lib/Cloudflare/Workers/DurableObjectNamespace.js.map +1 -1
- package/lib/Cloudflare/Workers/HttpServer.d.ts +1 -1
- package/lib/Cloudflare/Workers/HttpServer.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/HttpServer.js.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +5 -5
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +0 -1
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/index.d.ts +4 -1
- package/lib/Cloudflare/index.d.ts.map +1 -1
- package/lib/Cloudflare/index.js +4 -1
- package/lib/Cloudflare/index.js.map +1 -1
- package/lib/Deploy.d.ts +10 -0
- package/lib/Deploy.d.ts.map +1 -0
- package/lib/Deploy.js +12 -0
- package/lib/Deploy.js.map +1 -0
- package/lib/Destroy.d.ts +8 -3
- package/lib/Destroy.d.ts.map +1 -1
- package/lib/Destroy.js +8 -8
- package/lib/Destroy.js.map +1 -1
- package/lib/GitHub/Providers.d.ts +3 -0
- package/lib/GitHub/Providers.d.ts.map +1 -0
- package/lib/GitHub/Providers.js +5 -0
- package/lib/GitHub/Providers.js.map +1 -0
- package/lib/GitHub/index.d.ts +1 -0
- package/lib/GitHub/index.d.ts.map +1 -1
- package/lib/GitHub/index.js +1 -0
- package/lib/GitHub/index.js.map +1 -1
- package/lib/Http.d.ts +1 -1
- package/lib/Http.d.ts.map +1 -1
- package/lib/Http.js +7 -3
- package/lib/Http.js.map +1 -1
- package/lib/Kubernetes/client.d.ts +10 -10
- package/lib/Output.d.ts.map +1 -1
- package/lib/Output.js +4 -0
- package/lib/Output.js.map +1 -1
- package/lib/Plan.d.ts.map +1 -1
- package/lib/Plan.js +4 -0
- package/lib/Plan.js.map +1 -1
- package/lib/Platform.d.ts +2 -2
- package/lib/Platform.d.ts.map +1 -1
- package/lib/Platform.js.map +1 -1
- package/lib/Provider.d.ts.map +1 -1
- package/lib/Provider.js +0 -1
- package/lib/Provider.js.map +1 -1
- package/lib/Stack.d.ts +24 -12
- package/lib/Stack.d.ts.map +1 -1
- package/lib/Stack.js +47 -10
- package/lib/Stack.js.map +1 -1
- package/lib/State/HttpStateApi.d.ts +96 -0
- package/lib/State/HttpStateApi.d.ts.map +1 -0
- package/lib/State/HttpStateApi.js +96 -0
- package/lib/State/HttpStateApi.js.map +1 -0
- package/lib/State/HttpStateStore.d.ts +10 -0
- package/lib/State/HttpStateStore.d.ts.map +1 -0
- package/lib/State/HttpStateStore.js +102 -0
- package/lib/State/HttpStateStore.js.map +1 -0
- package/lib/State/InMemoryState.d.ts +2 -30
- package/lib/State/InMemoryState.d.ts.map +1 -1
- package/lib/State/InMemoryState.js +23 -18
- package/lib/State/InMemoryState.js.map +1 -1
- package/lib/State/LocalState.d.ts +4 -2
- package/lib/State/LocalState.d.ts.map +1 -1
- package/lib/State/LocalState.js +9 -41
- package/lib/State/LocalState.js.map +1 -1
- package/lib/State/State.d.ts +11 -4
- package/lib/State/State.d.ts.map +1 -1
- package/lib/State/State.js +1 -1
- package/lib/State/State.js.map +1 -1
- package/lib/State/StateEncoding.d.ts +31 -0
- package/lib/State/StateEncoding.d.ts.map +1 -0
- package/lib/State/StateEncoding.js +82 -0
- package/lib/State/StateEncoding.js.map +1 -0
- package/lib/State/Sync.d.ts +23 -0
- package/lib/State/Sync.d.ts.map +1 -0
- package/lib/State/Sync.js +49 -0
- package/lib/State/Sync.js.map +1 -0
- package/lib/State/index.d.ts +4 -0
- package/lib/State/index.d.ts.map +1 -1
- package/lib/State/index.js +4 -0
- package/lib/State/index.js.map +1 -1
- package/lib/Test/Bun.d.ts +13 -17
- package/lib/Test/Bun.d.ts.map +1 -1
- package/lib/Test/Bun.js +17 -36
- package/lib/Test/Bun.js.map +1 -1
- package/lib/Test/TestState.d.ts +1 -2
- package/lib/Test/TestState.d.ts.map +1 -1
- package/lib/Test/TestState.js +5 -9
- package/lib/Test/TestState.js.map +1 -1
- package/lib/Test/Vitest.d.ts +3 -2
- package/lib/Test/Vitest.d.ts.map +1 -1
- package/lib/Test/Vitest.js +20 -11
- package/lib/Test/Vitest.js.map +1 -1
- package/lib/Test/index.d.ts +0 -1
- package/lib/Test/index.d.ts.map +1 -1
- package/lib/Test/index.js +0 -1
- package/lib/Test/index.js.map +1 -1
- package/lib/Util/FileLogger.d.ts +2 -2
- package/lib/Util/FileLogger.d.ts.map +1 -1
- package/lib/Util/FileLogger.js +2 -2
- package/lib/Util/FileLogger.js.map +1 -1
- package/lib/cli/index.d.ts +10 -5
- package/lib/cli/index.d.ts.map +1 -1
- package/lib/cli/index.js +2 -43045
- package/lib/cli/index.js.map +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/package.json +30 -15
- package/src/AWS/ECS/Task.ts +4 -4
- package/src/AWS/Lambda/Function.ts +2 -3
- package/src/AWS/SQS/QueueSink.ts +1 -2
- package/src/Agent/coding/bash.ts +2 -3
- package/src/Agent/coding/glob.ts +1 -2
- package/src/Agent/coding/grep.ts +1 -2
- package/src/AlchemyContext.ts +27 -0
- package/src/Auth/AuthProvider.ts +1 -1
- package/src/Auth/index.ts +3 -1
- package/src/{Test/TestCli.ts → Cli/LoggingCli.ts} +2 -2
- package/src/Cli/index.ts +4 -1
- package/src/Cloudflare/ApiToken/AccountApiToken.ts +197 -0
- package/src/Cloudflare/ApiToken/Common.ts +148 -0
- package/src/Cloudflare/ApiToken/PermissionGroups.ts +1910 -0
- package/src/Cloudflare/ApiToken/UserApiToken.ts +168 -0
- package/src/Cloudflare/ApiToken/index.ts +4 -0
- package/src/Cloudflare/Auth/AuthProvider.ts +2 -2
- package/src/Cloudflare/Container/ContainerApplication.ts +8 -2
- package/src/Cloudflare/EdgeSession.ts +144 -0
- package/src/Cloudflare/Providers.ts +5 -0
- package/src/Cloudflare/StateStore/Api.ts +159 -0
- package/src/Cloudflare/StateStore/State.ts +318 -0
- package/src/Cloudflare/StateStore/Store.ts +290 -0
- package/src/Cloudflare/StateStore/Token.ts +68 -0
- package/src/Cloudflare/StateStore/index.ts +1 -0
- package/src/Cloudflare/Workers/DurableObjectNamespace.ts +1 -6
- package/src/Cloudflare/Workers/HttpServer.ts +1 -1
- package/src/Cloudflare/Workers/Worker.ts +7 -7
- package/src/Cloudflare/index.ts +4 -1
- package/src/Deploy.ts +25 -0
- package/src/Destroy.ts +25 -13
- package/src/GitHub/Providers.ts +6 -0
- package/src/GitHub/index.ts +1 -0
- package/src/Http.ts +31 -16
- package/src/Output.ts +3 -2
- package/src/Plan.ts +3 -0
- package/src/Platform.ts +8 -2
- package/src/Provider.ts +0 -2
- package/src/Stack.ts +146 -45
- package/src/State/HttpStateApi.ts +155 -0
- package/src/State/HttpStateStore.ts +156 -0
- package/src/State/InMemoryState.ts +15 -17
- package/src/State/LocalState.ts +13 -45
- package/src/State/State.ts +13 -4
- package/src/State/StateEncoding.ts +83 -0
- package/src/State/Sync.ts +89 -0
- package/src/State/index.ts +4 -0
- package/src/Test/Bun.ts +31 -89
- package/src/Test/TestState.ts +6 -8
- package/src/Test/Vitest.ts +32 -19
- package/src/Test/index.ts +0 -1
- package/src/Util/FileLogger.ts +2 -2
- package/src/index.ts +2 -1
- package/lib/Config.d.ts +0 -20
- package/lib/Config.d.ts.map +0 -1
- package/lib/Config.js +0 -22
- package/lib/Config.js.map +0 -1
- package/lib/Observability.d.ts +0 -2
- package/lib/Observability.d.ts.map +0 -1
- package/lib/Observability.js +0 -1
- package/lib/Observability.js.map +0 -1
- package/lib/Test/TestCli.d.ts +0 -4
- package/lib/Test/TestCli.d.ts.map +0 -1
- package/lib/Test/TestCli.js.map +0 -1
- package/src/Config.ts +0 -31
- package/src/Observability.ts +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as Redacted from "effect/Redacted";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
import * as HttpApi from "effect/unstable/httpapi/HttpApi";
|
|
7
|
+
import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint";
|
|
8
|
+
import * as HttpApiError from "effect/unstable/httpapi/HttpApiError";
|
|
9
|
+
import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup";
|
|
10
|
+
import * as HttpApiMiddleware from "effect/unstable/httpapi/HttpApiMiddleware";
|
|
11
|
+
import * as HttpApiSchema from "effect/unstable/httpapi/HttpApiSchema";
|
|
12
|
+
import * as HttpApiSecurity from "effect/unstable/httpapi/HttpApiSecurity";
|
|
13
|
+
|
|
14
|
+
export const ResourceStateSchema = Schema.Any;
|
|
15
|
+
|
|
16
|
+
export class BearerTokenValidator extends Context.Service<
|
|
17
|
+
BearerTokenValidator,
|
|
18
|
+
{
|
|
19
|
+
readonly validate: (
|
|
20
|
+
token: string,
|
|
21
|
+
) => Effect.Effect<void, HttpApiError.Unauthorized>;
|
|
22
|
+
}
|
|
23
|
+
>()("alchemy/State/BearerTokenValidator") {}
|
|
24
|
+
|
|
25
|
+
export class StateAuth extends HttpApiMiddleware.Service<
|
|
26
|
+
StateAuth,
|
|
27
|
+
{ requires: BearerTokenValidator }
|
|
28
|
+
>()("alchemy/State/StateAuth", {
|
|
29
|
+
security: {
|
|
30
|
+
bearer: HttpApiSecurity.bearer,
|
|
31
|
+
},
|
|
32
|
+
error: HttpApiError.UnauthorizedNoContent,
|
|
33
|
+
}) {}
|
|
34
|
+
|
|
35
|
+
export const StateAuthLive: Layer.Layer<
|
|
36
|
+
StateAuth,
|
|
37
|
+
never,
|
|
38
|
+
BearerTokenValidator
|
|
39
|
+
> = Layer.effect(
|
|
40
|
+
StateAuth,
|
|
41
|
+
Effect.gen(function* () {
|
|
42
|
+
const validator = yield* BearerTokenValidator;
|
|
43
|
+
return {
|
|
44
|
+
bearer: (httpEffect, { credential }) =>
|
|
45
|
+
validator
|
|
46
|
+
.validate(Redacted.value(credential))
|
|
47
|
+
.pipe(Effect.flatMap(() => httpEffect)),
|
|
48
|
+
};
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/** `stack` path segment for nested REST resources. */
|
|
53
|
+
const StackParams = Schema.Struct({
|
|
54
|
+
stack: Schema.String,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** Optional stage selector for stack deletion. */
|
|
58
|
+
const OptionalStageQuery = Schema.Struct({
|
|
59
|
+
stage: Schema.optional(Schema.String),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** `(stack, stage)` path segments shared by stage-scoped endpoints. */
|
|
63
|
+
const StackStage = Schema.Struct({
|
|
64
|
+
stack: Schema.String,
|
|
65
|
+
stage: Schema.String,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/** `(stack, stage, fqn)` path segments for a single resource. */
|
|
69
|
+
const ResourceKey = Schema.Struct({
|
|
70
|
+
stack: Schema.String,
|
|
71
|
+
stage: Schema.String,
|
|
72
|
+
fqn: Schema.String,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const ListStacks = HttpApiEndpoint.get("listStacks", "/state/stacks", {
|
|
76
|
+
success: Schema.Array(Schema.String),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const ListStages = HttpApiEndpoint.get(
|
|
80
|
+
"listStages",
|
|
81
|
+
"/state/stacks/:stack/stages",
|
|
82
|
+
{
|
|
83
|
+
params: StackParams,
|
|
84
|
+
success: Schema.Array(Schema.String),
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
export const ListResources = HttpApiEndpoint.get(
|
|
89
|
+
"listResources",
|
|
90
|
+
"/state/stacks/:stack/stages/:stage/resources",
|
|
91
|
+
{
|
|
92
|
+
params: StackStage,
|
|
93
|
+
success: Schema.Array(Schema.String),
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
export const GetState = HttpApiEndpoint.get(
|
|
98
|
+
"getState",
|
|
99
|
+
"/state/stacks/:stack/stages/:stage/resources/:fqn",
|
|
100
|
+
{
|
|
101
|
+
params: ResourceKey,
|
|
102
|
+
success: Schema.UndefinedOr(ResourceStateSchema),
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
export const SetState = HttpApiEndpoint.put(
|
|
107
|
+
"setState",
|
|
108
|
+
"/state/stacks/:stack/stages/:stage/resources/:fqn",
|
|
109
|
+
{
|
|
110
|
+
params: ResourceKey,
|
|
111
|
+
payload: ResourceStateSchema,
|
|
112
|
+
success: ResourceStateSchema,
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
export const DeleteState = HttpApiEndpoint.delete(
|
|
117
|
+
"deleteState",
|
|
118
|
+
"/state/stacks/:stack/stages/:stage/resources/:fqn",
|
|
119
|
+
{
|
|
120
|
+
params: ResourceKey,
|
|
121
|
+
success: HttpApiSchema.NoContent,
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
export const DeleteStack = HttpApiEndpoint.delete(
|
|
126
|
+
"deleteStack",
|
|
127
|
+
"/state/stacks/:stack",
|
|
128
|
+
{
|
|
129
|
+
params: StackParams,
|
|
130
|
+
query: OptionalStageQuery,
|
|
131
|
+
success: HttpApiSchema.NoContent,
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
export const GetReplacedResources = HttpApiEndpoint.get(
|
|
136
|
+
"getReplacedResources",
|
|
137
|
+
"/state/stacks/:stack/stages/:stage/replaced-resources",
|
|
138
|
+
{
|
|
139
|
+
params: StackStage,
|
|
140
|
+
success: Schema.Array(ResourceStateSchema),
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
export class StateGroup extends HttpApiGroup.make("state")
|
|
145
|
+
.add(ListStacks)
|
|
146
|
+
.add(ListStages)
|
|
147
|
+
.add(ListResources)
|
|
148
|
+
.add(GetState)
|
|
149
|
+
.add(SetState)
|
|
150
|
+
.add(DeleteState)
|
|
151
|
+
.add(GetReplacedResources)
|
|
152
|
+
.add(DeleteStack)
|
|
153
|
+
.middleware(StateAuth) {}
|
|
154
|
+
|
|
155
|
+
export class StateApi extends HttpApi.make("alchemy-state").add(StateGroup) {}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schedule from "effect/Schedule";
|
|
3
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
4
|
+
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
5
|
+
import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";
|
|
6
|
+
import { StateApi } from "./HttpStateApi.ts";
|
|
7
|
+
|
|
8
|
+
import type { ReplacedResourceState, ResourceState } from "./ResourceState.ts";
|
|
9
|
+
import { StateStoreError, type StateService } from "./State.ts";
|
|
10
|
+
import { encodeState, reviveStateRecursive } from "./StateEncoding.ts";
|
|
11
|
+
|
|
12
|
+
export interface HttpStateStoreProps {
|
|
13
|
+
url: string;
|
|
14
|
+
/** Bearer token used to authenticate every request. */
|
|
15
|
+
authToken: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const makeHttpStateStore = ({ url, authToken }: HttpStateStoreProps) =>
|
|
19
|
+
Effect.gen(function* () {
|
|
20
|
+
const apiClient = yield* HttpApiClient.make(StateApi, {
|
|
21
|
+
baseUrl: url,
|
|
22
|
+
transformClient: HttpClient.mapRequest(
|
|
23
|
+
HttpClientRequest.bearerToken(authToken),
|
|
24
|
+
),
|
|
25
|
+
});
|
|
26
|
+
const state = apiClient.state;
|
|
27
|
+
|
|
28
|
+
const service: StateService = {
|
|
29
|
+
listStacks: () =>
|
|
30
|
+
state.listStacks().pipe(
|
|
31
|
+
Effect.map((stacks) => [...stacks]),
|
|
32
|
+
mapStateStoreError,
|
|
33
|
+
),
|
|
34
|
+
listStages: (stack) =>
|
|
35
|
+
state.listStages({ params: { stack } }).pipe(mapStateStoreError),
|
|
36
|
+
list: (request) =>
|
|
37
|
+
state.listResources({ params: request }).pipe(mapStateStoreError),
|
|
38
|
+
get: (request) =>
|
|
39
|
+
state
|
|
40
|
+
.getState({
|
|
41
|
+
params: {
|
|
42
|
+
stack: request.stack,
|
|
43
|
+
stage: request.stage,
|
|
44
|
+
fqn: encodeURIComponent(request.fqn),
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
.pipe(
|
|
48
|
+
Effect.map((s) =>
|
|
49
|
+
s == null
|
|
50
|
+
? undefined
|
|
51
|
+
: (reviveStateRecursive(s) as ResourceState),
|
|
52
|
+
),
|
|
53
|
+
mapStateStoreError,
|
|
54
|
+
),
|
|
55
|
+
getReplacedResources: (request) =>
|
|
56
|
+
state.getReplacedResources({ params: request }).pipe(
|
|
57
|
+
Effect.map((resources) =>
|
|
58
|
+
resources.map(
|
|
59
|
+
(s) => reviveStateRecursive(s) as ReplacedResourceState,
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
mapStateStoreError,
|
|
63
|
+
),
|
|
64
|
+
set: <V extends ResourceState>(request: {
|
|
65
|
+
stack: string;
|
|
66
|
+
stage: string;
|
|
67
|
+
fqn: string;
|
|
68
|
+
value: V;
|
|
69
|
+
}) =>
|
|
70
|
+
state
|
|
71
|
+
.setState({
|
|
72
|
+
params: {
|
|
73
|
+
stack: request.stack,
|
|
74
|
+
stage: request.stage,
|
|
75
|
+
fqn: encodeURIComponent(request.fqn),
|
|
76
|
+
},
|
|
77
|
+
payload: encodeState(request.value),
|
|
78
|
+
})
|
|
79
|
+
.pipe(
|
|
80
|
+
// Server echoes the stored value, but the client already
|
|
81
|
+
// has the canonical object (including any Redacted<T>
|
|
82
|
+
// instances); returning the input avoids a lossy round-trip.
|
|
83
|
+
Effect.map(() => request.value),
|
|
84
|
+
mapStateStoreError,
|
|
85
|
+
),
|
|
86
|
+
delete: (request) =>
|
|
87
|
+
state
|
|
88
|
+
.deleteState({
|
|
89
|
+
params: {
|
|
90
|
+
stack: request.stack,
|
|
91
|
+
stage: request.stage,
|
|
92
|
+
fqn: encodeURIComponent(request.fqn),
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
.pipe(Effect.asVoid, mapStateStoreError),
|
|
96
|
+
deleteStack: (request) =>
|
|
97
|
+
state
|
|
98
|
+
.deleteStack({
|
|
99
|
+
params: { stack: request.stack },
|
|
100
|
+
query: request.stage === undefined ? {} : { stage: request.stage },
|
|
101
|
+
})
|
|
102
|
+
.pipe(Effect.asVoid, mapStateStoreError),
|
|
103
|
+
};
|
|
104
|
+
return service;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Predicate over an `HttpClientError`-shaped failure that returns `true`
|
|
109
|
+
* for failures we expect to clear up on their own.
|
|
110
|
+
*
|
|
111
|
+
* We retry:
|
|
112
|
+
* - all transport-level errors (no `response` — DNS, TCP reset, TLS,
|
|
113
|
+
* abort, etc.)
|
|
114
|
+
* - 408 (request timeout) and 429 (rate limit)
|
|
115
|
+
* - 404, which is normal in the seconds after a worker is first
|
|
116
|
+
* deployed and the route hasn't propagated yet
|
|
117
|
+
* - every 5xx
|
|
118
|
+
*
|
|
119
|
+
* Anything else (400/401/403/etc.) is a real client-side problem and
|
|
120
|
+
* shouldn't be hidden behind retries.
|
|
121
|
+
*/
|
|
122
|
+
const isTransient = (e: any): boolean => {
|
|
123
|
+
if (e?._tag !== "HttpClientError") return false;
|
|
124
|
+
const status: number | undefined = e.response?.status;
|
|
125
|
+
if (status == null) return true;
|
|
126
|
+
if (status === 404 || status === 408 || status === 429) return true;
|
|
127
|
+
return status >= 500 && status < 600;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const retryTransient = <A, Err, Req>(eff: Effect.Effect<A, Err, Req>) =>
|
|
131
|
+
Effect.retry(eff, {
|
|
132
|
+
while: isTransient,
|
|
133
|
+
// Exponential backoff capped at 2s, no attempt limit. The state
|
|
134
|
+
// store is on the deploy critical path; we'd rather block than
|
|
135
|
+
// surface a transient blip as a hard failure (which historically
|
|
136
|
+
// stranded `syncState` mid-flight, leaving the remote store
|
|
137
|
+
// partially populated).
|
|
138
|
+
schedule: Schedule.exponential(100).pipe(
|
|
139
|
+
Schedule.either(Schedule.spaced("2 seconds")),
|
|
140
|
+
),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/** Collapse any client failure into a {@link StateStoreError}. */
|
|
144
|
+
const mapStateStoreError = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
|
|
145
|
+
eff.pipe(
|
|
146
|
+
Effect.tapError(Effect.log),
|
|
147
|
+
retryTransient,
|
|
148
|
+
Effect.catch((e: E) =>
|
|
149
|
+
Effect.fail(
|
|
150
|
+
new StateStoreError({
|
|
151
|
+
message: e instanceof Error ? e.message : String(e),
|
|
152
|
+
cause: e instanceof Error ? e : undefined,
|
|
153
|
+
}),
|
|
154
|
+
),
|
|
155
|
+
),
|
|
156
|
+
) as Effect.Effect<A, StateStoreError, R>;
|
|
@@ -1,32 +1,23 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
2
|
import * as Layer from "effect/Layer";
|
|
3
3
|
import type { ResourceState } from "./ResourceState.ts";
|
|
4
|
-
import { State
|
|
4
|
+
import { State } from "./State.ts";
|
|
5
5
|
|
|
6
6
|
type StackId = string;
|
|
7
7
|
type StageId = string;
|
|
8
8
|
type Fqn = string;
|
|
9
9
|
|
|
10
|
-
export const
|
|
10
|
+
export const inMemoryState = (
|
|
11
11
|
initialState: Record<
|
|
12
12
|
StackId,
|
|
13
13
|
Record<StageId, Record<Fqn, ResourceState>>
|
|
14
14
|
> = {},
|
|
15
|
-
) =>
|
|
16
|
-
Layer.succeed(State, InMemoryService(initialState)) as Layer.Layer<
|
|
17
|
-
State,
|
|
18
|
-
never,
|
|
19
|
-
never
|
|
20
|
-
>;
|
|
15
|
+
) => Layer.succeed(State, InMemoryService(initialState));
|
|
21
16
|
|
|
22
17
|
export const InMemoryService = (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
> = {},
|
|
27
|
-
) => {
|
|
28
|
-
const state = initialState;
|
|
29
|
-
return {
|
|
18
|
+
state: Record<StackId, Record<StageId, Record<Fqn, ResourceState>>> = {},
|
|
19
|
+
) =>
|
|
20
|
+
State.of({
|
|
30
21
|
listStacks: () => Effect.succeed(Array.from(Object.keys(state))),
|
|
31
22
|
listStages: (stack: string) =>
|
|
32
23
|
Effect.succeed(
|
|
@@ -78,9 +69,16 @@ export const InMemoryService = (
|
|
|
78
69
|
stage: string;
|
|
79
70
|
fqn: string;
|
|
80
71
|
}) => Effect.succeed(delete state[stack]?.[stage]?.[fqn]),
|
|
72
|
+
deleteStack: ({ stack, stage }: { stack: string; stage?: string }) =>
|
|
73
|
+
Effect.sync(() => {
|
|
74
|
+
if (stage === undefined) {
|
|
75
|
+
delete state[stack];
|
|
76
|
+
} else {
|
|
77
|
+
delete state[stack]?.[stage];
|
|
78
|
+
}
|
|
79
|
+
}),
|
|
81
80
|
list: ({ stack, stage }: { stack: string; stage: string }) =>
|
|
82
81
|
Effect.succeed(
|
|
83
82
|
Array.from(Object.keys(state[stack]?.[stage] ?? {}) ?? []),
|
|
84
83
|
),
|
|
85
|
-
}
|
|
86
|
-
};
|
|
84
|
+
});
|
package/src/State/LocalState.ts
CHANGED
|
@@ -3,53 +3,13 @@ import * as FileSystem from "effect/FileSystem";
|
|
|
3
3
|
import * as Layer from "effect/Layer";
|
|
4
4
|
import * as Path from "effect/Path";
|
|
5
5
|
import type { PlatformError } from "effect/PlatformError";
|
|
6
|
-
import * as Redacted from "effect/Redacted";
|
|
7
6
|
import { decodeFqn, encodeFqn } from "../FQN.ts";
|
|
8
|
-
import { isResource } from "../Resource.ts";
|
|
9
7
|
import { State, StateStoreError, type StateService } from "./State.ts";
|
|
8
|
+
import { encodeState, reviveState } from "./StateEncoding.ts";
|
|
10
9
|
|
|
11
|
-
const
|
|
10
|
+
export const localState = () => Layer.effect(State, makeLocalState());
|
|
12
11
|
|
|
13
|
-
const
|
|
14
|
-
if (value === null || value === undefined) return value;
|
|
15
|
-
if (Redacted.isRedacted(value)) {
|
|
16
|
-
return {
|
|
17
|
-
[REDACTED_MARKER]: encodeState(Redacted.value(value)),
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
if (isResource(value)) {
|
|
21
|
-
return {
|
|
22
|
-
id: value.LogicalId,
|
|
23
|
-
type: value.Type,
|
|
24
|
-
props: encodeState(value.Props),
|
|
25
|
-
attr: encodeState(value.Attributes),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
if (Array.isArray(value)) return value.map(encodeState);
|
|
29
|
-
if (typeof value === "object") {
|
|
30
|
-
const result: Record<string, unknown> = {};
|
|
31
|
-
for (const [k, v] of Object.entries(value)) {
|
|
32
|
-
result[k] = encodeState(v);
|
|
33
|
-
}
|
|
34
|
-
return result;
|
|
35
|
-
}
|
|
36
|
-
return value;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const reviveState = (_key: string, value: unknown): unknown => {
|
|
40
|
-
if (
|
|
41
|
-
value !== null &&
|
|
42
|
-
typeof value === "object" &&
|
|
43
|
-
!Array.isArray(value) &&
|
|
44
|
-
REDACTED_MARKER in value
|
|
45
|
-
) {
|
|
46
|
-
return Redacted.make((value as Record<string, unknown>)[REDACTED_MARKER]);
|
|
47
|
-
}
|
|
48
|
-
return value;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export const LocalState = Layer.effect(
|
|
52
|
-
State,
|
|
12
|
+
export const makeLocalState = () =>
|
|
53
13
|
Effect.gen(function* () {
|
|
54
14
|
const fs = yield* FileSystem.FileSystem;
|
|
55
15
|
const path = yield* Path.Path;
|
|
@@ -132,6 +92,15 @@ export const LocalState = Layer.effect(
|
|
|
132
92
|
Effect.map(() => request.value),
|
|
133
93
|
),
|
|
134
94
|
delete: (request) => fs.remove(resource(request)).pipe(recover),
|
|
95
|
+
deleteStack: ({ stack, stage }) =>
|
|
96
|
+
fs
|
|
97
|
+
.remove(
|
|
98
|
+
stage === undefined
|
|
99
|
+
? path.join(stateDir, stack)
|
|
100
|
+
: stageDir({ stack, stage }),
|
|
101
|
+
{ recursive: true },
|
|
102
|
+
)
|
|
103
|
+
.pipe(recover),
|
|
135
104
|
list: (request) =>
|
|
136
105
|
fs.readDirectory(stageDir(request)).pipe(
|
|
137
106
|
recover,
|
|
@@ -143,5 +112,4 @@ export const LocalState = Layer.effect(
|
|
|
143
112
|
),
|
|
144
113
|
};
|
|
145
114
|
return state;
|
|
146
|
-
})
|
|
147
|
-
);
|
|
115
|
+
});
|
package/src/State/State.ts
CHANGED
|
@@ -9,7 +9,7 @@ export class StateStoreError extends Data.TaggedError("StateStoreError")<{
|
|
|
9
9
|
}> {}
|
|
10
10
|
|
|
11
11
|
export class State extends Context.Service<State, StateService>()(
|
|
12
|
-
"
|
|
12
|
+
"alchemy/State",
|
|
13
13
|
) {}
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -21,8 +21,10 @@ export class State extends Context.Service<State, StateService>()(
|
|
|
21
21
|
* for provider operations.
|
|
22
22
|
*/
|
|
23
23
|
export interface StateService {
|
|
24
|
-
listStacks(): Effect.Effect<string[], StateStoreError, never>;
|
|
25
|
-
listStages(
|
|
24
|
+
listStacks(): Effect.Effect<readonly string[], StateStoreError, never>;
|
|
25
|
+
listStages(
|
|
26
|
+
stack: string,
|
|
27
|
+
): Effect.Effect<readonly string[], StateStoreError, never>;
|
|
26
28
|
/**
|
|
27
29
|
* Get a resource by its FQN (namespace-qualified key).
|
|
28
30
|
*/
|
|
@@ -62,11 +64,18 @@ export interface StateService {
|
|
|
62
64
|
stage: string;
|
|
63
65
|
fqn: string;
|
|
64
66
|
}): Effect.Effect<void, StateStoreError, never>;
|
|
67
|
+
/**
|
|
68
|
+
* Delete an entire stack, or a single stage when `stage` is provided.
|
|
69
|
+
*/
|
|
70
|
+
deleteStack(request: {
|
|
71
|
+
stack: string;
|
|
72
|
+
stage?: string;
|
|
73
|
+
}): Effect.Effect<void, StateStoreError, never>;
|
|
65
74
|
/**
|
|
66
75
|
* List all resource FQNs in a stack/stage.
|
|
67
76
|
*/
|
|
68
77
|
list(request: {
|
|
69
78
|
stack: string;
|
|
70
79
|
stage: string;
|
|
71
|
-
}): Effect.Effect<string[], StateStoreError, never>;
|
|
80
|
+
}): Effect.Effect<readonly string[], StateStoreError, never>;
|
|
72
81
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as Redacted from "effect/Redacted";
|
|
2
|
+
import { isResource } from "../Resource.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* JSON marker used to tag a `Redacted<T>` value when writing state.
|
|
6
|
+
* The reviver recognises objects with exactly this key and rebuilds
|
|
7
|
+
* the `Redacted` wrapper on read.
|
|
8
|
+
*/
|
|
9
|
+
export const REDACTED_MARKER = "__redacted__";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Recursively encode a state value for JSON serialisation.
|
|
13
|
+
*
|
|
14
|
+
* - `Redacted<T>` values are wrapped as `{ [REDACTED_MARKER]: <inner> }`
|
|
15
|
+
* so the actual string is persisted rather than the `<redacted>`
|
|
16
|
+
* placeholder produced by the default `toJSON`.
|
|
17
|
+
* - `Resource` instances are flattened to `{ id, type, props, attr }`
|
|
18
|
+
* so persisted state matches the schema used by the loader.
|
|
19
|
+
* - Plain objects and arrays are walked structurally.
|
|
20
|
+
*/
|
|
21
|
+
export const encodeState = (value: unknown): unknown => {
|
|
22
|
+
if (value === null || value === undefined) return value;
|
|
23
|
+
if (Redacted.isRedacted(value)) {
|
|
24
|
+
return {
|
|
25
|
+
[REDACTED_MARKER]: encodeState(Redacted.value(value)),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (isResource(value)) {
|
|
29
|
+
return {
|
|
30
|
+
id: value.LogicalId,
|
|
31
|
+
type: value.Type,
|
|
32
|
+
props: encodeState(value.Props),
|
|
33
|
+
attr: encodeState(value.Attributes),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(value)) return value.map(encodeState);
|
|
37
|
+
if (typeof value === "object") {
|
|
38
|
+
const result: Record<string, unknown> = {};
|
|
39
|
+
for (const [k, v] of Object.entries(value)) {
|
|
40
|
+
result[k] = encodeState(v);
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* JSON reviver that rebuilds `Redacted<T>` values that were written
|
|
49
|
+
* through {@link encodeState}. Intended for use with `JSON.parse`.
|
|
50
|
+
*/
|
|
51
|
+
export const reviveState = (_key: string, value: unknown): unknown => {
|
|
52
|
+
if (
|
|
53
|
+
value !== null &&
|
|
54
|
+
typeof value === "object" &&
|
|
55
|
+
!Array.isArray(value) &&
|
|
56
|
+
REDACTED_MARKER in value
|
|
57
|
+
) {
|
|
58
|
+
return Redacted.make((value as Record<string, unknown>)[REDACTED_MARKER]);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Recursively walk an already-decoded value and rebuild `Redacted<T>`
|
|
65
|
+
* instances from `{ [REDACTED_MARKER]: <inner> }` envelopes. Mirror
|
|
66
|
+
* image of {@link encodeState} for callers that hold a parsed JS
|
|
67
|
+
* value rather than a JSON string (e.g. the HTTP state-store client,
|
|
68
|
+
* which receives values pre-parsed by `HttpApiClient`).
|
|
69
|
+
*/
|
|
70
|
+
export const reviveStateRecursive = (value: unknown): unknown => {
|
|
71
|
+
if (value === null || typeof value !== "object") return value;
|
|
72
|
+
if (Array.isArray(value)) return value.map(reviveStateRecursive);
|
|
73
|
+
const obj = value as Record<string, unknown>;
|
|
74
|
+
const keys = Object.keys(obj);
|
|
75
|
+
if (keys.length === 1 && keys[0] === REDACTED_MARKER) {
|
|
76
|
+
return Redacted.make(reviveStateRecursive(obj[REDACTED_MARKER]));
|
|
77
|
+
}
|
|
78
|
+
const result: Record<string, unknown> = {};
|
|
79
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
80
|
+
result[k] = reviveStateRecursive(v);
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { type StateService } from "./State.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Synchronize all state (every stack/stage/resource) from `source` into
|
|
6
|
+
* `destination` so that `destination` becomes a mirror of `source`.
|
|
7
|
+
*
|
|
8
|
+
* For each `{ stack, stage, fqn }` present in `source`, the resource is
|
|
9
|
+
* written into `destination`, overwriting any existing entry under the same
|
|
10
|
+
* key. Any keys present in `destination` but absent from `source` are
|
|
11
|
+
* deleted, ensuring the two stores end up structurally identical.
|
|
12
|
+
*
|
|
13
|
+
* Stacks are walked sequentially; stages within a stack and resources
|
|
14
|
+
* within a stage are processed concurrently for throughput.
|
|
15
|
+
*/
|
|
16
|
+
export const syncState = Effect.fnUntraced(function* (
|
|
17
|
+
source: StateService,
|
|
18
|
+
destination: StateService,
|
|
19
|
+
options?: {
|
|
20
|
+
stacks?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Maximum number of resources to copy in parallel within a single stage.
|
|
23
|
+
* @default "unbounded".
|
|
24
|
+
*/
|
|
25
|
+
concurrency?: number | "unbounded";
|
|
26
|
+
},
|
|
27
|
+
) {
|
|
28
|
+
const concurrency = options?.concurrency ?? "unbounded";
|
|
29
|
+
const [sourceStacks, destStacks] = yield* Effect.all([
|
|
30
|
+
source.listStacks(),
|
|
31
|
+
destination.listStacks(),
|
|
32
|
+
]);
|
|
33
|
+
const sourceStackSet = new Set(
|
|
34
|
+
sourceStacks.filter((stack) => options?.stacks?.includes(stack) ?? true),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
yield* Effect.forEach(
|
|
38
|
+
sourceStacks,
|
|
39
|
+
Effect.fnUntraced(function* (stack) {
|
|
40
|
+
const [sourceStages, destStages] = yield* Effect.all([
|
|
41
|
+
source.listStages(stack),
|
|
42
|
+
destination.listStages(stack),
|
|
43
|
+
]);
|
|
44
|
+
const stages = union(sourceStages, destStages);
|
|
45
|
+
|
|
46
|
+
yield* Effect.forEach(
|
|
47
|
+
stages,
|
|
48
|
+
Effect.fnUntraced(function* (stage) {
|
|
49
|
+
const sourceFqns = yield* source.list({ stack, stage });
|
|
50
|
+
const destFqns = yield* destination.list({ stack, stage });
|
|
51
|
+
|
|
52
|
+
const sourceSet = new Set(sourceFqns);
|
|
53
|
+
const toDelete = destFqns.filter((fqn) => !sourceSet.has(fqn));
|
|
54
|
+
|
|
55
|
+
yield* Effect.all(
|
|
56
|
+
[
|
|
57
|
+
Effect.forEach(
|
|
58
|
+
sourceFqns,
|
|
59
|
+
Effect.fnUntraced(function* (fqn) {
|
|
60
|
+
const value = yield* source.get({ stack, stage, fqn });
|
|
61
|
+
if (value) {
|
|
62
|
+
yield* destination.set({ stack, stage, fqn, value });
|
|
63
|
+
}
|
|
64
|
+
}),
|
|
65
|
+
{ concurrency },
|
|
66
|
+
),
|
|
67
|
+
Effect.forEach(
|
|
68
|
+
toDelete,
|
|
69
|
+
(fqn) => destination.delete({ stack, stage, fqn }),
|
|
70
|
+
{ concurrency },
|
|
71
|
+
),
|
|
72
|
+
],
|
|
73
|
+
{ concurrency: "unbounded" },
|
|
74
|
+
);
|
|
75
|
+
}),
|
|
76
|
+
{ concurrency: "unbounded" },
|
|
77
|
+
);
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
yield* Effect.forEach(
|
|
82
|
+
destStacks.filter((stack) => !sourceStackSet.has(stack)),
|
|
83
|
+
(stack) => destination.deleteStack({ stack }),
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const union = <T>(left: Iterable<T>, right: Iterable<T>) => [
|
|
88
|
+
...new Set([...left, ...right]),
|
|
89
|
+
];
|
package/src/State/index.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
export * from "./HttpStateApi.ts";
|
|
2
|
+
export * from "./HttpStateStore.ts";
|
|
1
3
|
export * from "./InMemoryState.ts";
|
|
2
4
|
export * from "./LocalState.ts";
|
|
3
5
|
export * from "./ResourceState.ts";
|
|
4
6
|
export * from "./State.ts";
|
|
7
|
+
export * from "./StateEncoding.ts";
|
|
8
|
+
export * from "./Sync.ts";
|