alchemy-effect 0.6.2 → 0.6.4

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.
@@ -10,6 +10,7 @@ import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner
10
10
  import { RpcClient, RpcSerialization } from "effect/unstable/rpc";
11
11
  import * as Socket from "effect/unstable/socket/Socket";
12
12
  import { resolveSocketPath } from "./Config.ts";
13
+ import { DaemonConnectFailed, DaemonSocketNotReady } from "./Errors.ts";
13
14
  import { DaemonRpcs } from "./RpcSchema.ts";
14
15
 
15
16
  export type DaemonClient = Effect.Success<ReturnType<typeof makeClient>>;
@@ -58,17 +59,19 @@ export const DaemonLive: Layer.Layer<
58
59
  const socketPath = yield* resolveSocketPath;
59
60
 
60
61
  const client = yield* tryConnect(socketPath).pipe(
61
- Effect.catch(() =>
62
+ Effect.catchTag("DaemonConnectFailed", () =>
62
63
  Effect.gen(function* () {
63
64
  yield* Effect.logInfo("Starting daemon…");
64
65
  yield* startDaemonProcess.pipe(Effect.forkChild);
65
66
  yield* waitForSocket(socketPath);
66
67
  yield* Effect.sleep("200 millis");
67
- return yield* makeClient(socketPath);
68
+ return yield* tryConnect(socketPath);
68
69
  }),
69
70
  ),
70
71
  Effect.catchTag("PlatformError", (e) => Effect.die(e)),
71
- Effect.catch(() => Effect.die(new Error("Failed to connect to daemon"))),
72
+ Effect.catchTag("DaemonConnectFailed", () =>
73
+ Effect.die(new Error("Failed to connect to daemon")),
74
+ ),
72
75
  );
73
76
 
74
77
  yield* client
@@ -90,19 +93,20 @@ const waitForSocket = (socketPath: string) =>
90
93
  const fs = yield* FileSystem.FileSystem;
91
94
  yield* fs.exists(socketPath).pipe(
92
95
  Effect.flatMap((exists) =>
93
- exists ? Effect.void : Effect.fail("not yet" as const),
96
+ exists ? Effect.void : Effect.fail(new DaemonSocketNotReady()),
94
97
  ),
95
98
  Effect.retry(
96
- Schedule.spaced("100 millis").pipe(
97
- Schedule.compose(Schedule.recurs(50)),
98
- ),
99
+ Schedule.spaced("100 millis").pipe(Schedule.both(Schedule.recurs(50))),
100
+ ),
101
+ Effect.catchTag("DaemonSocketNotReady", () =>
102
+ Effect.fail(new DaemonConnectFailed()),
99
103
  ),
100
104
  );
101
105
  });
102
106
 
103
107
  const tryConnect = (socketPath: string) =>
104
108
  makeClient(socketPath).pipe(
105
- Effect.catch(() => Effect.fail("connect-failed" as const)),
109
+ Effect.catch(() => Effect.fail(new DaemonConnectFailed())),
106
110
  );
107
111
 
108
112
  const startDaemonProcess = Effect.gen(function* () {
@@ -13,7 +13,7 @@ export class ProcessNotFound extends Schema.TaggedClass<ProcessNotFound>()(
13
13
  { id: Schema.String },
14
14
  ) {}
15
15
 
16
- // Data-based errors used only by the server
16
+ // Data-based errors used internally by the daemon runtime
17
17
 
18
18
  export class DaemonAlreadyRunning extends Data.TaggedError(
19
19
  "DaemonAlreadyRunning",
@@ -30,3 +30,19 @@ export class DaemonAlreadyRunning extends Data.TaggedError(
30
30
  export class LockCompromised extends Data.TaggedError("LockCompromised")<{
31
31
  readonly lockDir: string;
32
32
  }> {}
33
+
34
+ export class DaemonSocketNotReady extends Data.TaggedError(
35
+ "DaemonSocketNotReady",
36
+ ) {
37
+ get message() {
38
+ return "Daemon socket is not ready yet";
39
+ }
40
+ }
41
+
42
+ export class DaemonConnectFailed extends Data.TaggedError(
43
+ "DaemonConnectFailed",
44
+ ) {
45
+ get message() {
46
+ return "Failed to connect to daemon";
47
+ }
48
+ }
@@ -15,6 +15,8 @@ export type { DaemonClient } from "./Client.ts";
15
15
 
16
16
  export {
17
17
  DaemonAlreadyRunning,
18
+ DaemonConnectFailed,
19
+ DaemonSocketNotReady,
18
20
  LockCompromised,
19
21
  ProcessAlreadyExists,
20
22
  ProcessNotFound,
package/src/Plan.ts CHANGED
@@ -342,20 +342,29 @@ export const make = <A>(
342
342
  ]),
343
343
  );
344
344
 
345
- // Combined prop + binding upstream, filtered to resources in this graph
346
- const allUpstreamDependencies: {
345
+ // Combined prop + binding upstream for the desired graph, including
346
+ // references to resources outside the current graph so delete validation can
347
+ // tell whether any surviving resource still points at an orphan.
348
+ const rawUpstreamDependencies: {
347
349
  [fqn: string]: string[];
348
350
  } = Object.fromEntries(
349
351
  resources.map((resource) => {
350
352
  const fqn = resource.FQN;
351
353
  const propDeps = newUpstreamDependencies[fqn] ?? [];
352
354
  const bindDeps = bindingUpstreamDependencies[fqn] ?? [];
353
- return [
354
- fqn,
355
- [...new Set([...propDeps, ...bindDeps])].filter((dep) =>
356
- newResourceFqns.has(dep),
357
- ),
358
- ];
355
+ return [fqn, [...new Set([...propDeps, ...bindDeps])]];
356
+ }),
357
+ );
358
+
359
+ // Combined prop + binding upstream, filtered to resources in this graph for
360
+ // scheduling and cycle detection.
361
+ const allUpstreamDependencies: {
362
+ [fqn: string]: string[];
363
+ } = Object.fromEntries(
364
+ resources.map((resource) => {
365
+ const fqn = resource.FQN;
366
+ const deps = rawUpstreamDependencies[fqn] ?? [];
367
+ return [fqn, deps.filter((dep) => newResourceFqns.has(dep))];
359
368
  }),
360
369
  );
361
370
 
@@ -755,11 +764,13 @@ export const make = <A>(
755
764
  )).filter((v) => !!v),
756
765
  );
757
766
 
758
- for (const [resourceFqn, deletion] of Object.entries(deletions)) {
759
- // downstream is stored as FQNs - check if any exist in resourceGraph
760
- const dependencies = deletion.state.downstream.filter(
761
- (d) => d in resourceGraph,
762
- );
767
+ for (const resourceFqn of Object.keys(deletions)) {
768
+ const dependencies = Object.entries(rawUpstreamDependencies)
769
+ .filter(
770
+ ([survivorFqn, upstream]) =>
771
+ survivorFqn in resourceGraph && upstream.includes(resourceFqn),
772
+ )
773
+ .map(([survivorFqn]) => survivorFqn);
763
774
  if (dependencies.length > 0) {
764
775
  return yield* new DeleteResourceHasDownstreamDependencies({
765
776
  message: `Resource ${resourceFqn} has downstream dependencies`,
package/src/Random.ts ADDED
@@ -0,0 +1,61 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Redacted from "effect/Redacted";
3
+ import { Resource } from "./Resource.ts";
4
+
5
+ export interface RandomProps {
6
+ /**
7
+ * Number of random bytes to generate before hex encoding.
8
+ * @default 32
9
+ */
10
+ bytes?: number;
11
+ }
12
+
13
+ export type Random = Resource<
14
+ "Alchemy.Random",
15
+ RandomProps,
16
+ {
17
+ text: Redacted.Redacted<string>;
18
+ }
19
+ >;
20
+
21
+ export const makeRandom = (id: string, props?: RandomProps) =>
22
+ Random(id, props).pipe(Effect.flatMap((rand) => rand.text.asEffect()));
23
+
24
+ /**
25
+ * A deterministic-in-state random secret generator.
26
+ *
27
+ * The value is generated once on create and then persisted in state so
28
+ * subsequent deploys keep the same secret unless the resource is replaced.
29
+ */
30
+ export const Random = Resource<Random>("Alchemy.Random");
31
+
32
+ export const RandomProvider = () =>
33
+ Random.provider.succeed({
34
+ create: Effect.fn(function* ({ news = {}, output }) {
35
+ if (output?.text) {
36
+ return output;
37
+ }
38
+
39
+ const byteLength = news.bytes ?? 32;
40
+ const text = yield* Effect.sync(() => {
41
+ const bytes = new Uint8Array(byteLength);
42
+ crypto.getRandomValues(bytes);
43
+ return Redacted.make(
44
+ Array.from(bytes)
45
+ .map((b) => b.toString(16).padStart(2, "0"))
46
+ .join(""),
47
+ );
48
+ });
49
+
50
+ return { text };
51
+ }),
52
+ update: Effect.fn(function* ({ output }) {
53
+ return output;
54
+ }),
55
+ delete: Effect.fn(function* () {
56
+ return undefined;
57
+ }),
58
+ read: Effect.fn(function* ({ output }) {
59
+ return output;
60
+ }),
61
+ });
package/src/Tags.ts CHANGED
@@ -31,7 +31,7 @@ export const createTagsList = (tags: Tags) =>
31
31
  Value,
32
32
  }));
33
33
 
34
- export const createInternalTags = Effect.fn(function* (id: string) {
34
+ export const createInternalTags = Effect.fnUntraced(function* (id: string) {
35
35
  const stack = yield* Stack;
36
36
  const stage = yield* Stage;
37
37
  return {
@@ -45,7 +45,9 @@ export const createInternalTags = Effect.fn(function* (id: string) {
45
45
  * Creates AWS-compatible tag filters for finding resources by alchemy tags.
46
46
  * Use with AWS describe APIs that accept Filter parameters.
47
47
  */
48
- export const createAlchemyTagFilters = Effect.fn(function* (id: string) {
48
+ export const createAlchemyTagFilters = Effect.fnUntraced(function* (
49
+ id: string,
50
+ ) {
49
51
  const stack = yield* Stack;
50
52
  const stage = yield* Stage;
51
53
  return [
@@ -58,7 +60,7 @@ export const createAlchemyTagFilters = Effect.fn(function* (id: string) {
58
60
  /**
59
61
  * Checks if a resource has the expected alchemy tags for this app/stage/id.
60
62
  */
61
- export const hasAlchemyTags = Effect.fn(function* (
63
+ export const hasAlchemyTags = Effect.fnUntraced(function* (
62
64
  id: string,
63
65
  tags: Tags | undefined,
64
66
  ) {
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ export * as Output from "./Output.ts";
17
17
  export * from "./PhysicalName.ts";
18
18
  export * as Plan from "./Plan.ts";
19
19
  export * from "./Provider.ts";
20
+ export * from "./Random.ts";
20
21
  export * from "./Ref.ts";
21
22
  export * as RemovalPolicy from "./RemovalPolicy.ts";
22
23
  export * from "./Resource.ts";