alchemy-effect 0.0.0

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.
Files changed (101) hide show
  1. package/lib/app.d.ts +23 -0
  2. package/lib/app.d.ts.map +1 -0
  3. package/lib/app.js +26 -0
  4. package/lib/app.js.map +1 -0
  5. package/lib/apply.d.ts +22 -0
  6. package/lib/apply.d.ts.map +1 -0
  7. package/lib/apply.js +191 -0
  8. package/lib/apply.js.map +1 -0
  9. package/lib/approve.d.ts +15 -0
  10. package/lib/approve.d.ts.map +1 -0
  11. package/lib/approve.js +7 -0
  12. package/lib/approve.js.map +1 -0
  13. package/lib/binding.d.ts +58 -0
  14. package/lib/binding.d.ts.map +1 -0
  15. package/lib/binding.js +27 -0
  16. package/lib/binding.js.map +1 -0
  17. package/lib/capability.d.ts +11 -0
  18. package/lib/capability.d.ts.map +1 -0
  19. package/lib/capability.js +1 -0
  20. package/lib/capability.js.map +1 -0
  21. package/lib/destroy.d.ts +4 -0
  22. package/lib/destroy.d.ts.map +1 -0
  23. package/lib/destroy.js +1 -0
  24. package/lib/destroy.js.map +1 -0
  25. package/lib/dot-alchemy.d.ts +9 -0
  26. package/lib/dot-alchemy.d.ts.map +1 -0
  27. package/lib/dot-alchemy.js +14 -0
  28. package/lib/dot-alchemy.js.map +1 -0
  29. package/lib/env.d.ts +4 -0
  30. package/lib/env.d.ts.map +1 -0
  31. package/lib/env.js +33 -0
  32. package/lib/env.js.map +1 -0
  33. package/lib/event.d.ts +16 -0
  34. package/lib/event.d.ts.map +1 -0
  35. package/lib/event.js +1 -0
  36. package/lib/event.js.map +1 -0
  37. package/lib/index.d.ts +18 -0
  38. package/lib/index.d.ts.map +1 -0
  39. package/lib/index.js +18 -0
  40. package/lib/index.js.map +1 -0
  41. package/lib/phase.d.ts +2 -0
  42. package/lib/phase.d.ts.map +1 -0
  43. package/lib/phase.js +1 -0
  44. package/lib/phase.js.map +1 -0
  45. package/lib/physical-name.d.ts +4 -0
  46. package/lib/physical-name.d.ts.map +1 -0
  47. package/lib/physical-name.js +7 -0
  48. package/lib/physical-name.js.map +1 -0
  49. package/lib/plan.d.ts +98 -0
  50. package/lib/plan.d.ts.map +1 -0
  51. package/lib/plan.js +228 -0
  52. package/lib/plan.js.map +1 -0
  53. package/lib/policy.d.ts +37 -0
  54. package/lib/policy.d.ts.map +1 -0
  55. package/lib/policy.js +16 -0
  56. package/lib/policy.js.map +1 -0
  57. package/lib/provider.d.ts +57 -0
  58. package/lib/provider.d.ts.map +1 -0
  59. package/lib/provider.js +2 -0
  60. package/lib/provider.js.map +1 -0
  61. package/lib/reporter.d.ts +8 -0
  62. package/lib/reporter.d.ts.map +1 -0
  63. package/lib/reporter.js +4 -0
  64. package/lib/reporter.js.map +1 -0
  65. package/lib/resource.d.ts +38 -0
  66. package/lib/resource.d.ts.map +1 -0
  67. package/lib/resource.js +25 -0
  68. package/lib/resource.js.map +1 -0
  69. package/lib/runtime.d.ts +39 -0
  70. package/lib/runtime.d.ts.map +1 -0
  71. package/lib/runtime.js +54 -0
  72. package/lib/runtime.js.map +1 -0
  73. package/lib/service.d.ts +33 -0
  74. package/lib/service.d.ts.map +1 -0
  75. package/lib/service.js +4 -0
  76. package/lib/service.js.map +1 -0
  77. package/lib/state.d.ts +38 -0
  78. package/lib/state.d.ts.map +1 -0
  79. package/lib/state.js +66 -0
  80. package/lib/state.js.map +1 -0
  81. package/package.json +49 -0
  82. package/src/app.ts +43 -0
  83. package/src/apply.ts +318 -0
  84. package/src/approve.ts +13 -0
  85. package/src/binding.ts +144 -0
  86. package/src/capability.ts +19 -0
  87. package/src/destroy.ts +4 -0
  88. package/src/dot-alchemy.ts +17 -0
  89. package/src/env.ts +51 -0
  90. package/src/event.ts +27 -0
  91. package/src/index.ts +17 -0
  92. package/src/phase.ts +1 -0
  93. package/src/physical-name.ts +7 -0
  94. package/src/plan.ts +470 -0
  95. package/src/policy.ts +77 -0
  96. package/src/provider.ts +74 -0
  97. package/src/reporter.ts +8 -0
  98. package/src/resource.ts +68 -0
  99. package/src/runtime.ts +145 -0
  100. package/src/service.ts +55 -0
  101. package/src/state.ts +196 -0
package/src/runtime.ts ADDED
@@ -0,0 +1,145 @@
1
+ import type { Types } from "effect";
2
+ import * as Context from "effect/Context";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Layer from "effect/Layer";
5
+ import type { Capability } from "./capability.ts";
6
+ import type { Policy } from "./policy.ts";
7
+ import type { ProviderService } from "./provider.ts";
8
+ import type { Resource, ResourceTags } from "./resource.ts";
9
+ import type { IService, Service } from "./service.ts";
10
+
11
+ export type RuntimeHandler<
12
+ Inputs extends any[] = any[],
13
+ Output = any,
14
+ Err = any,
15
+ Req = any,
16
+ > = (...inputs: Inputs) => Effect.Effect<Output, Err, Req>;
17
+
18
+ export declare namespace RuntimeHandler {
19
+ export type Caps<H extends RuntimeHandler | unknown> = Extract<
20
+ Effect.Effect.Context<ReturnType<Extract<H, RuntimeHandler>>>,
21
+ Capability
22
+ >;
23
+ }
24
+
25
+ export declare namespace Runtime {
26
+ export type Binding<F, Cap> = F extends {
27
+ readonly Binding: unknown;
28
+ }
29
+ ? (F & {
30
+ readonly cap: Cap;
31
+ })["Binding"]
32
+ : {
33
+ readonly F: F;
34
+ readonly cap: Types.Contravariant<Cap>;
35
+ };
36
+ }
37
+
38
+ export type AnyRuntime = Runtime<string>;
39
+
40
+ export interface RuntimeProps<Run extends Runtime, Req> {
41
+ bindings: Policy<Run, Extract<Req, Capability>>;
42
+ }
43
+
44
+ export interface Runtime<
45
+ Type extends string = string,
46
+ Handler = unknown,
47
+ Props = unknown,
48
+ > extends Resource<Type, string, Props, unknown> {
49
+ type: Type;
50
+ props: Props;
51
+ handler: Handler;
52
+ binding: unknown;
53
+ /** @internal phantom */
54
+ capability: unknown;
55
+ <
56
+ const ID extends string,
57
+ Inputs extends any[],
58
+ Output,
59
+ Err,
60
+ Req,
61
+ Handler extends RuntimeHandler<Inputs, Output, Err, Req>,
62
+ >(
63
+ id: ID,
64
+ { handle }: { handle: Handler },
65
+ ): <const Props extends this["props"]>(
66
+ props: Props,
67
+ ) => Service<
68
+ ID,
69
+ this,
70
+ Handler,
71
+ // @ts-expect-error
72
+ Props
73
+ >;
74
+ }
75
+
76
+ export const Runtime =
77
+ <const Type extends string>(type: Type) =>
78
+ <Self extends Runtime>(): Self & {
79
+ provider: ResourceTags<Self>;
80
+ } => {
81
+ const Tag = Context.Tag(type)();
82
+ const provider = {
83
+ tag: Tag,
84
+ effect: (eff: Effect.Effect<ProviderService<Self>, any, any>) =>
85
+ Layer.effect(Tag, eff),
86
+ succeed: (service: ProviderService<Self>) => Layer.succeed(Tag, service),
87
+ };
88
+ const self = Object.assign(
89
+ (
90
+ ...args:
91
+ | [cap: Capability]
92
+ | [
93
+ id: string,
94
+ { handle: (...args: any[]) => Effect.Effect<any, never, any> },
95
+ ]
96
+ ) => {
97
+ if (args.length === 1) {
98
+ const [cap] = args;
99
+ const tag = `${type}(${cap})` as const;
100
+ return class extends Context.Tag(tag)<Self, string>() {
101
+ Capability = cap;
102
+ };
103
+ } else {
104
+ const [id, { handle }] = args;
105
+ return <const Props extends RuntimeProps<Self, any>>(props: Props) =>
106
+ Object.assign(
107
+ class {
108
+ constructor() {
109
+ throw new Error("Cannot instantiate a Service directly");
110
+ }
111
+ },
112
+ {
113
+ kind: "Service",
114
+ type,
115
+ id,
116
+ attr: undefined!,
117
+ impl: handle,
118
+ handler: Effect.succeed(handle as any),
119
+ props,
120
+ runtime: self,
121
+ // TODO(sam): is this right?
122
+ parent: self,
123
+ // @ts-expect-error
124
+ provider,
125
+ } satisfies IService<string, Self, any, any>,
126
+ );
127
+ }
128
+ },
129
+ {
130
+ kind: "Runtime",
131
+ type: type,
132
+ id: undefined! as string,
133
+ capability: undefined! as Capability[],
134
+ provider,
135
+ toString() {
136
+ return `${this.type}(${this.id}${
137
+ this.capability?.length
138
+ ? `, ${this.capability.map((c) => `${c}`).join(", ")}`
139
+ : ""
140
+ })`;
141
+ },
142
+ },
143
+ ) as unknown as Self;
144
+ return self as any;
145
+ };
package/src/service.ts ADDED
@@ -0,0 +1,55 @@
1
+ import type { Effect } from "effect/Effect";
2
+ import type { Capability } from "./capability.ts";
3
+ import type { Resource } from "./resource.ts";
4
+ import type { Runtime, RuntimeHandler, RuntimeProps } from "./runtime.ts";
5
+
6
+ export interface IService<
7
+ ID extends string = string,
8
+ F extends Runtime = Runtime,
9
+ Handler extends RuntimeHandler = RuntimeHandler,
10
+ Props extends RuntimeProps<F, any> = RuntimeProps<F, any>,
11
+ Attr = (F & { props: Props })["attr"],
12
+ > extends Resource<F["type"], ID, Props, Attr> {
13
+ kind: "Service";
14
+ type: F["type"];
15
+ id: ID;
16
+ runtime: F;
17
+ /**
18
+ * The raw handler function as passed in to the Runtime.
19
+ *
20
+ * @internal phantom type
21
+ */
22
+ impl: Handler;
23
+ /**
24
+ * An Effect that produces a handler stripped of its Infrastructure-time-only Capabilities.
25
+ */
26
+ handler: Effect<
27
+ (
28
+ ...inputs: Parameters<Handler>
29
+ ) => Effect<
30
+ Effect.Success<ReturnType<Handler>>,
31
+ Effect.Error<ReturnType<Handler>>,
32
+ never
33
+ >,
34
+ never,
35
+ Exclude<Effect.Context<ReturnType<Handler>>, Capability>
36
+ >;
37
+ props: Props;
38
+ /** @internal phantom type of this resource's output attributes */
39
+ attr: Attr;
40
+ /** @internal phantom type of this resource's parent */
41
+ parent: unknown;
42
+ }
43
+
44
+ export interface Service<
45
+ ID extends string = string,
46
+ F extends Runtime = Runtime,
47
+ Handler extends RuntimeHandler = RuntimeHandler,
48
+ Props extends RuntimeProps<F, any> = RuntimeProps<F, any>,
49
+ Attr = (F & { props: Props })["attr"],
50
+ > extends IService<ID, F, Handler, Props, Attr>,
51
+ Resource<F["type"], ID, Props, Attr> {}
52
+
53
+ export const isService = (resource: any): resource is IService => {
54
+ return resource && resource.kind === "Service";
55
+ };
package/src/state.ts ADDED
@@ -0,0 +1,196 @@
1
+ import { FileSystem } from "@effect/platform";
2
+ import type { PlatformError } from "@effect/platform/Error";
3
+ import * as Context from "effect/Context";
4
+ import * as Data from "effect/Data";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Layer from "effect/Layer";
7
+ import path from "node:path";
8
+ import { App } from "./app.ts";
9
+ import type { BindNode } from "./plan.ts";
10
+ import { isResource } from "./resource.ts";
11
+
12
+ // SQL only?? no
13
+ // DynamoDB is faster but bounded to 400KB (<10ms minimum latency)
14
+ // S3 is slower but unbounded in size (200ms minimum latency)
15
+ // -> dual purpose for assets
16
+ // -> batching? or one file? -> Pipeline to one file. Versioned S3 Object for logs.
17
+ // -> concern with one file is size: some of our resources have like the whole fucking lambda
18
+ // -> hash them? or etag. etag is md5
19
+ // -> there are more large data that aren't files?
20
+ // -> e.g. asset manifest
21
+ // -> pointer to one file? too clever?
22
+ // -> sqlite on S3?
23
+ // SQlite on EFS. But needs a VPN.
24
+ // Roll back just from the state store??? -> needs to be fast and "build-less"
25
+ // JSON or Message Pack? I vote JSON (easy to read)
26
+
27
+ // Artifact -> stored hash only, compared on hash, not available during DELETE
28
+ // -> can't rollback just from state
29
+ // -> store it as a separate file, avoid re-writes, etc.
30
+
31
+ // SQLite in S3
32
+ // -> download, do all updates locally, upload?
33
+ // -> stream uploads
34
+ // -> not durable, but we accept that we CANT be durable
35
+ // -> it's also fast if you don't upload often
36
+
37
+ // S3 would still be fast because we sync locally
38
+
39
+ // ## Encryption
40
+ // ALCHEMY_PASSWORD suck
41
+ // ALCHEMY_STATE_TOKEN suck
42
+ // We are flattening (no more nested any state)
43
+
44
+ // in AWS this is easy - SSE (SSE + KMS)
45
+ // -> some companies would prefer CSE
46
+
47
+ // Library level encryption (SDK) -> default to no-op, favor SSE on AWS S3
48
+ // On AWS it would be KMS (we can just use IAM Role)
49
+ // On CF -> generate a Token and store in Secrets Manager?
50
+ // -> Store it in R2 because we can't get it out?
51
+ // -> Or build KMS on top of Workers+DO?
52
+ // -> R2 lets us use OAuth to gain access to the encryption token
53
+
54
+ // Scrap the "key-value" store on State/Scope
55
+
56
+ export type ResourceStatus =
57
+ | "creating"
58
+ | "created"
59
+ | "updating"
60
+ | "updated"
61
+ | "deleting"
62
+ | "deleted";
63
+
64
+ export type ResourceState = {
65
+ type: string;
66
+ id: string;
67
+ status: ResourceStatus;
68
+ props: any;
69
+ output: any;
70
+ bindings?: BindNode[];
71
+ };
72
+
73
+ export class StateStoreError extends Data.TaggedError("StateStoreError")<{
74
+ message: string;
75
+ }> {}
76
+
77
+ export interface StateService {
78
+ listApps(): Effect.Effect<string[], StateStoreError, never>;
79
+ listStages(appName?: string): Effect.Effect<string[], StateStoreError, never>;
80
+ // stub
81
+ get(
82
+ id: string,
83
+ ): Effect.Effect<ResourceState | undefined, StateStoreError, never>;
84
+ set<V extends ResourceState>(
85
+ id: string,
86
+ value: V,
87
+ ): Effect.Effect<V, StateStoreError, never>;
88
+ delete(id: string): Effect.Effect<void, StateStoreError, never>;
89
+ list(): Effect.Effect<string[], StateStoreError, never>;
90
+ }
91
+
92
+ export class State extends Context.Tag("AWS::Lambda::State")<
93
+ State,
94
+ StateService
95
+ >() {}
96
+
97
+ // TODO(sam): implement with SQLite3
98
+ export const localFs = Layer.effect(
99
+ State,
100
+ Effect.gen(function* () {
101
+ const app = yield* App;
102
+ const fs = yield* FileSystem.FileSystem;
103
+ const dotAlchemy = path.join(process.cwd(), ".alchemy");
104
+ const stateDir = path.join(dotAlchemy, "state");
105
+ const appDir = path.join(stateDir, app.name);
106
+ const stageDir = path.join(appDir, app.stage);
107
+
108
+ const fail = (err: PlatformError) =>
109
+ Effect.fail(
110
+ new StateStoreError({
111
+ message: err.description ?? err.message,
112
+ }),
113
+ );
114
+
115
+ const recover = <T>(effect: Effect.Effect<T, PlatformError, never>) =>
116
+ effect.pipe(
117
+ Effect.catchTag("SystemError", (e) =>
118
+ e.reason === "NotFound" ? Effect.succeed(undefined) : fail(e),
119
+ ),
120
+ Effect.catchTag("BadArgument", (e) => fail(e)),
121
+ );
122
+
123
+ const resourceFile = (id: string) => path.join(stageDir, `${id}.json`);
124
+
125
+ yield* fs.makeDirectory(stageDir, { recursive: true });
126
+
127
+ return {
128
+ listApps: () =>
129
+ fs.readDirectory(stateDir).pipe(
130
+ recover,
131
+ Effect.map((files) => files ?? []),
132
+ ),
133
+ listStages: (appName: string = app.name) =>
134
+ fs.readDirectory(path.join(stateDir, appName)).pipe(
135
+ recover,
136
+ Effect.map((files) => files ?? []),
137
+ ),
138
+ get: (id) =>
139
+ fs.readFile(resourceFile(id)).pipe(
140
+ Effect.map((file) => JSON.parse(file.toString())),
141
+ recover,
142
+ ),
143
+ set: <V extends ResourceState>(id: string, value: V) =>
144
+ fs
145
+ .writeFileString(
146
+ resourceFile(id),
147
+ JSON.stringify(
148
+ value,
149
+ (k, v) => {
150
+ if (isResource(v)) {
151
+ return {
152
+ id: v.id,
153
+ type: v.type,
154
+ props: v.props,
155
+ attr: v.attr,
156
+ };
157
+ }
158
+ return v;
159
+ },
160
+ 2,
161
+ ),
162
+ )
163
+ .pipe(
164
+ recover,
165
+ Effect.map(() => value),
166
+ ),
167
+ delete: (id) => fs.remove(resourceFile(id)).pipe(recover),
168
+ list: () =>
169
+ fs.readDirectory(stageDir).pipe(
170
+ recover,
171
+ Effect.map(
172
+ (files) => files?.map((file) => file.replace(/\.json$/, "")) ?? [],
173
+ ),
174
+ ),
175
+ };
176
+ }),
177
+ );
178
+
179
+ export const inMemory = () => {
180
+ const state = new Map<string, any>();
181
+ return Layer.succeed(State, {
182
+ listApps: () => Effect.succeed([]),
183
+ // oxlint-disable-next-line require-yield
184
+ listStages: (_appName?: string) => Effect.succeed([]),
185
+ get: (id: string) => Effect.succeed(state.get(id)),
186
+ set: <V>(id: string, value: V) => {
187
+ state.set(id, value);
188
+ return Effect.succeed(value);
189
+ },
190
+ delete: (id: string) => {
191
+ state.delete(id);
192
+ return Effect.succeed(undefined);
193
+ },
194
+ list: () => Effect.succeed(Array.from(state.keys())),
195
+ });
196
+ };