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.
Files changed (50) hide show
  1. package/lib/alchemy.d.ts.map +1 -1
  2. package/lib/alchemy.js +6 -5
  3. package/lib/alchemy.js.map +1 -1
  4. package/lib/apply.d.ts.map +1 -1
  5. package/lib/apply.js +31 -31
  6. package/lib/apply.js.map +1 -1
  7. package/lib/cloudflare/queue.d.ts.map +1 -1
  8. package/lib/cloudflare/queue.js +3 -2
  9. package/lib/cloudflare/queue.js.map +1 -1
  10. package/lib/cloudflare/r2-rest-state-store.d.ts +1 -1
  11. package/lib/cloudflare/r2-rest-state-store.d.ts.map +1 -1
  12. package/lib/cloudflare/r2-rest-state-store.js +5 -4
  13. package/lib/cloudflare/r2-rest-state-store.js.map +1 -1
  14. package/lib/cloudflare/worker.js +1 -1
  15. package/lib/cloudflare/worker.js.map +1 -1
  16. package/lib/context.d.ts +1 -1
  17. package/lib/context.d.ts.map +1 -1
  18. package/lib/context.js +6 -5
  19. package/lib/context.js.map +1 -1
  20. package/lib/destroy.d.ts.map +1 -1
  21. package/lib/destroy.js +20 -20
  22. package/lib/destroy.js.map +1 -1
  23. package/lib/fs/file-system-state-store.d.ts +1 -1
  24. package/lib/fs/file-system-state-store.d.ts.map +1 -1
  25. package/lib/fs/file-system-state-store.js +5 -3
  26. package/lib/fs/file-system-state-store.js.map +1 -1
  27. package/lib/resource.d.ts +15 -11
  28. package/lib/resource.d.ts.map +1 -1
  29. package/lib/resource.js +12 -7
  30. package/lib/resource.js.map +1 -1
  31. package/lib/scope.d.ts +1 -1
  32. package/lib/scope.d.ts.map +1 -1
  33. package/lib/scope.js +2 -1
  34. package/lib/scope.js.map +1 -1
  35. package/lib/state.d.ts +3 -2
  36. package/lib/state.d.ts.map +1 -1
  37. package/lib/state.js +29 -0
  38. package/lib/state.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/alchemy.ts +13 -6
  41. package/src/apply.ts +37 -30
  42. package/src/cloudflare/queue.ts +6 -3
  43. package/src/cloudflare/r2-rest-state-store.ts +5 -5
  44. package/src/cloudflare/worker.ts +1 -1
  45. package/src/context.ts +21 -13
  46. package/src/destroy.ts +34 -20
  47. package/src/fs/file-system-state-store.ts +5 -7
  48. package/src/resource.ts +22 -18
  49. package/src/scope.ts +2 -2
  50. 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 { deserialize, serialize } from "../serde.js";
3
- import type { State, StateStore } from "../state.js";
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 rawData = await response.json();
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
- Scope: this.scope,
222
+ [ResourceScope]: this.scope,
223
223
  },
224
224
  };
225
225
  } catch (error: any) {
@@ -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.id,
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 type {
3
- Resource,
2
+ import {
4
3
  ResourceFQN,
5
4
  ResourceID,
6
5
  ResourceKind,
7
- ResourceProps,
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
- function create(props: Omit<Out, "Kind" | "ID" | "Scope">): Out;
100
- function create(id: string, props: Omit<Out, "Kind" | "ID" | "Scope">): Out;
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, "Kind" | "ID" | "Scope">]
104
- | [id: string, props: Omit<Out, "Kind" | "ID" | "Scope">]
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
- Kind: kind,
112
- ID,
113
- FQN: fqn,
114
- Scope: scope,
115
- Seq: seq,
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 { PROVIDERS, type Provider, type Resource } from "./resource.js";
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.Kind === Scope.KIND) {
64
+ if (instance[ResourceKind] === Scope.KIND) {
56
65
  const scope = new Scope({
57
- parent: instance.Scope,
58
- scopeName: instance.ID,
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(instance.Kind);
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.FQN}" type ${instance.Kind} - no provider found. You may need to import the provider in your alchemy.config.ts.`,
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.Scope;
82
+ const scope = instance[ResourceScope];
72
83
  if (!scope) {
73
- console.warn(`Resource "${instance.FQN}" has no scope`);
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.FQN}"`);
90
+ console.log(`Delete: "${instance[ResourceFQN]}"`);
80
91
  }
81
92
 
82
- const state = await scope.state.get(instance.ID);
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.Kind,
92
- id: instance.ID,
93
- fqn: instance.FQN,
94
- seq: instance.Seq,
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.ID,
117
+ instance[ResourceID],
107
118
  {
108
119
  // TODO(sam): this is an awful hack to differentiate between naked scopes and resources
109
- isResource: instance.Kind !== "alchemy::Scope",
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)(instance.ID, state.props);
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.ID);
143
+ await scope.delete(instance[ResourceID]);
130
144
 
131
145
  if (!quiet) {
132
- console.log(`Deleted: "${instance.FQN}"`);
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.Seq - a.Seq);
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 { deserialize, serialize } from "../serde.js";
5
- import type { State, StateStore } from "../state.js";
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 = (await deserialize(
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.Scope = this.scope;
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
- Kind: Kind;
41
- ID: ID;
42
- FQN: FQN;
43
- Seq: Seq;
44
- Scope: Scope;
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
- Kind: Kind;
58
- ID: ID;
59
- FQN: FQN;
60
- Scope: Scope;
61
- Seq: Seq;
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?.Kind !== type) {
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?.Kind}' !== '${type}'`,
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
- Kind: type,
131
- ID: resourceID,
132
- FQN: scope.fqn(resourceID),
133
- Seq: seq,
134
- Scope: scope,
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 type { PendingResource, ResourceID } from "./resource.js";
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.ID)
160
+ .map((r) => r[ResourceID])
161
161
  .join(",\n ")}]
162
162
  )`;
163
163
  }
package/src/state.ts CHANGED
@@ -1,5 +1,14 @@
1
- import type { Resource, ResourceProps } from "./resource.js";
2
- import type { Scope } from "./scope.js";
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
+ }