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.
- package/lib/app.d.ts +23 -0
- package/lib/app.d.ts.map +1 -0
- package/lib/app.js +26 -0
- package/lib/app.js.map +1 -0
- package/lib/apply.d.ts +22 -0
- package/lib/apply.d.ts.map +1 -0
- package/lib/apply.js +191 -0
- package/lib/apply.js.map +1 -0
- package/lib/approve.d.ts +15 -0
- package/lib/approve.d.ts.map +1 -0
- package/lib/approve.js +7 -0
- package/lib/approve.js.map +1 -0
- package/lib/binding.d.ts +58 -0
- package/lib/binding.d.ts.map +1 -0
- package/lib/binding.js +27 -0
- package/lib/binding.js.map +1 -0
- package/lib/capability.d.ts +11 -0
- package/lib/capability.d.ts.map +1 -0
- package/lib/capability.js +1 -0
- package/lib/capability.js.map +1 -0
- package/lib/destroy.d.ts +4 -0
- package/lib/destroy.d.ts.map +1 -0
- package/lib/destroy.js +1 -0
- package/lib/destroy.js.map +1 -0
- package/lib/dot-alchemy.d.ts +9 -0
- package/lib/dot-alchemy.d.ts.map +1 -0
- package/lib/dot-alchemy.js +14 -0
- package/lib/dot-alchemy.js.map +1 -0
- package/lib/env.d.ts +4 -0
- package/lib/env.d.ts.map +1 -0
- package/lib/env.js +33 -0
- package/lib/env.js.map +1 -0
- package/lib/event.d.ts +16 -0
- package/lib/event.d.ts.map +1 -0
- package/lib/event.js +1 -0
- package/lib/event.js.map +1 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +18 -0
- package/lib/index.js.map +1 -0
- package/lib/phase.d.ts +2 -0
- package/lib/phase.d.ts.map +1 -0
- package/lib/phase.js +1 -0
- package/lib/phase.js.map +1 -0
- package/lib/physical-name.d.ts +4 -0
- package/lib/physical-name.d.ts.map +1 -0
- package/lib/physical-name.js +7 -0
- package/lib/physical-name.js.map +1 -0
- package/lib/plan.d.ts +98 -0
- package/lib/plan.d.ts.map +1 -0
- package/lib/plan.js +228 -0
- package/lib/plan.js.map +1 -0
- package/lib/policy.d.ts +37 -0
- package/lib/policy.d.ts.map +1 -0
- package/lib/policy.js +16 -0
- package/lib/policy.js.map +1 -0
- package/lib/provider.d.ts +57 -0
- package/lib/provider.d.ts.map +1 -0
- package/lib/provider.js +2 -0
- package/lib/provider.js.map +1 -0
- package/lib/reporter.d.ts +8 -0
- package/lib/reporter.d.ts.map +1 -0
- package/lib/reporter.js +4 -0
- package/lib/reporter.js.map +1 -0
- package/lib/resource.d.ts +38 -0
- package/lib/resource.d.ts.map +1 -0
- package/lib/resource.js +25 -0
- package/lib/resource.js.map +1 -0
- package/lib/runtime.d.ts +39 -0
- package/lib/runtime.d.ts.map +1 -0
- package/lib/runtime.js +54 -0
- package/lib/runtime.js.map +1 -0
- package/lib/service.d.ts +33 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +4 -0
- package/lib/service.js.map +1 -0
- package/lib/state.d.ts +38 -0
- package/lib/state.d.ts.map +1 -0
- package/lib/state.js +66 -0
- package/lib/state.js.map +1 -0
- package/package.json +49 -0
- package/src/app.ts +43 -0
- package/src/apply.ts +318 -0
- package/src/approve.ts +13 -0
- package/src/binding.ts +144 -0
- package/src/capability.ts +19 -0
- package/src/destroy.ts +4 -0
- package/src/dot-alchemy.ts +17 -0
- package/src/env.ts +51 -0
- package/src/event.ts +27 -0
- package/src/index.ts +17 -0
- package/src/phase.ts +1 -0
- package/src/physical-name.ts +7 -0
- package/src/plan.ts +470 -0
- package/src/policy.ts +77 -0
- package/src/provider.ts +74 -0
- package/src/reporter.ts +8 -0
- package/src/resource.ts +68 -0
- package/src/runtime.ts +145 -0
- package/src/service.ts +55 -0
- 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
|
+
};
|