alchemy 0.16.0 → 0.16.1
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/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +6 -5
- package/lib/alchemy.js.map +1 -1
- package/lib/apply.d.ts.map +1 -1
- package/lib/apply.js +31 -31
- package/lib/apply.js.map +1 -1
- package/lib/cloudflare/queue.d.ts.map +1 -1
- package/lib/cloudflare/queue.js +3 -2
- package/lib/cloudflare/queue.js.map +1 -1
- package/lib/cloudflare/r2-rest-state-store.d.ts +1 -1
- package/lib/cloudflare/r2-rest-state-store.d.ts.map +1 -1
- package/lib/cloudflare/r2-rest-state-store.js +5 -4
- package/lib/cloudflare/r2-rest-state-store.js.map +1 -1
- package/lib/cloudflare/worker.js +1 -1
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/context.d.ts +1 -1
- package/lib/context.d.ts.map +1 -1
- package/lib/context.js +6 -5
- package/lib/context.js.map +1 -1
- package/lib/destroy.d.ts.map +1 -1
- package/lib/destroy.js +20 -20
- package/lib/destroy.js.map +1 -1
- package/lib/fs/file-system-state-store.d.ts +1 -1
- package/lib/fs/file-system-state-store.d.ts.map +1 -1
- package/lib/fs/file-system-state-store.js +5 -3
- package/lib/fs/file-system-state-store.js.map +1 -1
- package/lib/resource.d.ts +15 -11
- package/lib/resource.d.ts.map +1 -1
- package/lib/resource.js +12 -7
- package/lib/resource.js.map +1 -1
- package/lib/scope.d.ts +1 -1
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +2 -1
- package/lib/scope.js.map +1 -1
- package/lib/state.d.ts +3 -2
- package/lib/state.d.ts.map +1 -1
- package/lib/state.js +29 -0
- package/lib/state.js.map +1 -1
- package/package.json +1 -1
- package/src/alchemy.ts +13 -6
- package/src/apply.ts +37 -30
- package/src/cloudflare/queue.ts +6 -3
- package/src/cloudflare/r2-rest-state-store.ts +5 -5
- package/src/cloudflare/worker.ts +1 -1
- package/src/context.ts +21 -13
- package/src/destroy.ts +34 -20
- package/src/fs/file-system-state-store.ts +5 -7
- package/src/resource.ts +22 -18
- package/src/scope.ts +2 -2
- package/src/state.ts +42 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { ResourceScope } from "../resource.js";
|
|
1
2
|
import type { Scope } from "../scope.js";
|
|
2
|
-
import {
|
|
3
|
-
import type
|
|
3
|
+
import { serialize } from "../serde.js";
|
|
4
|
+
import { deserializeState, type State, type StateStore } from "../state.js";
|
|
4
5
|
import { withExponentialBackoff } from "../util/retry.js";
|
|
5
6
|
import { CloudflareApiError, handleApiError } from "./api-error.js";
|
|
6
7
|
import {
|
|
@@ -211,15 +212,14 @@ export class R2RestStateStore implements StateStore {
|
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
// Parse and deserialize the state data
|
|
214
|
-
const
|
|
215
|
-
const state = (await deserialize(this.scope, rawData)) as State;
|
|
215
|
+
const state = await deserializeState(this.scope, await response.text());
|
|
216
216
|
|
|
217
217
|
// Create a new state object with proper output
|
|
218
218
|
return {
|
|
219
219
|
...state,
|
|
220
220
|
output: {
|
|
221
221
|
...(state.output || {}),
|
|
222
|
-
|
|
222
|
+
[ResourceScope]: this.scope,
|
|
223
223
|
},
|
|
224
224
|
};
|
|
225
225
|
} catch (error: any) {
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -814,7 +814,7 @@ async function prepareWorkerMetadata<B extends Bindings>(
|
|
|
814
814
|
meta.bindings.push({
|
|
815
815
|
type: "service",
|
|
816
816
|
name: bindingName,
|
|
817
|
-
service: binding.
|
|
817
|
+
service: binding.name,
|
|
818
818
|
});
|
|
819
819
|
} else if (binding.type === "durable_object_namespace") {
|
|
820
820
|
meta.bindings.push({
|
package/src/context.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { DestroyedSignal } from "./destroy.js";
|
|
2
|
-
import
|
|
3
|
-
Resource,
|
|
2
|
+
import {
|
|
4
3
|
ResourceFQN,
|
|
5
4
|
ResourceID,
|
|
6
5
|
ResourceKind,
|
|
7
|
-
|
|
6
|
+
ResourceScope,
|
|
7
|
+
ResourceSeq,
|
|
8
|
+
type Resource,
|
|
9
|
+
type ResourceProps,
|
|
8
10
|
} from "./resource.js";
|
|
9
11
|
import type { Scope } from "./scope.js";
|
|
10
12
|
import type { State } from "./state.js";
|
|
@@ -96,23 +98,29 @@ export function context<
|
|
|
96
98
|
state: State<Kind, Props, Out>;
|
|
97
99
|
replace: () => void;
|
|
98
100
|
}): Context<Out> {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
type InternalSymbols =
|
|
102
|
+
| typeof ResourceID
|
|
103
|
+
| typeof ResourceKind
|
|
104
|
+
| typeof ResourceFQN
|
|
105
|
+
| typeof ResourceSeq
|
|
106
|
+
| typeof ResourceScope;
|
|
107
|
+
function create(props: Omit<Out, InternalSymbols>): Out;
|
|
108
|
+
function create(id: string, props: Omit<Out, InternalSymbols>): Out;
|
|
101
109
|
function create(
|
|
102
110
|
...args:
|
|
103
|
-
| [props: Omit<Out,
|
|
104
|
-
| [id: string, props: Omit<Out,
|
|
111
|
+
| [props: Omit<Out, InternalSymbols>]
|
|
112
|
+
| [id: string, props: Omit<Out, InternalSymbols>]
|
|
105
113
|
): Out {
|
|
106
114
|
const [ID, props] =
|
|
107
115
|
typeof args[0] === "string" ? (args as [string, any]) : [id, args[0]];
|
|
108
116
|
|
|
109
117
|
return {
|
|
110
118
|
...props,
|
|
111
|
-
|
|
112
|
-
ID,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
[ResourceKind]: kind,
|
|
120
|
+
[ResourceID]: ID,
|
|
121
|
+
[ResourceFQN]: fqn,
|
|
122
|
+
[ResourceScope]: scope,
|
|
123
|
+
[ResourceSeq]: seq,
|
|
116
124
|
} as Out;
|
|
117
125
|
}
|
|
118
126
|
return Object.assign(create, {
|
|
@@ -138,5 +146,5 @@ export function context<
|
|
|
138
146
|
throw new DestroyedSignal();
|
|
139
147
|
},
|
|
140
148
|
create,
|
|
141
|
-
}) as Context<Out>;
|
|
149
|
+
}) as unknown as Context<Out>;
|
|
142
150
|
}
|
package/src/destroy.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { alchemy } from "./alchemy.js";
|
|
2
2
|
import { context } from "./context.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
PROVIDERS,
|
|
5
|
+
ResourceFQN,
|
|
6
|
+
ResourceID,
|
|
7
|
+
ResourceKind,
|
|
8
|
+
ResourceScope,
|
|
9
|
+
ResourceSeq,
|
|
10
|
+
type Provider,
|
|
11
|
+
type Resource,
|
|
12
|
+
} from "./resource.js";
|
|
4
13
|
import { Scope } from "./scope.js";
|
|
5
14
|
|
|
6
15
|
export class DestroyedSignal extends Error {}
|
|
@@ -52,34 +61,36 @@ export async function destroy<Type extends string>(
|
|
|
52
61
|
return;
|
|
53
62
|
}
|
|
54
63
|
|
|
55
|
-
if (instance
|
|
64
|
+
if (instance[ResourceKind] === Scope.KIND) {
|
|
56
65
|
const scope = new Scope({
|
|
57
|
-
parent: instance
|
|
58
|
-
scopeName: instance
|
|
66
|
+
parent: instance[ResourceScope],
|
|
67
|
+
scopeName: instance[ResourceID],
|
|
59
68
|
});
|
|
60
69
|
console.log("Destroying scope", scope.chain.join("/"));
|
|
61
70
|
return await destroy(scope, options);
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
const Provider: Provider<Type> | undefined = PROVIDERS.get(
|
|
73
|
+
const Provider: Provider<Type> | undefined = PROVIDERS.get(
|
|
74
|
+
instance[ResourceKind],
|
|
75
|
+
);
|
|
65
76
|
if (!Provider) {
|
|
66
77
|
throw new Error(
|
|
67
|
-
`Cannot destroy resource "${instance
|
|
78
|
+
`Cannot destroy resource "${instance[ResourceFQN]}" type ${instance[ResourceKind]} - no provider found. You may need to import the provider in your alchemy.config.ts.`,
|
|
68
79
|
);
|
|
69
80
|
}
|
|
70
81
|
|
|
71
|
-
const scope = instance
|
|
82
|
+
const scope = instance[ResourceScope];
|
|
72
83
|
if (!scope) {
|
|
73
|
-
console.warn(`Resource "${instance
|
|
84
|
+
console.warn(`Resource "${instance[ResourceFQN]}" has no scope`);
|
|
74
85
|
}
|
|
75
86
|
const quiet = options?.quiet ?? scope.quiet;
|
|
76
87
|
|
|
77
88
|
try {
|
|
78
89
|
if (!quiet) {
|
|
79
|
-
console.log(`Delete: "${instance
|
|
90
|
+
console.log(`Delete: "${instance[ResourceFQN]}"`);
|
|
80
91
|
}
|
|
81
92
|
|
|
82
|
-
const state = await scope.state.get(instance
|
|
93
|
+
const state = await scope.state.get(instance[ResourceID]);
|
|
83
94
|
|
|
84
95
|
if (state === undefined) {
|
|
85
96
|
return;
|
|
@@ -88,10 +99,10 @@ export async function destroy<Type extends string>(
|
|
|
88
99
|
const ctx = context({
|
|
89
100
|
scope,
|
|
90
101
|
phase: "delete",
|
|
91
|
-
kind: instance
|
|
92
|
-
id: instance
|
|
93
|
-
fqn: instance
|
|
94
|
-
seq: instance
|
|
102
|
+
kind: instance[ResourceKind],
|
|
103
|
+
id: instance[ResourceID],
|
|
104
|
+
fqn: instance[ResourceFQN],
|
|
105
|
+
seq: instance[ResourceSeq],
|
|
95
106
|
props: state.props,
|
|
96
107
|
state,
|
|
97
108
|
replace: () => {
|
|
@@ -103,15 +114,18 @@ export async function destroy<Type extends string>(
|
|
|
103
114
|
try {
|
|
104
115
|
// BUG: this does not restore persisted scope
|
|
105
116
|
await alchemy.run(
|
|
106
|
-
instance
|
|
117
|
+
instance[ResourceID],
|
|
107
118
|
{
|
|
108
119
|
// TODO(sam): this is an awful hack to differentiate between naked scopes and resources
|
|
109
|
-
isResource: instance
|
|
120
|
+
isResource: instance[ResourceKind] !== "alchemy::Scope",
|
|
110
121
|
parent: scope,
|
|
111
122
|
},
|
|
112
123
|
async (scope) => {
|
|
113
124
|
nestedScope = scope;
|
|
114
|
-
return await Provider.handler.bind(ctx)(
|
|
125
|
+
return await Provider.handler.bind(ctx)(
|
|
126
|
+
instance[ResourceID],
|
|
127
|
+
state.props,
|
|
128
|
+
);
|
|
115
129
|
},
|
|
116
130
|
);
|
|
117
131
|
} catch (err) {
|
|
@@ -126,10 +140,10 @@ export async function destroy<Type extends string>(
|
|
|
126
140
|
await destroy(nestedScope, options);
|
|
127
141
|
}
|
|
128
142
|
|
|
129
|
-
await scope.delete(instance
|
|
143
|
+
await scope.delete(instance[ResourceID]);
|
|
130
144
|
|
|
131
145
|
if (!quiet) {
|
|
132
|
-
console.log(`Deleted: "${instance
|
|
146
|
+
console.log(`Deleted: "${instance[ResourceFQN]}"`);
|
|
133
147
|
}
|
|
134
148
|
} catch (error) {
|
|
135
149
|
console.error(error);
|
|
@@ -140,7 +154,7 @@ export async function destroy<Type extends string>(
|
|
|
140
154
|
export namespace destroy {
|
|
141
155
|
export async function all(resources: Resource[], options?: DestroyOptions) {
|
|
142
156
|
if (options?.strategy !== "parallel") {
|
|
143
|
-
const sorted = resources.sort((a, b) => b
|
|
157
|
+
const sorted = resources.sort((a, b) => b[ResourceSeq] - a[ResourceSeq]);
|
|
144
158
|
for (const resource of sorted) {
|
|
145
159
|
await destroy(resource, options);
|
|
146
160
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { ResourceScope } from "../resource.js";
|
|
3
4
|
import type { Scope } from "../scope.js";
|
|
4
|
-
import {
|
|
5
|
-
import type
|
|
5
|
+
import { serialize } from "../serde.js";
|
|
6
|
+
import { deserializeState, type State, type StateStore } from "../state.js";
|
|
6
7
|
import { ignore } from "../util/ignore.js";
|
|
7
8
|
|
|
8
9
|
const stateRootDir = path.join(process.cwd(), ".alchemy");
|
|
@@ -50,14 +51,11 @@ export class FileSystemStateStore implements StateStore {
|
|
|
50
51
|
async get(key: string): Promise<State | undefined> {
|
|
51
52
|
try {
|
|
52
53
|
const content = await fs.promises.readFile(this.getPath(key), "utf8");
|
|
53
|
-
const state =
|
|
54
|
-
this.scope,
|
|
55
|
-
JSON.parse(content),
|
|
56
|
-
)) as State;
|
|
54
|
+
const state = await deserializeState(this.scope, content);
|
|
57
55
|
if (state.output === undefined) {
|
|
58
56
|
state.output = {} as any;
|
|
59
57
|
}
|
|
60
|
-
state.output
|
|
58
|
+
state.output[ResourceScope] = this.scope;
|
|
61
59
|
return state;
|
|
62
60
|
} catch (error: any) {
|
|
63
61
|
if (error.code === "ENOENT") {
|
package/src/resource.ts
CHANGED
|
@@ -5,8 +5,13 @@ import { Scope as _Scope } from "./scope.js";
|
|
|
5
5
|
export const PROVIDERS = new Map<ResourceKind, Provider<string, any>>();
|
|
6
6
|
|
|
7
7
|
export type ResourceID = string;
|
|
8
|
+
export const ResourceID = Symbol.for("alchemy::ResourceID");
|
|
8
9
|
export type ResourceFQN = string;
|
|
10
|
+
export const ResourceFQN = Symbol.for("alchemy::ResourceFQN");
|
|
9
11
|
export type ResourceKind = string;
|
|
12
|
+
export const ResourceKind = Symbol.for("alchemy::ResourceKind");
|
|
13
|
+
export const ResourceScope = Symbol.for("alchemy::ResourceScope");
|
|
14
|
+
export const ResourceSeq = Symbol.for("alchemy::ResourceSeq");
|
|
10
15
|
|
|
11
16
|
export interface ProviderOptions {
|
|
12
17
|
/**
|
|
@@ -37,12 +42,11 @@ export type PendingResource<
|
|
|
37
42
|
Scope extends _Scope = _Scope,
|
|
38
43
|
Seq extends number = number,
|
|
39
44
|
> = Promise<Out> & {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
signal: () => void;
|
|
45
|
+
[ResourceKind]: Kind;
|
|
46
|
+
[ResourceID]: ID;
|
|
47
|
+
[ResourceFQN]: FQN;
|
|
48
|
+
[ResourceScope]: Scope;
|
|
49
|
+
[ResourceSeq]: Seq;
|
|
46
50
|
};
|
|
47
51
|
|
|
48
52
|
export interface Resource<
|
|
@@ -54,11 +58,11 @@ export interface Resource<
|
|
|
54
58
|
Seq extends number = number,
|
|
55
59
|
> {
|
|
56
60
|
// use capital letters to avoid collision with conventional camelCase typescript properties
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
[ResourceKind]: Kind;
|
|
62
|
+
[ResourceID]: ID;
|
|
63
|
+
[ResourceFQN]: FQN;
|
|
64
|
+
[ResourceScope]: Scope;
|
|
65
|
+
[ResourceSeq]: Seq;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
// helper for semantic syntax highlighting (color as a type/class instead of function/value)
|
|
@@ -113,10 +117,10 @@ export function Resource<
|
|
|
113
117
|
// TODO(sam): do we want to throw?
|
|
114
118
|
// it's kind of awesome that you can re-create a resource and call apply
|
|
115
119
|
const otherResource = scope.resources.get(resourceID);
|
|
116
|
-
if (otherResource?.
|
|
120
|
+
if (otherResource?.[ResourceKind] !== type) {
|
|
117
121
|
scope.fail();
|
|
118
122
|
throw new Error(
|
|
119
|
-
`Resource ${resourceID} already exists in the stack and is of a different type: '${otherResource?.
|
|
123
|
+
`Resource ${resourceID} already exists in the stack and is of a different type: '${otherResource?.[ResourceKind]}' !== '${type}'`,
|
|
120
124
|
);
|
|
121
125
|
}
|
|
122
126
|
// console.warn(
|
|
@@ -127,11 +131,11 @@ export function Resource<
|
|
|
127
131
|
// get a sequence number (unique within the scope) for the resource
|
|
128
132
|
const seq = scope.seq();
|
|
129
133
|
const meta = {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
[ResourceKind]: type,
|
|
135
|
+
[ResourceID]: resourceID,
|
|
136
|
+
[ResourceFQN]: scope.fqn(resourceID),
|
|
137
|
+
[ResourceSeq]: seq,
|
|
138
|
+
[ResourceScope]: scope,
|
|
135
139
|
} as any as PendingResource<Out>;
|
|
136
140
|
const promise = apply(meta, props, options);
|
|
137
141
|
const resource = Object.assign(promise, meta);
|
package/src/scope.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import type { Phase } from "./alchemy.js";
|
|
3
3
|
import { destroy } from "./destroy.js";
|
|
4
4
|
import { FileSystemStateStore } from "./fs/file-system-state-store.js";
|
|
5
|
-
import
|
|
5
|
+
import { ResourceID, type PendingResource } from "./resource.js";
|
|
6
6
|
import type { StateStore, StateStoreType } from "./state.js";
|
|
7
7
|
|
|
8
8
|
const scopeStorage = new AsyncLocalStorage<Scope>();
|
|
@@ -157,7 +157,7 @@ export class Scope {
|
|
|
157
157
|
return `Scope(
|
|
158
158
|
chain=${this.chain.join("/")},
|
|
159
159
|
resources=[${Array.from(this.resources.values())
|
|
160
|
-
.map((r) => r
|
|
160
|
+
.map((r) => r[ResourceID])
|
|
161
161
|
.join(",\n ")}]
|
|
162
162
|
)`;
|
|
163
163
|
}
|
package/src/state.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
ResourceFQN,
|
|
3
|
+
ResourceID,
|
|
4
|
+
ResourceKind,
|
|
5
|
+
ResourceScope,
|
|
6
|
+
ResourceSeq,
|
|
7
|
+
type Resource,
|
|
8
|
+
type ResourceProps,
|
|
9
|
+
} from "./resource.js";
|
|
10
|
+
import { Scope } from "./scope.js";
|
|
11
|
+
import { deserialize } from "./serde.js";
|
|
3
12
|
|
|
4
13
|
export type State<
|
|
5
14
|
Kind extends string = string,
|
|
@@ -41,3 +50,34 @@ export interface StateStore {
|
|
|
41
50
|
set(key: string, value: State): Promise<void>;
|
|
42
51
|
delete(key: string): Promise<void>;
|
|
43
52
|
}
|
|
53
|
+
|
|
54
|
+
export async function deserializeState(
|
|
55
|
+
scope: Scope,
|
|
56
|
+
content: string,
|
|
57
|
+
): Promise<State> {
|
|
58
|
+
const state = (await deserialize(scope, JSON.parse(content))) as State;
|
|
59
|
+
|
|
60
|
+
if (ResourceID in state.output) {
|
|
61
|
+
// this is a new state
|
|
62
|
+
return state;
|
|
63
|
+
}
|
|
64
|
+
const output: any = state.output;
|
|
65
|
+
delete output.Kind;
|
|
66
|
+
delete output.ID;
|
|
67
|
+
delete output.FQN;
|
|
68
|
+
delete output.Scope;
|
|
69
|
+
delete output.Seq;
|
|
70
|
+
// fix this bug
|
|
71
|
+
if (state.kind === "scope") {
|
|
72
|
+
state.kind = Scope.KIND;
|
|
73
|
+
}
|
|
74
|
+
output[ResourceKind] = state.kind;
|
|
75
|
+
output[ResourceID] = state.id;
|
|
76
|
+
output[ResourceFQN] = state.fqn;
|
|
77
|
+
output[ResourceScope] = scope;
|
|
78
|
+
output[ResourceSeq] = state.seq;
|
|
79
|
+
state.output = output;
|
|
80
|
+
// re-write the state with the migrated format
|
|
81
|
+
await scope.state.set(state.id, state);
|
|
82
|
+
return state;
|
|
83
|
+
}
|