alchemy 2.0.0-beta.36 → 2.0.0-beta.37
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/exec.js +166 -43
- package/bin/exec.js.map +1 -1
- package/lib/AWS/Lambda/Function.d.ts.map +1 -1
- package/lib/AWS/Lambda/Function.js +22 -2
- package/lib/AWS/Lambda/Function.js.map +1 -1
- package/lib/Apply.d.ts.map +1 -1
- package/lib/Apply.js +6 -1
- package/lib/Apply.js.map +1 -1
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.d.ts +54 -0
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.d.ts.map +1 -0
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.js +17 -0
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.js.map +1 -0
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.d.ts +37 -0
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.d.ts.map +1 -0
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.js +45 -0
- package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.js.map +1 -0
- package/lib/Cloudflare/AnalyticsEngine/index.d.ts +3 -0
- package/lib/Cloudflare/AnalyticsEngine/index.d.ts.map +1 -0
- package/lib/Cloudflare/AnalyticsEngine/index.js +3 -0
- package/lib/Cloudflare/AnalyticsEngine/index.js.map +1 -0
- package/lib/Cloudflare/D1/D1Connection.d.ts +47 -3
- package/lib/Cloudflare/D1/D1Connection.d.ts.map +1 -1
- package/lib/Cloudflare/D1/D1Connection.js +62 -5
- package/lib/Cloudflare/D1/D1Connection.js.map +1 -1
- package/lib/Cloudflare/Local/SidecarHandlers.js +9 -1
- package/lib/Cloudflare/Local/SidecarHandlers.js.map +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/R2/R2Bucket.d.ts.map +1 -1
- package/lib/Cloudflare/R2/R2Bucket.js +16 -0
- package/lib/Cloudflare/R2/R2Bucket.js.map +1 -1
- package/lib/Cloudflare/StateStore/Api.d.ts +6 -1
- package/lib/Cloudflare/StateStore/Api.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/Api.js +23 -1
- package/lib/Cloudflare/StateStore/Api.js.map +1 -1
- package/lib/Cloudflare/StateStore/Store.d.ts +16 -1
- package/lib/Cloudflare/StateStore/Store.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/Store.js +22 -5
- package/lib/Cloudflare/StateStore/Store.js.map +1 -1
- package/lib/Cloudflare/Workers/CronEventSource.d.ts +35 -0
- package/lib/Cloudflare/Workers/CronEventSource.d.ts.map +1 -0
- package/lib/Cloudflare/Workers/CronEventSource.js +55 -0
- package/lib/Cloudflare/Workers/CronEventSource.js.map +1 -0
- package/lib/Cloudflare/Workers/InferEnv.d.ts +37 -2
- package/lib/Cloudflare/Workers/InferEnv.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js +2 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
- package/lib/Cloudflare/Workers/Rpc.d.ts +76 -3
- package/lib/Cloudflare/Workers/Rpc.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Rpc.js +197 -13
- package/lib/Cloudflare/Workers/Rpc.js.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +22 -2
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +174 -43
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBundle.js +15 -5
- package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -1
- package/lib/Cloudflare/Workers/Workflow.d.ts +50 -47
- package/lib/Cloudflare/Workers/Workflow.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Workflow.js +41 -35
- package/lib/Cloudflare/Workers/Workflow.js.map +1 -1
- package/lib/Cloudflare/Workers/index.d.ts +1 -0
- package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/index.js +1 -0
- package/lib/Cloudflare/Workers/index.js.map +1 -1
- package/lib/Cloudflare/index.d.ts +1 -0
- package/lib/Cloudflare/index.d.ts.map +1 -1
- package/lib/Cloudflare/index.js +1 -0
- package/lib/Cloudflare/index.js.map +1 -1
- package/lib/Deploy.d.ts +4 -1
- package/lib/Deploy.d.ts.map +1 -1
- package/lib/Deploy.js +2 -2
- package/lib/Deploy.js.map +1 -1
- package/lib/Destroy.d.ts +4 -1
- package/lib/Destroy.d.ts.map +1 -1
- package/lib/Destroy.js +2 -2
- package/lib/Destroy.js.map +1 -1
- package/lib/Output.d.ts +45 -5
- package/lib/Output.d.ts.map +1 -1
- package/lib/Output.js +100 -4
- package/lib/Output.js.map +1 -1
- package/lib/Plan.d.ts.map +1 -1
- package/lib/Plan.js +93 -12
- package/lib/Plan.js.map +1 -1
- package/lib/Platform.d.ts.map +1 -1
- package/lib/Platform.js +16 -3
- package/lib/Platform.js.map +1 -1
- package/lib/Ref.d.ts +4 -1
- package/lib/Ref.d.ts.map +1 -1
- package/lib/Ref.js +1 -1
- package/lib/Ref.js.map +1 -1
- package/lib/Resource.d.ts +8 -0
- package/lib/Resource.d.ts.map +1 -1
- package/lib/Resource.js +10 -0
- package/lib/Resource.js.map +1 -1
- package/lib/RuntimeContext.d.ts +3 -1
- package/lib/RuntimeContext.d.ts.map +1 -1
- package/lib/RuntimeContext.js.map +1 -1
- package/lib/Secret.d.ts +50 -0
- package/lib/Secret.d.ts.map +1 -0
- package/lib/Secret.js +23 -0
- package/lib/Secret.js.map +1 -0
- package/lib/Serverless/Function.d.ts +9 -1
- package/lib/Serverless/Function.d.ts.map +1 -1
- package/lib/Stack.d.ts +14 -5
- package/lib/Stack.d.ts.map +1 -1
- package/lib/Stack.js +31 -19
- package/lib/Stack.js.map +1 -1
- package/lib/State/HttpStateApi.d.ts +15 -1
- package/lib/State/HttpStateApi.d.ts.map +1 -1
- package/lib/State/HttpStateApi.js +11 -0
- package/lib/State/HttpStateApi.js.map +1 -1
- package/lib/State/HttpStateStore.d.ts.map +1 -1
- package/lib/State/HttpStateStore.js +11 -0
- package/lib/State/HttpStateStore.js.map +1 -1
- package/lib/State/InMemoryState.d.ts +2 -2
- package/lib/State/InMemoryState.d.ts.map +1 -1
- package/lib/State/InMemoryState.js +12 -6
- package/lib/State/InMemoryState.js.map +1 -1
- package/lib/State/LocalState.d.ts.map +1 -1
- package/lib/State/LocalState.js +11 -2
- package/lib/State/LocalState.js.map +1 -1
- package/lib/State/State.d.ts +21 -0
- package/lib/State/State.d.ts.map +1 -1
- package/lib/Test/Bun.d.ts.map +1 -1
- package/lib/Test/Bun.js +30 -7
- package/lib/Test/Bun.js.map +1 -1
- package/lib/Test/Core.d.ts +17 -2
- package/lib/Test/Core.d.ts.map +1 -1
- package/lib/Test/Core.js +29 -11
- package/lib/Test/Core.js.map +1 -1
- package/lib/Test/Vitest.d.ts.map +1 -1
- package/lib/Test/Vitest.js +24 -5
- package/lib/Test/Vitest.js.map +1 -1
- package/lib/Util/LocalhostDns.d.ts +14 -10
- package/lib/Util/LocalhostDns.d.ts.map +1 -1
- package/lib/Util/LocalhostDns.js +27 -36
- package/lib/Util/LocalhostDns.js.map +1 -1
- package/lib/Util/effect.d.ts +7 -0
- package/lib/Util/effect.d.ts.map +1 -1
- package/lib/Util/effect.js +6 -0
- package/lib/Util/effect.js.map +1 -1
- package/lib/Variable.d.ts +29 -0
- package/lib/Variable.d.ts.map +1 -0
- package/lib/Variable.js +9 -0
- package/lib/Variable.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -1
- package/lib/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +6 -7
- package/src/AWS/Lambda/Function.ts +30 -2
- package/src/Apply.ts +8 -1
- package/src/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.ts +82 -0
- package/src/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.ts +106 -0
- package/src/Cloudflare/AnalyticsEngine/index.ts +2 -0
- package/src/Cloudflare/D1/D1Connection.ts +95 -12
- package/src/Cloudflare/Local/SidecarHandlers.ts +9 -1
- package/src/Cloudflare/Providers.ts +5 -0
- package/src/Cloudflare/R2/R2Bucket.ts +31 -0
- package/src/Cloudflare/StateStore/Api.ts +34 -1
- package/src/Cloudflare/StateStore/Store.ts +40 -10
- package/src/Cloudflare/Workers/CronEventSource.ts +89 -0
- package/src/Cloudflare/Workers/InferEnv.ts +94 -20
- package/src/Cloudflare/Workers/LocalWorkerProvider.ts +9 -1
- package/src/Cloudflare/Workers/Rpc.ts +227 -18
- package/src/Cloudflare/Workers/Worker.ts +277 -63
- package/src/Cloudflare/Workers/WorkerBundle.ts +15 -5
- package/src/Cloudflare/Workers/Workflow.ts +74 -70
- package/src/Cloudflare/Workers/index.ts +1 -0
- package/src/Cloudflare/index.ts +1 -0
- package/src/Deploy.ts +5 -1
- package/src/Destroy.ts +5 -1
- package/src/Output.ts +121 -12
- package/src/Plan.ts +101 -12
- package/src/Platform.ts +19 -4
- package/src/Ref.ts +10 -5
- package/src/Resource.ts +22 -0
- package/src/RuntimeContext.ts +1 -0
- package/src/Secret.ts +87 -0
- package/src/Serverless/Function.ts +10 -1
- package/src/Stack.ts +52 -29
- package/src/State/HttpStateApi.ts +21 -0
- package/src/State/HttpStateStore.ts +21 -0
- package/src/State/InMemoryState.ts +29 -8
- package/src/State/LocalState.ts +28 -4
- package/src/State/State.ts +21 -0
- package/src/Test/Bun.ts +43 -7
- package/src/Test/Core.ts +29 -8
- package/src/Test/Vitest.ts +33 -4
- package/src/Util/LocalhostDns.ts +30 -36
- package/src/Util/effect.ts +25 -0
- package/src/Variable.ts +44 -0
- package/src/index.ts +3 -0
package/src/Platform.ts
CHANGED
|
@@ -242,8 +242,18 @@ export const Platform = <
|
|
|
242
242
|
}),
|
|
243
243
|
)
|
|
244
244
|
).pipe(
|
|
245
|
-
Effect.tap(
|
|
246
|
-
|
|
245
|
+
Effect.tap((resource) =>
|
|
246
|
+
hooks.onCreate
|
|
247
|
+
? Effect.flatMap(
|
|
248
|
+
// `props` may itself be an Effect (e.g. when wrapped by
|
|
249
|
+
// `Cloudflare.Vite` via `Effect.map`); resolve it before
|
|
250
|
+
// handing it to the hook so `onCreate` always sees the
|
|
251
|
+
// plain props object — the second call site (in
|
|
252
|
+
// `cls.make`) already does this.
|
|
253
|
+
Effect.isEffect(props) ? props : Effect.succeed(props ?? {}),
|
|
254
|
+
(resolved) => hooks.onCreate!(resource as R, resolved),
|
|
255
|
+
)
|
|
256
|
+
: Effect.void,
|
|
247
257
|
),
|
|
248
258
|
);
|
|
249
259
|
return Object.assign(
|
|
@@ -324,8 +334,13 @@ export const Platform = <
|
|
|
324
334
|
yield* impl.pipe(
|
|
325
335
|
Effect.flatMap((impl) =>
|
|
326
336
|
impl?.fetch
|
|
327
|
-
?
|
|
328
|
-
|
|
337
|
+
? // Hand the full impl to `serve` so the runtime can
|
|
338
|
+
// expose any non-handler methods on the impl shape
|
|
339
|
+
// (e.g. RPC methods on a Cloudflare Worker) alongside
|
|
340
|
+
// the standard `fetch` handler.
|
|
341
|
+
(runtimeContext.serve?.(impl.fetch, {
|
|
342
|
+
shape: impl as Record<string, unknown>,
|
|
343
|
+
}) ?? Effect.die("No serve handler"))
|
|
329
344
|
: Effect.void,
|
|
330
345
|
),
|
|
331
346
|
Effect.provide(
|
package/src/Ref.ts
CHANGED
|
@@ -22,11 +22,16 @@ export interface RefMetadata<R extends ResourceLike> {
|
|
|
22
22
|
stage?: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export const ref = <R extends ResourceLike>(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
export const ref = <R extends ResourceLike>(
|
|
26
|
+
id: string,
|
|
27
|
+
{
|
|
28
|
+
stack,
|
|
29
|
+
stage,
|
|
30
|
+
}: {
|
|
31
|
+
stack?: string;
|
|
32
|
+
stage?: string;
|
|
33
|
+
} = {},
|
|
34
|
+
): Ref<R> => {
|
|
30
35
|
const ref = new Proxy(
|
|
31
36
|
{},
|
|
32
37
|
{
|
package/src/Resource.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { Input, InputProps } from "./Input.ts";
|
|
|
7
7
|
import { CurrentNamespace, type NamespaceNode } from "./Namespace.ts";
|
|
8
8
|
import * as Output from "./Output.ts";
|
|
9
9
|
import { Provider } from "./Provider.ts";
|
|
10
|
+
import { ref as makeRef } from "./Ref.ts";
|
|
10
11
|
import { RemovalPolicy } from "./RemovalPolicy.ts";
|
|
11
12
|
import { Self } from "./Self.ts";
|
|
12
13
|
import { Stack } from "./Stack.ts";
|
|
@@ -47,6 +48,10 @@ export type ResourceClassWithMethods<
|
|
|
47
48
|
Effect.Effect<ResourceConstructor<R>> & {
|
|
48
49
|
Self: Self<R>;
|
|
49
50
|
Provider: Provider<R>;
|
|
51
|
+
ref(
|
|
52
|
+
id: string,
|
|
53
|
+
options?: { stage?: string; stack?: string },
|
|
54
|
+
): Effect.Effect<R>;
|
|
50
55
|
} & Methods;
|
|
51
56
|
|
|
52
57
|
export type ResourceClass<R extends ResourceLike> = ResourceConstructor<
|
|
@@ -56,6 +61,10 @@ export type ResourceClass<R extends ResourceLike> = ResourceConstructor<
|
|
|
56
61
|
Effect.Effect<ResourceConstructor<R>> & {
|
|
57
62
|
Self: Self<R>;
|
|
58
63
|
Provider: Provider<R>;
|
|
64
|
+
ref(
|
|
65
|
+
id: string,
|
|
66
|
+
options?: { stage?: string; stack?: string },
|
|
67
|
+
): Effect.Effect<R>;
|
|
59
68
|
};
|
|
60
69
|
|
|
61
70
|
export type LogicalId = string;
|
|
@@ -286,6 +295,19 @@ export function Resource<R extends ResourceLike>(
|
|
|
286
295
|
constructor(id, props),
|
|
287
296
|
);
|
|
288
297
|
},
|
|
298
|
+
/**
|
|
299
|
+
* Build a typed reference to a deployed instance of this resource
|
|
300
|
+
* — in the current stack/stage by default, or in another via
|
|
301
|
+
* `options`. Resolves to the same shape as `yield*
|
|
302
|
+
* MyResource("id", props)` so downstream code can read attributes
|
|
303
|
+
* (`ref.someAttr`) exactly the way it would for a locally-declared
|
|
304
|
+
* resource.
|
|
305
|
+
*/
|
|
306
|
+
ref: (
|
|
307
|
+
id: string,
|
|
308
|
+
options?: { stage?: string; stack?: string },
|
|
309
|
+
): Effect.Effect<R> =>
|
|
310
|
+
Effect.succeed(Output.of(makeRef<R>(id, options)) as unknown as R),
|
|
289
311
|
Type: type,
|
|
290
312
|
Provider: ProviderTag,
|
|
291
313
|
Self: self,
|
package/src/RuntimeContext.ts
CHANGED
package/src/Secret.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as Config from "effect/Config";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Redacted from "effect/Redacted";
|
|
4
|
+
import * as Output from "./Output.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The shapes a {@link secret} can be derived from.
|
|
8
|
+
*
|
|
9
|
+
* - `string` — wrapped in `Redacted.make` and bound as a literal.
|
|
10
|
+
* - `Redacted<string>` — bound as-is.
|
|
11
|
+
* - `Effect<string | Redacted<string>>` — resolved at deploy time.
|
|
12
|
+
* - `Config<string | Redacted<string>>` — resolved against the active
|
|
13
|
+
* `ConfigProvider` at deploy time.
|
|
14
|
+
*/
|
|
15
|
+
export type SecretInput<R = never> =
|
|
16
|
+
| string
|
|
17
|
+
| Redacted.Redacted<string>
|
|
18
|
+
| Effect.Effect<string | Redacted.Redacted<string>, any, R>
|
|
19
|
+
| Config.Config<string | Redacted.Redacted<string>>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Bind a secret value into the active {@link RuntimeContext} (e.g. a
|
|
23
|
+
* Cloudflare Worker's `env`) under the given `name` so the resource
|
|
24
|
+
* provider deploys it as a secret (e.g. `secret_text` for Cloudflare).
|
|
25
|
+
*
|
|
26
|
+
* The result is an `Output<Redacted<string>>` — `yield*`-ing it returns
|
|
27
|
+
* an `Accessor<Redacted<string>>` that resolves the value at runtime.
|
|
28
|
+
*
|
|
29
|
+
* @example One-arg shortcut — reads `Config.redacted("API_KEY")`
|
|
30
|
+
* ```ts
|
|
31
|
+
* const apiKey = yield* Alchemy.Secret("API_KEY");
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example From a literal
|
|
35
|
+
* ```ts
|
|
36
|
+
* const apiKey = yield* Alchemy.Secret("API_KEY", "sk-123");
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @example From an Effect
|
|
40
|
+
* ```ts
|
|
41
|
+
* const apiKey = yield* Alchemy.Secret(
|
|
42
|
+
* "API_KEY",
|
|
43
|
+
* Effect.succeed(Redacted.make("sk-123")),
|
|
44
|
+
* );
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @example From a Config
|
|
48
|
+
* ```ts
|
|
49
|
+
* const apiKey = yield* Alchemy.Secret("API_KEY", Config.redacted("API_KEY"));
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function Secret(
|
|
53
|
+
name: string,
|
|
54
|
+
): Output.Output<Redacted.Redacted<string>, never>;
|
|
55
|
+
export function Secret<R = never>(
|
|
56
|
+
name: string,
|
|
57
|
+
value: SecretInput<R>,
|
|
58
|
+
): Output.Output<Redacted.Redacted<string>, R>;
|
|
59
|
+
export function Secret<R = never>(
|
|
60
|
+
name: string,
|
|
61
|
+
value?: SecretInput<R>,
|
|
62
|
+
): Output.Output<Redacted.Redacted<string>, R> {
|
|
63
|
+
const source = value ?? Config.redacted(name);
|
|
64
|
+
const base = toOutput<string | Redacted.Redacted<string>, R>(source);
|
|
65
|
+
return Output.named(
|
|
66
|
+
base.pipe(
|
|
67
|
+
Output.map((v) => (Redacted.isRedacted(v) ? v : Redacted.make(v))),
|
|
68
|
+
) as Output.Output<Redacted.Redacted<string>, R>,
|
|
69
|
+
name,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @internal */
|
|
74
|
+
export const toOutput = <T, R = never>(
|
|
75
|
+
source: T | Effect.Effect<T, any, R> | Config.Config<T> | Output.Output<T, R>,
|
|
76
|
+
): Output.Output<T, R> => {
|
|
77
|
+
if (Output.isOutput(source)) {
|
|
78
|
+
return source;
|
|
79
|
+
}
|
|
80
|
+
if (Config.isConfig(source)) {
|
|
81
|
+
return Output.asOutput(source.asEffect() as any) as Output.Output<T, R>;
|
|
82
|
+
}
|
|
83
|
+
if (Effect.isEffect(source)) {
|
|
84
|
+
return Output.asOutput(source as any) as Output.Output<T, R>;
|
|
85
|
+
}
|
|
86
|
+
return Output.asOutput(source as T) as Output.Output<T, R>;
|
|
87
|
+
};
|
|
@@ -3,7 +3,16 @@ import type { HttpEffect } from "../Http.ts";
|
|
|
3
3
|
import type { BaseRuntimeContext } from "../RuntimeContext.ts";
|
|
4
4
|
|
|
5
5
|
export interface FunctionContext extends BaseRuntimeContext {
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Register an HTTP handler. The optional `shape` payload is the user's
|
|
8
|
+
* full default-export shape — Cloudflare Workers use it to expose
|
|
9
|
+
* non-handler methods (e.g. `greet`) as RPC methods on the deployed
|
|
10
|
+
* `WorkerEntrypoint`. Other platforms (Lambda, etc.) ignore it.
|
|
11
|
+
*/
|
|
12
|
+
serve<Req = never>(
|
|
13
|
+
handler: HttpEffect<Req>,
|
|
14
|
+
options?: { shape?: Record<string, unknown> },
|
|
15
|
+
): Effect.Effect<void, never, Req>;
|
|
7
16
|
listen<A, Req = never>(
|
|
8
17
|
handler: FunctionListener<A, Req>,
|
|
9
18
|
): Effect.Effect<void, never, Req>;
|
package/src/Stack.ts
CHANGED
|
@@ -6,7 +6,7 @@ import * as Layer from "effect/Layer";
|
|
|
6
6
|
import * as Logger from "effect/Logger";
|
|
7
7
|
import * as Option from "effect/Option";
|
|
8
8
|
import { Path } from "effect/Path";
|
|
9
|
-
import
|
|
9
|
+
import * as Scope from "effect/Scope";
|
|
10
10
|
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
11
11
|
import type { HttpClient } from "effect/unstable/http/HttpClient";
|
|
12
12
|
import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
|
|
@@ -18,19 +18,18 @@ import { Profile, ProfileLive } from "./Auth/Profile.ts";
|
|
|
18
18
|
import { Cli } from "./Cli/Cli.ts";
|
|
19
19
|
import type { Input, InputProps } from "./Input.ts";
|
|
20
20
|
import * as Output from "./Output.ts";
|
|
21
|
-
import { ref } from "./Ref.ts";
|
|
22
21
|
import type { ResourceBinding, ResourceLike } from "./Resource.ts";
|
|
23
22
|
import { Stage } from "./Stage.ts";
|
|
24
23
|
import type { State } from "./State/State.ts";
|
|
25
24
|
import { loadConfigProvider } from "./Util/ConfigProvider.ts";
|
|
26
|
-
import { taggedFunction } from "./Util/effect.ts";
|
|
25
|
+
import { effectClass, taggedFunction } from "./Util/effect.ts";
|
|
27
26
|
import { fileLogger } from "./Util/FileLogger.ts";
|
|
28
27
|
import { PlatformServices } from "./Util/PlatformServices.ts";
|
|
29
28
|
|
|
30
29
|
export type StackServices =
|
|
31
30
|
| Stack
|
|
32
31
|
| Stage
|
|
33
|
-
| Scope
|
|
32
|
+
| Scope.Scope
|
|
34
33
|
| FileSystem
|
|
35
34
|
| Path
|
|
36
35
|
| AlchemyContext
|
|
@@ -46,7 +45,7 @@ export type StackEffect<A, Err = never, Req = never> = Effect.Effect<
|
|
|
46
45
|
Err,
|
|
47
46
|
| PlatformServices
|
|
48
47
|
| HttpClient
|
|
49
|
-
| Scope
|
|
48
|
+
| Scope.Scope
|
|
50
49
|
| AuthProviders
|
|
51
50
|
| AlchemyContext
|
|
52
51
|
| Cli
|
|
@@ -98,6 +97,7 @@ export const Stack: Context.ServiceClass<
|
|
|
98
97
|
(stackName: string): Effect.Effect<Self> & {
|
|
99
98
|
new (_: never): Output.ToOutput<Shape>;
|
|
100
99
|
make: <A, Req>(
|
|
100
|
+
options: StackProps<NoInfer<Req>>,
|
|
101
101
|
effect: Effect.Effect<A, never, Req>,
|
|
102
102
|
) => Effect.Effect<CompiledStack<A>>;
|
|
103
103
|
stage: {
|
|
@@ -114,36 +114,46 @@ export const Stack: Context.ServiceClass<
|
|
|
114
114
|
taggedFunction(
|
|
115
115
|
Context.Service<Stack, Omit<StackSpec, "output">>()("Stack"),
|
|
116
116
|
<A, Req>(
|
|
117
|
-
stackName
|
|
118
|
-
options
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
117
|
+
stackName?: string,
|
|
118
|
+
options?: StackProps<NoInfer<Req>>,
|
|
119
|
+
eff?: Effect.Effect<A, never, Req>,
|
|
120
|
+
) => {
|
|
121
|
+
if (!stackName) {
|
|
122
|
+
return (stackName: string) =>
|
|
123
|
+
Object.assign(
|
|
124
|
+
// by default, reference the stack at the "current" stage of the importer
|
|
125
|
+
Output.stackRef<A>(stackName).pipe(effectClass),
|
|
126
|
+
{
|
|
127
|
+
stage: createStageProxy(stackName),
|
|
128
|
+
make: <Req = never>(
|
|
129
|
+
options: StackProps<NoInfer<Req>>,
|
|
130
|
+
eff: Effect.Effect<A, never, Req>,
|
|
131
|
+
) => Stack(stackName, options, eff),
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return eff!.pipe(
|
|
125
136
|
make({
|
|
126
137
|
name: stackName,
|
|
127
|
-
...options
|
|
138
|
+
...options!,
|
|
128
139
|
}),
|
|
129
140
|
(eff) =>
|
|
130
141
|
Object.assign(eff, {
|
|
131
|
-
stage:
|
|
132
|
-
{},
|
|
133
|
-
{
|
|
134
|
-
get: (_, stage: string) =>
|
|
135
|
-
ref({
|
|
136
|
-
stack: stackName,
|
|
137
|
-
stage,
|
|
138
|
-
id: stackName,
|
|
139
|
-
}),
|
|
140
|
-
},
|
|
141
|
-
),
|
|
142
|
+
stage: createStageProxy(stackName),
|
|
142
143
|
}),
|
|
143
|
-
)
|
|
144
|
+
);
|
|
145
|
+
},
|
|
144
146
|
),
|
|
145
147
|
) as any;
|
|
146
148
|
|
|
149
|
+
const createStageProxy = (stackName: string) =>
|
|
150
|
+
new Proxy(
|
|
151
|
+
{},
|
|
152
|
+
{
|
|
153
|
+
get: (_, stage: string) => Output.stackRef(stackName, { stage }),
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
|
|
147
157
|
export interface StackSpec<Output = any> {
|
|
148
158
|
name: string;
|
|
149
159
|
stage: string;
|
|
@@ -283,9 +293,18 @@ export const evalStack = <A, B, Err, Req>(
|
|
|
283
293
|
options: {
|
|
284
294
|
stage: string;
|
|
285
295
|
dev?: boolean;
|
|
296
|
+
/**
|
|
297
|
+
* Optional caller-supplied scope. When provided, scoped resources
|
|
298
|
+
* (e.g. the dev sidecar process) live until the caller closes this
|
|
299
|
+
* scope instead of being torn down when `evalStack` resolves. Used
|
|
300
|
+
* by the test harness so the sidecar survives across `beforeAll`,
|
|
301
|
+
* tests, and `afterAll`. When omitted, behaves as before with a
|
|
302
|
+
* private scope closed on completion.
|
|
303
|
+
*/
|
|
304
|
+
scope?: Scope.Scope;
|
|
286
305
|
},
|
|
287
|
-
) =>
|
|
288
|
-
Effect.gen(function* () {
|
|
306
|
+
) => {
|
|
307
|
+
const body = Effect.gen(function* () {
|
|
289
308
|
const stack = yield* effect;
|
|
290
309
|
const configProvider = yield* loadConfigProvider(Option.none());
|
|
291
310
|
|
|
@@ -305,5 +324,9 @@ export const evalStack = <A, B, Err, Req>(
|
|
|
305
324
|
),
|
|
306
325
|
Effect.provide(Layer.succeed(Stage, options.stage)),
|
|
307
326
|
Effect.provide(Layer.provideMerge(alchemy({ dev: options.dev }), platform)),
|
|
308
|
-
Effect.scoped,
|
|
309
327
|
);
|
|
328
|
+
|
|
329
|
+
return options.scope === undefined
|
|
330
|
+
? Effect.scoped(body)
|
|
331
|
+
: Scope.provide(body, options.scope);
|
|
332
|
+
};
|
|
@@ -155,6 +155,25 @@ export const GetReplacedResources = HttpApiEndpoint.get(
|
|
|
155
155
|
},
|
|
156
156
|
);
|
|
157
157
|
|
|
158
|
+
export const GetStackOutput = HttpApiEndpoint.get(
|
|
159
|
+
"getStackOutput",
|
|
160
|
+
"/state/stacks/:stack/stages/:stage/output",
|
|
161
|
+
{
|
|
162
|
+
params: StackStage,
|
|
163
|
+
success: Schema.UndefinedOr(ResourceStateSchema),
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
export const SetStackOutput = HttpApiEndpoint.put(
|
|
168
|
+
"setStackOutput",
|
|
169
|
+
"/state/stacks/:stack/stages/:stage/output",
|
|
170
|
+
{
|
|
171
|
+
params: StackStage,
|
|
172
|
+
payload: ResourceStateSchema,
|
|
173
|
+
success: ResourceStateSchema,
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
158
177
|
/** Response shape for the unauthenticated `/version` probe. */
|
|
159
178
|
export const VersionResponse = Schema.Struct({
|
|
160
179
|
version: Schema.Number,
|
|
@@ -179,6 +198,8 @@ export class StateGroup extends HttpApiGroup.make("state")
|
|
|
179
198
|
.add(DeleteState)
|
|
180
199
|
.add(GetReplacedResources)
|
|
181
200
|
.add(DeleteStack)
|
|
201
|
+
.add(GetStackOutput)
|
|
202
|
+
.add(SetStackOutput)
|
|
182
203
|
.middleware(StateAuth) {}
|
|
183
204
|
|
|
184
205
|
export class VersionGroup extends HttpApiGroup.make("version").add(
|
|
@@ -129,6 +129,27 @@ export const makeHttpStateStore = ({
|
|
|
129
129
|
query: request.stage === undefined ? {} : { stage: request.stage },
|
|
130
130
|
})
|
|
131
131
|
.pipe(Effect.asVoid, mapStateStoreError),
|
|
132
|
+
getOutput: (request) =>
|
|
133
|
+
state
|
|
134
|
+
.getStackOutput({
|
|
135
|
+
params: { stack: request.stack, stage: request.stage },
|
|
136
|
+
})
|
|
137
|
+
.pipe(
|
|
138
|
+
Effect.map((s) =>
|
|
139
|
+
s == null ? undefined : reviveStateRecursive(s),
|
|
140
|
+
),
|
|
141
|
+
mapStateStoreError,
|
|
142
|
+
),
|
|
143
|
+
setOutput: (request) =>
|
|
144
|
+
state
|
|
145
|
+
.setStackOutput({
|
|
146
|
+
params: { stack: request.stack, stage: request.stage },
|
|
147
|
+
payload: encodeState(request.value as any),
|
|
148
|
+
})
|
|
149
|
+
.pipe(
|
|
150
|
+
Effect.map(() => request.value),
|
|
151
|
+
mapStateStoreError,
|
|
152
|
+
),
|
|
132
153
|
};
|
|
133
154
|
return service;
|
|
134
155
|
});
|
|
@@ -13,14 +13,18 @@ export const inMemoryState = (
|
|
|
13
13
|
StackId,
|
|
14
14
|
Record<StageId, Record<Fqn, ResourceState>>
|
|
15
15
|
> = {},
|
|
16
|
+
initialOutputs: Record<StackId, Record<StageId, unknown>> = {},
|
|
16
17
|
) =>
|
|
17
18
|
Layer.effect(
|
|
18
19
|
State,
|
|
19
|
-
Effect.succeed(InMemoryService(initialState)).pipe(
|
|
20
|
+
Effect.succeed(InMemoryService(initialState, initialOutputs)).pipe(
|
|
21
|
+
recordStateStoreInit,
|
|
22
|
+
),
|
|
20
23
|
);
|
|
21
24
|
|
|
22
25
|
export const InMemoryService = (
|
|
23
26
|
state: Record<StackId, Record<StageId, Record<Fqn, ResourceState>>> = {},
|
|
27
|
+
outputs: Record<StackId, Record<StageId, unknown>> = {},
|
|
24
28
|
) =>
|
|
25
29
|
State.of({
|
|
26
30
|
id: "inmemory",
|
|
@@ -60,12 +64,13 @@ export const InMemoryService = (
|
|
|
60
64
|
stage: string;
|
|
61
65
|
fqn: string;
|
|
62
66
|
value: V;
|
|
63
|
-
}) =>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
}) =>
|
|
68
|
+
Effect.sync(() => {
|
|
69
|
+
const stackState = (state[stack] ??= {});
|
|
70
|
+
const stageState = (stackState[stage] ??= {});
|
|
71
|
+
stageState[fqn] = value;
|
|
72
|
+
return value;
|
|
73
|
+
}),
|
|
69
74
|
delete: ({
|
|
70
75
|
stack,
|
|
71
76
|
stage,
|
|
@@ -74,7 +79,7 @@ export const InMemoryService = (
|
|
|
74
79
|
stack: string;
|
|
75
80
|
stage: string;
|
|
76
81
|
fqn: string;
|
|
77
|
-
}) => Effect.
|
|
82
|
+
}) => Effect.sync(() => delete state[stack]?.[stage]?.[fqn]),
|
|
78
83
|
deleteStack: ({ stack, stage }: { stack: string; stage?: string }) =>
|
|
79
84
|
Effect.sync(() => {
|
|
80
85
|
if (stage === undefined) {
|
|
@@ -87,4 +92,20 @@ export const InMemoryService = (
|
|
|
87
92
|
Effect.succeed(
|
|
88
93
|
Array.from(Object.keys(state[stack]?.[stage] ?? {}) ?? []),
|
|
89
94
|
),
|
|
95
|
+
getOutput: ({ stack, stage }: { stack: string; stage: string }) =>
|
|
96
|
+
Effect.succeed(outputs[stack]?.[stage]),
|
|
97
|
+
setOutput: ({
|
|
98
|
+
stack,
|
|
99
|
+
stage,
|
|
100
|
+
value,
|
|
101
|
+
}: {
|
|
102
|
+
stack: string;
|
|
103
|
+
stage: string;
|
|
104
|
+
value: unknown;
|
|
105
|
+
}) =>
|
|
106
|
+
Effect.sync(() => {
|
|
107
|
+
const stackOutputs = (outputs[stack] ??= {});
|
|
108
|
+
stackOutputs[stage] = value;
|
|
109
|
+
return value;
|
|
110
|
+
}),
|
|
90
111
|
});
|
package/src/State/LocalState.ts
CHANGED
|
@@ -46,6 +46,9 @@ export const makeLocalState = () =>
|
|
|
46
46
|
fqn: string;
|
|
47
47
|
}) => path.join(stateDir, stack, stage, `${encodeFqn(fqn)}.json`);
|
|
48
48
|
|
|
49
|
+
const outputFile = ({ stack, stage }: { stack: string; stage: string }) =>
|
|
50
|
+
path.join(stateDir, stack, stage, `__stack_output__.json`);
|
|
51
|
+
|
|
49
52
|
const created = new Set<string>();
|
|
50
53
|
|
|
51
54
|
const ensure = (dir: string) =>
|
|
@@ -107,11 +110,32 @@ export const makeLocalState = () =>
|
|
|
107
110
|
list: (request) =>
|
|
108
111
|
fs.readDirectory(stageDir(request)).pipe(
|
|
109
112
|
recover,
|
|
110
|
-
Effect.map(
|
|
111
|
-
(files)
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
Effect.map((files) =>
|
|
114
|
+
(files ?? [])
|
|
115
|
+
// Filter the bookkeeping file before decoding — `decodeFqn`
|
|
116
|
+
// replaces `__` with `/`, which would turn the literal name
|
|
117
|
+
// `__stack_output__` into `/stack_output/` and slip past the
|
|
118
|
+
// filter, leaving the engine to look up a non-existent
|
|
119
|
+
// resource.
|
|
120
|
+
.filter((file) => file !== "__stack_output__.json")
|
|
121
|
+
.map((file) => decodeFqn(file.replace(/\.json$/, ""))),
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
getOutput: (request) =>
|
|
125
|
+
fs.readFile(outputFile(request)).pipe(
|
|
126
|
+
Effect.map((file) => JSON.parse(file.toString(), reviveState)),
|
|
127
|
+
recover,
|
|
128
|
+
),
|
|
129
|
+
setOutput: (request) =>
|
|
130
|
+
ensure(stageDir(request)).pipe(
|
|
131
|
+
Effect.flatMap(() =>
|
|
132
|
+
fs.writeFileString(
|
|
133
|
+
outputFile(request),
|
|
134
|
+
JSON.stringify(encodeState(request.value as any), null, 2),
|
|
135
|
+
),
|
|
114
136
|
),
|
|
137
|
+
recover,
|
|
138
|
+
Effect.map(() => request.value),
|
|
115
139
|
),
|
|
116
140
|
};
|
|
117
141
|
return state;
|
package/src/State/State.ts
CHANGED
|
@@ -87,4 +87,25 @@ export interface StateService {
|
|
|
87
87
|
stack: string;
|
|
88
88
|
stage: string;
|
|
89
89
|
}): Effect.Effect<readonly string[], StateStoreError, never>;
|
|
90
|
+
/**
|
|
91
|
+
* Read the persisted stack output for `(stack, stage)`. Returns
|
|
92
|
+
* `undefined` when the stack has not been deployed (or has been
|
|
93
|
+
* destroyed) at this stage.
|
|
94
|
+
*
|
|
95
|
+
* Stack outputs are written by `apply` once the deploy succeeds and
|
|
96
|
+
* read by cross-stack references (`yield* OtherStack` /
|
|
97
|
+
* `OtherStack.stage.<name>` / `Output.stackRef(...)`).
|
|
98
|
+
*/
|
|
99
|
+
getOutput(request: {
|
|
100
|
+
stack: string;
|
|
101
|
+
stage: string;
|
|
102
|
+
}): Effect.Effect<unknown, StateStoreError, never>;
|
|
103
|
+
/**
|
|
104
|
+
* Persist the resolved stack output for `(stack, stage)`.
|
|
105
|
+
*/
|
|
106
|
+
setOutput(request: {
|
|
107
|
+
stack: string;
|
|
108
|
+
stage: string;
|
|
109
|
+
value: unknown;
|
|
110
|
+
}): Effect.Effect<unknown, StateStoreError, never>;
|
|
90
111
|
}
|
package/src/Test/Bun.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import bun from "bun:test";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Exit from "effect/Exit";
|
|
4
|
+
import * as Scope from "effect/Scope";
|
|
3
5
|
import type { HookOptions } from "node:test";
|
|
4
6
|
|
|
5
7
|
import type { AlchemyContext } from "../AlchemyContext.ts";
|
|
6
8
|
import type { CompiledStack } from "../Stack.ts";
|
|
7
9
|
import type { Stage } from "../Stage.ts";
|
|
10
|
+
import { installLocalhostDns } from "../Util/LocalhostDns.ts";
|
|
8
11
|
import * as Core from "./Core.ts";
|
|
9
12
|
|
|
13
|
+
installLocalhostDns();
|
|
14
|
+
|
|
10
15
|
export type MakeOptions<ROut = any> = Core.MakeOptions<ROut>;
|
|
11
16
|
export type ScratchStack = Core.ScratchStack;
|
|
12
17
|
export type TestEffect<A, R = never> = Core.TestEffect<A, R>;
|
|
@@ -106,7 +111,15 @@ const DEFAULT_HOOK_TIMEOUT: HookOptions = { timeout: 120_000 };
|
|
|
106
111
|
* ```
|
|
107
112
|
*/
|
|
108
113
|
export const make = <ROut = any>(options: MakeOptions<ROut>): TestApi => {
|
|
109
|
-
|
|
114
|
+
// Single scope shared across `beforeAll`, every `test`, and `afterAll`.
|
|
115
|
+
// Scoped resources in dev mode (the Cloudflare sidecar process and its
|
|
116
|
+
// workerd children) must outlive a single `Effect.runPromise` boundary,
|
|
117
|
+
// otherwise the proxy is killed the moment `beforeAll(deploy(Stack))`
|
|
118
|
+
// resolves and every later `HttpClient.get(workerUrl)` hits a dead port.
|
|
119
|
+
// The scope is closed by `destroy(...)` (or never — the next test run
|
|
120
|
+
// reclaims any leaked sidecar via the lock file).
|
|
121
|
+
const sharedScope = Scope.makeUnsafe("sequential");
|
|
122
|
+
const runEff = <A>(eff: TestEffect<A>) => Core.run(eff, options, sharedScope);
|
|
110
123
|
|
|
111
124
|
const test = ((name, eff, opts) => {
|
|
112
125
|
bun.test(name, () => runEff(eff), opts);
|
|
@@ -130,10 +143,11 @@ export const make = <ROut = any>(options: MakeOptions<ROut>): TestApi => {
|
|
|
130
143
|
fn: (stack: ScratchStack) => Effect.Effect<void, any, any>,
|
|
131
144
|
) => {
|
|
132
145
|
const scratch = Core.scratchStack(options, name);
|
|
133
|
-
return Core.run(
|
|
134
|
-
|
|
135
|
-
state: scratch.state,
|
|
136
|
-
|
|
146
|
+
return Core.run(
|
|
147
|
+
Core.withProviders(fn(scratch), options, scratch.name),
|
|
148
|
+
{ ...options, state: scratch.state },
|
|
149
|
+
sharedScope,
|
|
150
|
+
);
|
|
137
151
|
};
|
|
138
152
|
|
|
139
153
|
const provider = ((name, fn, opts) => {
|
|
@@ -175,13 +189,35 @@ export const make = <ROut = any>(options: MakeOptions<ROut>): TestApi => {
|
|
|
175
189
|
bun.afterEach(() => runEff(eff), hookOptions);
|
|
176
190
|
};
|
|
177
191
|
|
|
192
|
+
// `destroy(Stack)` needs the dev sidecar alive so it can call `sidecar.stop`
|
|
193
|
+
// for each worker. We close the shared scope only AFTER destroy completes.
|
|
194
|
+
// `Scope.close` on an already-closed scope is a no-op, so it's safe for both
|
|
195
|
+
// the destroy wrapper AND the fallback cleanup hook below to call it.
|
|
196
|
+
const closeScope = Effect.suspend(() =>
|
|
197
|
+
Scope.close(sharedScope, Exit.void),
|
|
198
|
+
).pipe(Effect.ignore);
|
|
199
|
+
|
|
200
|
+
// Fallback cleanup: if the user never calls `destroy(Stack)` (e.g.
|
|
201
|
+
// `NO_DESTROY=1`), nothing else closes the shared scope and the sidecar
|
|
202
|
+
// child process leaks past the test process. Register an `afterAll` that
|
|
203
|
+
// closes it. We defer registration to a microtask so it runs AFTER any
|
|
204
|
+
// user-registered `afterAll` (including `destroy(Stack)`); bun runs
|
|
205
|
+
// afterAll hooks in registration order.
|
|
206
|
+
queueMicrotask(() => {
|
|
207
|
+
bun.afterAll(() => Effect.runPromise(closeScope), DEFAULT_HOOK_TIMEOUT);
|
|
208
|
+
});
|
|
209
|
+
|
|
178
210
|
return {
|
|
179
211
|
test,
|
|
180
212
|
beforeAll,
|
|
181
213
|
beforeEach,
|
|
182
214
|
afterAll,
|
|
183
215
|
afterEach,
|
|
184
|
-
deploy: (stack, callOpts) =>
|
|
185
|
-
|
|
216
|
+
deploy: (stack, callOpts) =>
|
|
217
|
+
Core.deploy(options, stack, { ...callOpts, scope: sharedScope }),
|
|
218
|
+
destroy: (stack, callOpts) =>
|
|
219
|
+
Core.destroy(options, stack, { ...callOpts, scope: sharedScope }).pipe(
|
|
220
|
+
Effect.ensuring(closeScope),
|
|
221
|
+
),
|
|
186
222
|
};
|
|
187
223
|
};
|