alchemy 0.44.4 → 0.45.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 (258) hide show
  1. package/lib/alchemy.d.ts +3 -3
  2. package/lib/alchemy.d.ts.map +1 -1
  3. package/lib/alchemy.js +5 -2
  4. package/lib/alchemy.js.map +1 -1
  5. package/lib/build-date.d.ts +1 -1
  6. package/lib/build-date.js +1 -1
  7. package/lib/cloudflare/auth.js +1 -1
  8. package/lib/cloudflare/auth.js.map +1 -1
  9. package/lib/cloudflare/bundle/esbuild.d.ts +16 -0
  10. package/lib/cloudflare/bundle/esbuild.d.ts.map +1 -0
  11. package/lib/cloudflare/bundle/esbuild.js +159 -0
  12. package/lib/cloudflare/bundle/esbuild.js.map +1 -0
  13. package/lib/cloudflare/bundle/fs.d.ts +18 -0
  14. package/lib/cloudflare/bundle/fs.d.ts.map +1 -0
  15. package/lib/cloudflare/bundle/fs.js +75 -0
  16. package/lib/cloudflare/bundle/fs.js.map +1 -0
  17. package/lib/cloudflare/bundle/index.d.ts +3 -0
  18. package/lib/cloudflare/bundle/index.d.ts.map +1 -0
  19. package/lib/cloudflare/bundle/index.js +2 -0
  20. package/lib/cloudflare/bundle/index.js.map +1 -0
  21. package/lib/cloudflare/bundle/inline.d.ts +12 -0
  22. package/lib/cloudflare/bundle/inline.d.ts.map +1 -0
  23. package/lib/cloudflare/bundle/inline.js +32 -0
  24. package/lib/cloudflare/bundle/inline.js.map +1 -0
  25. package/lib/cloudflare/bundle/normalize.d.ts +21 -0
  26. package/lib/cloudflare/bundle/normalize.d.ts.map +1 -0
  27. package/lib/cloudflare/bundle/normalize.js +49 -0
  28. package/lib/cloudflare/bundle/normalize.js.map +1 -0
  29. package/lib/cloudflare/bundle/{nodejs-import-warning-plugin.d.ts → plugin-compat-warning.d.ts} +2 -2
  30. package/lib/cloudflare/bundle/plugin-compat-warning.d.ts.map +1 -0
  31. package/lib/cloudflare/bundle/{nodejs-import-warning-plugin.js → plugin-compat-warning.js} +3 -3
  32. package/lib/cloudflare/bundle/plugin-compat-warning.js.map +1 -0
  33. package/lib/cloudflare/bundle/plugin-hot-reload.d.ts +8 -0
  34. package/lib/cloudflare/bundle/plugin-hot-reload.d.ts.map +1 -0
  35. package/lib/cloudflare/bundle/plugin-hot-reload.js +19 -0
  36. package/lib/cloudflare/bundle/plugin-hot-reload.js.map +1 -0
  37. package/lib/cloudflare/bundle/{nodejs-compat.d.ts → plugin-hybrid-node-compat.d.ts} +2 -2
  38. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.d.ts.map +1 -0
  39. package/lib/cloudflare/bundle/{nodejs-compat.js → plugin-hybrid-node-compat.js} +10 -10
  40. package/lib/cloudflare/bundle/plugin-hybrid-node-compat.js.map +1 -0
  41. package/lib/cloudflare/bundle/plugin-wasm.d.ts +3 -0
  42. package/lib/cloudflare/bundle/plugin-wasm.d.ts.map +1 -0
  43. package/lib/cloudflare/bundle/plugin-wasm.js +55 -0
  44. package/lib/cloudflare/bundle/plugin-wasm.js.map +1 -0
  45. package/lib/cloudflare/bundle/shared.d.ts +30 -0
  46. package/lib/cloudflare/bundle/shared.d.ts.map +1 -0
  47. package/lib/cloudflare/bundle/shared.js +34 -0
  48. package/lib/cloudflare/bundle/shared.js.map +1 -0
  49. package/lib/cloudflare/bundle/{nodejs-compat-mode.d.ts → validate-node-compat.d.ts} +8 -4
  50. package/lib/cloudflare/bundle/validate-node-compat.d.ts.map +1 -0
  51. package/lib/cloudflare/bundle/{nodejs-compat-mode.js → validate-node-compat.js} +27 -7
  52. package/lib/cloudflare/bundle/validate-node-compat.js.map +1 -0
  53. package/lib/cloudflare/container.d.ts +231 -0
  54. package/lib/cloudflare/container.d.ts.map +1 -1
  55. package/lib/cloudflare/container.js +7 -0
  56. package/lib/cloudflare/container.js.map +1 -1
  57. package/lib/cloudflare/d1-migrations.d.ts +1 -0
  58. package/lib/cloudflare/d1-migrations.d.ts.map +1 -1
  59. package/lib/cloudflare/d1-migrations.js +18 -6
  60. package/lib/cloudflare/d1-migrations.js.map +1 -1
  61. package/lib/cloudflare/d1-state-store.d.ts +10 -0
  62. package/lib/cloudflare/d1-state-store.d.ts.map +1 -0
  63. package/lib/cloudflare/d1-state-store.js +70 -0
  64. package/lib/cloudflare/d1-state-store.js.map +1 -0
  65. package/lib/cloudflare/do-state-store/internal.js +1 -1
  66. package/lib/cloudflare/do-state-store/internal.js.map +1 -1
  67. package/lib/cloudflare/do-state-store/store.js +1 -1
  68. package/lib/cloudflare/do-state-store/store.js.map +1 -1
  69. package/lib/cloudflare/index.d.ts +3 -2
  70. package/lib/cloudflare/index.d.ts.map +1 -1
  71. package/lib/cloudflare/index.js +3 -2
  72. package/lib/cloudflare/index.js.map +1 -1
  73. package/lib/cloudflare/local-dev-cloudflare-shim.d.ts.map +1 -0
  74. package/lib/cloudflare/{bundle/local-dev-cloudflare-shim.js → local-dev-cloudflare-shim.js} +1 -1
  75. package/lib/cloudflare/local-dev-cloudflare-shim.js.map +1 -0
  76. package/lib/cloudflare/miniflare/http-server.d.ts.map +1 -0
  77. package/lib/cloudflare/miniflare/http-server.js.map +1 -0
  78. package/lib/cloudflare/{worker → miniflare}/miniflare-worker-options.d.ts +4 -3
  79. package/lib/cloudflare/miniflare/miniflare-worker-options.d.ts.map +1 -0
  80. package/lib/cloudflare/{worker → miniflare}/miniflare-worker-options.js +6 -3
  81. package/lib/cloudflare/miniflare/miniflare-worker-options.js.map +1 -0
  82. package/lib/cloudflare/miniflare/miniflare.d.ts.map +1 -0
  83. package/lib/cloudflare/{worker → miniflare}/miniflare.js +1 -1
  84. package/lib/cloudflare/miniflare/miniflare.js.map +1 -0
  85. package/lib/cloudflare/miniflare/mixed-mode.d.ts.map +1 -0
  86. package/lib/cloudflare/{worker → miniflare}/mixed-mode.js +1 -2
  87. package/lib/cloudflare/miniflare/mixed-mode.js.map +1 -0
  88. package/lib/cloudflare/user.js +1 -1
  89. package/lib/cloudflare/user.js.map +1 -1
  90. package/lib/cloudflare/worker/shared.d.ts +4 -0
  91. package/lib/cloudflare/worker/shared.d.ts.map +1 -0
  92. package/lib/cloudflare/worker/{get-worker-template.js → shared.js} +10 -1
  93. package/lib/cloudflare/worker/shared.js.map +1 -0
  94. package/lib/cloudflare/worker/tail.d.ts +18 -0
  95. package/lib/cloudflare/worker/tail.d.ts.map +1 -0
  96. package/lib/cloudflare/worker/tail.js +140 -0
  97. package/lib/cloudflare/worker/tail.js.map +1 -0
  98. package/lib/cloudflare/worker-metadata.d.ts +6 -2
  99. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  100. package/lib/cloudflare/worker-metadata.js +7 -23
  101. package/lib/cloudflare/worker-metadata.js.map +1 -1
  102. package/lib/cloudflare/worker-stub.d.ts.map +1 -1
  103. package/lib/cloudflare/worker-stub.js +8 -7
  104. package/lib/cloudflare/worker-stub.js.map +1 -1
  105. package/lib/cloudflare/worker-subdomain.d.ts +31 -0
  106. package/lib/cloudflare/worker-subdomain.d.ts.map +1 -0
  107. package/lib/cloudflare/worker-subdomain.js +55 -0
  108. package/lib/cloudflare/worker-subdomain.js.map +1 -0
  109. package/lib/cloudflare/worker.d.ts +30 -36
  110. package/lib/cloudflare/worker.d.ts.map +1 -1
  111. package/lib/cloudflare/worker.js +641 -569
  112. package/lib/cloudflare/worker.js.map +1 -1
  113. package/lib/fs/file-system-state-store.js +1 -1
  114. package/lib/fs/file-system-state-store.js.map +1 -1
  115. package/lib/scope.d.ts +2 -2
  116. package/lib/scope.d.ts.map +1 -1
  117. package/lib/scope.js +23 -8
  118. package/lib/scope.js.map +1 -1
  119. package/lib/serde.d.ts +1 -1
  120. package/lib/serde.d.ts.map +1 -1
  121. package/lib/serde.js +4 -4
  122. package/lib/serde.js.map +1 -1
  123. package/lib/sqlite/base-sqlite-state-store.d.ts +31 -0
  124. package/lib/sqlite/base-sqlite-state-store.d.ts.map +1 -0
  125. package/lib/sqlite/base-sqlite-state-store.js +124 -0
  126. package/lib/sqlite/base-sqlite-state-store.js.map +1 -0
  127. package/lib/sqlite/index.d.ts +2 -0
  128. package/lib/sqlite/index.d.ts.map +1 -0
  129. package/lib/sqlite/index.js +2 -0
  130. package/lib/sqlite/index.js.map +1 -0
  131. package/lib/sqlite/schema.d.ts +198 -0
  132. package/lib/sqlite/schema.d.ts.map +1 -0
  133. package/lib/sqlite/schema.js +23 -0
  134. package/lib/sqlite/schema.js.map +1 -0
  135. package/lib/sqlite/sqlite-state-store.d.ts +60 -0
  136. package/lib/sqlite/sqlite-state-store.d.ts.map +1 -0
  137. package/lib/sqlite/sqlite-state-store.js +83 -0
  138. package/lib/sqlite/sqlite-state-store.js.map +1 -0
  139. package/lib/test/bun.d.ts.map +1 -1
  140. package/lib/test/bun.js +0 -6
  141. package/lib/test/bun.js.map +1 -1
  142. package/lib/test/vitest.d.ts.map +1 -1
  143. package/lib/test/vitest.js +19 -5
  144. package/lib/test/vitest.js.map +1 -1
  145. package/lib/util/memoize.d.ts +1 -1
  146. package/lib/util/memoize.d.ts.map +1 -1
  147. package/lib/util/memoize.js +11 -1
  148. package/lib/util/memoize.js.map +1 -1
  149. package/package.json +12 -3
  150. package/src/alchemy.ts +8 -6
  151. package/src/build-date.ts +1 -1
  152. package/src/cloudflare/auth.ts +1 -1
  153. package/src/cloudflare/bundle/esbuild.ts +194 -0
  154. package/src/cloudflare/bundle/fs.ts +98 -0
  155. package/src/cloudflare/bundle/index.ts +6 -0
  156. package/src/cloudflare/bundle/inline.ts +42 -0
  157. package/src/cloudflare/bundle/normalize.ts +76 -0
  158. package/src/cloudflare/bundle/{nodejs-import-warning-plugin.ts → plugin-compat-warning.ts} +4 -2
  159. package/src/cloudflare/bundle/plugin-hot-reload.ts +27 -0
  160. package/src/cloudflare/bundle/{nodejs-compat.ts → plugin-hybrid-node-compat.ts} +10 -10
  161. package/src/cloudflare/bundle/plugin-wasm.ts +62 -0
  162. package/src/cloudflare/bundle/shared.ts +73 -0
  163. package/src/cloudflare/bundle/{nodejs-compat-mode.ts → validate-node-compat.ts} +41 -16
  164. package/src/cloudflare/container.ts +296 -0
  165. package/src/cloudflare/d1-migrations.ts +29 -16
  166. package/src/cloudflare/d1-state-store.ts +105 -0
  167. package/src/cloudflare/do-state-store/internal.ts +1 -1
  168. package/src/cloudflare/do-state-store/store.ts +1 -1
  169. package/src/cloudflare/index.ts +3 -2
  170. package/src/cloudflare/{bundle/local-dev-cloudflare-shim.ts → local-dev-cloudflare-shim.ts} +1 -1
  171. package/src/cloudflare/{worker → miniflare}/miniflare-worker-options.ts +14 -6
  172. package/src/cloudflare/{worker → miniflare}/miniflare.ts +1 -1
  173. package/src/cloudflare/{worker → miniflare}/mixed-mode.ts +1 -2
  174. package/src/cloudflare/user.ts +1 -1
  175. package/src/cloudflare/worker/{subdomain.ts → shared.ts} +14 -0
  176. package/src/cloudflare/worker/tail.ts +287 -0
  177. package/src/cloudflare/worker-metadata.ts +18 -26
  178. package/src/cloudflare/worker-stub.ts +9 -7
  179. package/src/cloudflare/worker-subdomain.ts +123 -0
  180. package/src/cloudflare/worker.ts +863 -760
  181. package/src/fs/file-system-state-store.ts +1 -1
  182. package/src/scope.ts +30 -11
  183. package/src/serde.ts +4 -4
  184. package/src/sqlite/base-sqlite-state-store.ts +199 -0
  185. package/src/sqlite/index.ts +1 -0
  186. package/src/sqlite/schema.ts +37 -0
  187. package/src/sqlite/sqlite-state-store.ts +174 -0
  188. package/src/test/bun.ts +0 -8
  189. package/src/test/vitest.ts +19 -8
  190. package/src/util/memoize.ts +13 -1
  191. package/templates/astro/types/env.d.ts +1 -1
  192. package/templates/nuxt/types/env.d.ts +1 -1
  193. package/templates/react-router/types/env.d.ts +1 -1
  194. package/templates/rwsdk/types/env.d.ts +1 -1
  195. package/templates/sveltekit/types/env.d.ts +1 -1
  196. package/templates/tanstack-start/types/env.d.ts +1 -1
  197. package/templates/typescript/types/env.d.ts +1 -1
  198. package/templates/vite/types/env.d.ts +1 -1
  199. package/lib/cloudflare/bundle/alias-plugin.d.ts +0 -6
  200. package/lib/cloudflare/bundle/alias-plugin.d.ts.map +0 -1
  201. package/lib/cloudflare/bundle/alias-plugin.js +0 -36
  202. package/lib/cloudflare/bundle/alias-plugin.js.map +0 -1
  203. package/lib/cloudflare/bundle/build-failures.d.ts +0 -12
  204. package/lib/cloudflare/bundle/build-failures.d.ts.map +0 -1
  205. package/lib/cloudflare/bundle/build-failures.js +0 -45
  206. package/lib/cloudflare/bundle/build-failures.js.map +0 -1
  207. package/lib/cloudflare/bundle/bundle-worker-dev.d.ts +0 -26
  208. package/lib/cloudflare/bundle/bundle-worker-dev.d.ts.map +0 -1
  209. package/lib/cloudflare/bundle/bundle-worker-dev.js +0 -88
  210. package/lib/cloudflare/bundle/bundle-worker-dev.js.map +0 -1
  211. package/lib/cloudflare/bundle/bundle-worker.d.ts +0 -13
  212. package/lib/cloudflare/bundle/bundle-worker.d.ts.map +0 -1
  213. package/lib/cloudflare/bundle/bundle-worker.js +0 -98
  214. package/lib/cloudflare/bundle/bundle-worker.js.map +0 -1
  215. package/lib/cloudflare/bundle/external.d.ts +0 -4
  216. package/lib/cloudflare/bundle/external.d.ts.map +0 -1
  217. package/lib/cloudflare/bundle/external.js +0 -40
  218. package/lib/cloudflare/bundle/external.js.map +0 -1
  219. package/lib/cloudflare/bundle/local-dev-cloudflare-shim.d.ts.map +0 -1
  220. package/lib/cloudflare/bundle/local-dev-cloudflare-shim.js.map +0 -1
  221. package/lib/cloudflare/bundle/nodejs-compat-mode.d.ts.map +0 -1
  222. package/lib/cloudflare/bundle/nodejs-compat-mode.js.map +0 -1
  223. package/lib/cloudflare/bundle/nodejs-compat.d.ts.map +0 -1
  224. package/lib/cloudflare/bundle/nodejs-compat.js.map +0 -1
  225. package/lib/cloudflare/bundle/nodejs-import-warning-plugin.d.ts.map +0 -1
  226. package/lib/cloudflare/bundle/nodejs-import-warning-plugin.js.map +0 -1
  227. package/lib/cloudflare/bundle/wasm-plugin.d.ts +0 -3
  228. package/lib/cloudflare/bundle/wasm-plugin.d.ts.map +0 -1
  229. package/lib/cloudflare/bundle/wasm-plugin.js +0 -53
  230. package/lib/cloudflare/bundle/wasm-plugin.js.map +0 -1
  231. package/lib/cloudflare/worker/get-worker-template.d.ts +0 -2
  232. package/lib/cloudflare/worker/get-worker-template.d.ts.map +0 -1
  233. package/lib/cloudflare/worker/get-worker-template.js.map +0 -1
  234. package/lib/cloudflare/worker/http-server.d.ts.map +0 -1
  235. package/lib/cloudflare/worker/http-server.js.map +0 -1
  236. package/lib/cloudflare/worker/miniflare-worker-options.d.ts.map +0 -1
  237. package/lib/cloudflare/worker/miniflare-worker-options.js.map +0 -1
  238. package/lib/cloudflare/worker/miniflare.d.ts.map +0 -1
  239. package/lib/cloudflare/worker/miniflare.js.map +0 -1
  240. package/lib/cloudflare/worker/mixed-mode.d.ts.map +0 -1
  241. package/lib/cloudflare/worker/mixed-mode.js.map +0 -1
  242. package/lib/cloudflare/worker/subdomain.d.ts +0 -3
  243. package/lib/cloudflare/worker/subdomain.d.ts.map +0 -1
  244. package/lib/cloudflare/worker/subdomain.js +0 -10
  245. package/lib/cloudflare/worker/subdomain.js.map +0 -1
  246. package/src/cloudflare/bundle/alias-plugin.ts +0 -48
  247. package/src/cloudflare/bundle/build-failures.ts +0 -55
  248. package/src/cloudflare/bundle/bundle-worker-dev.ts +0 -129
  249. package/src/cloudflare/bundle/bundle-worker.ts +0 -150
  250. package/src/cloudflare/bundle/external.ts +0 -41
  251. package/src/cloudflare/bundle/wasm-plugin.ts +0 -57
  252. package/src/cloudflare/worker/get-worker-template.ts +0 -14
  253. /package/lib/cloudflare/{bundle/local-dev-cloudflare-shim.d.ts → local-dev-cloudflare-shim.d.ts} +0 -0
  254. /package/lib/cloudflare/{worker → miniflare}/http-server.d.ts +0 -0
  255. /package/lib/cloudflare/{worker → miniflare}/http-server.js +0 -0
  256. /package/lib/cloudflare/{worker → miniflare}/miniflare.d.ts +0 -0
  257. /package/lib/cloudflare/{worker → miniflare}/mixed-mode.d.ts +0 -0
  258. /package/src/cloudflare/{worker → miniflare}/http-server.ts +0 -0
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { ResourceScope } from "../resource.ts";
4
4
  import type { Scope } from "../scope.ts";
5
- import { serialize, deserialize } from "../serde.ts";
5
+ import { deserialize, serialize } from "../serde.ts";
6
6
  import type { State, StateStore } from "../state.ts";
7
7
  import { ignore } from "../util/ignore.ts";
8
8
 
package/src/scope.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import util from "node:util";
3
3
  import type { Phase } from "./alchemy.ts";
4
+ import { D1StateStore } from "./cloudflare/d1-state-store.ts";
4
5
  import { DOStateStore } from "./cloudflare/do-state-store/index.ts";
5
6
  import { destroy, destroyAll } from "./destroy.ts";
6
7
  import { FileSystemStateStore } from "./fs/file-system-state-store.ts";
@@ -38,7 +39,7 @@ export interface ScopeOptions {
38
39
  stateStore?: StateStoreType;
39
40
  quiet?: boolean;
40
41
  phase?: Phase;
41
- dev?: boolean;
42
+ dev?: "prefer-local" | "prefer-remote";
42
43
  telemetryClient?: ITelemetryClient;
43
44
  logger?: LoggerApi;
44
45
  }
@@ -107,7 +108,7 @@ export class Scope {
107
108
  public readonly stateStore: StateStoreType;
108
109
  public readonly quiet: boolean;
109
110
  public readonly phase: Phase;
110
- public readonly dev?: boolean;
111
+ public readonly dev?: "prefer-local" | "prefer-remote";
111
112
  public readonly logger: LoggerApi;
112
113
  public readonly telemetryClient: ITelemetryClient;
113
114
  public readonly dataMutex: AsyncMutex;
@@ -162,21 +163,16 @@ export class Scope {
162
163
  options.logger,
163
164
  );
164
165
 
165
- this.dev = options.dev ?? this.parent?.dev ?? false;
166
+ this.dev = options.dev ?? this.parent?.dev;
166
167
 
167
168
  if (this.dev) {
168
169
  this.logger.warnOnce(
169
- "Local development mode is in beta. Please report any issues to https://github.com/sam-goodwin/alchemy/issues.",
170
+ "Development mode is in beta. Please report any issues to https://github.com/sam-goodwin/alchemy/issues.",
170
171
  );
171
172
  }
172
173
 
173
174
  this.stateStore =
174
- options.stateStore ??
175
- this.parent?.stateStore ??
176
- ((scope) =>
177
- process.env.ALCHEMY_STATE_STORE === "cloudflare"
178
- ? new DOStateStore(scope)
179
- : new FileSystemStateStore(scope));
175
+ options.stateStore ?? this.parent?.stateStore ?? defaultStateStore;
180
176
  this.state = this.stateStore(this);
181
177
  if (!options.telemetryClient && !this.parent?.telemetryClient) {
182
178
  throw new Error("Telemetry client is required");
@@ -206,11 +202,23 @@ export class Scope {
206
202
  }
207
203
 
208
204
  public get chain(): string[] {
205
+ // Since the root scope name is the same as the app name, this ensures
206
+ // the root scope chain is "<app-name>" instead of "<app-name>/<app-name>".
207
+ if (
208
+ !this.parent &&
209
+ this.appName &&
210
+ this.scopeName &&
211
+ this.appName === this.scopeName
212
+ ) {
213
+ return [this.appName];
214
+ }
215
+
209
216
  const thisScope = this.scopeName ? [this.scopeName] : [];
210
- const app = this.appName ? [this.appName] : [];
211
217
  if (this.parent) {
212
218
  return [...this.parent.chain, ...thisScope];
213
219
  }
220
+
221
+ const app = this.appName ? [this.appName] : [];
214
222
  return [...app, ...thisScope];
215
223
  }
216
224
 
@@ -489,6 +497,17 @@ export class Scope {
489
497
  }
490
498
  }
491
499
 
500
+ const defaultStateStore: StateStoreType = (scope: Scope) => {
501
+ switch (process.env.ALCHEMY_STATE_STORE) {
502
+ case "cloudflare-d1":
503
+ return new D1StateStore(scope);
504
+ case "cloudflare":
505
+ return new DOStateStore(scope);
506
+ default:
507
+ return new FileSystemStateStore(scope);
508
+ }
509
+ };
510
+
492
511
  declare global {
493
512
  // for runtime
494
513
  // TODO(sam): maybe inject is a better way to achieve this
package/src/serde.ts CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  ResourceKind,
6
6
  type Resource,
7
7
  } from "./resource.ts";
8
- import { isScope, Scope } from "./scope.ts";
8
+ import { isScope, type Scope } from "./scope.ts";
9
9
  import { isSecret, Secret } from "./secret.ts";
10
10
 
11
11
  import type { Type } from "arktype";
@@ -77,7 +77,7 @@ export async function serializeScope(scope: Scope): Promise<SerializedScope> {
77
77
  async function serializeScope(scope: Scope): Promise<SerializedScope> {
78
78
  await Promise.all(
79
79
  Array.from(scope.resources.values()).map(async (resource) => {
80
- if (resource[ResourceKind] === Scope.KIND) {
80
+ if (resource[ResourceKind] === "alchemy::Scope") {
81
81
  return;
82
82
  }
83
83
  map[resource[ResourceFQN]] = await serialize(scope, await resource, {
@@ -121,7 +121,7 @@ export async function serialize(
121
121
  if (!scope.password) {
122
122
  throw new Error(
123
123
  "Cannot serialize secret without password, did you forget to set password when initializing your alchemy app?\n" +
124
- "See: https://alchemy.run/docs/concepts/secret.html#encryption-password",
124
+ "See: https://alchemy.run/concepts/secret/#encryption-password",
125
125
  );
126
126
  }
127
127
  return {
@@ -220,7 +220,7 @@ export async function deserialize(
220
220
  if (!scope.password) {
221
221
  throw new Error(
222
222
  "Cannot deserialize secret without password, did you forget to set password when initializing your alchemy app?\n" +
223
- "See: https://alchemy.run/docs/concepts/secret.html#encryption-password",
223
+ "See: https://alchemy.run/concepts/secret/#encryption-password",
224
224
  );
225
225
  }
226
226
  if (typeof value["@secret"] === "object") {
@@ -0,0 +1,199 @@
1
+ import { and, eq, getTableColumns, inArray } from "drizzle-orm";
2
+ import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";
3
+ import path from "node:path";
4
+ import type { Scope } from "../scope.ts";
5
+ import type { State, StateStore } from "../state.ts";
6
+ import * as schema from "./schema.ts";
7
+
8
+ type Database = BaseSQLiteDatabase<any, any, typeof schema>;
9
+
10
+ export interface SQLiteStateStoreOptions<T extends Database = Database> {
11
+ create: () => Promise<T>;
12
+ }
13
+
14
+ const { scope: _, ...columns } = getTableColumns(schema.resources);
15
+
16
+ export class BaseSQLiteStateStore<T extends Database = Database>
17
+ implements StateStore
18
+ {
19
+ private chain: string[];
20
+ private create: () => Promise<T>;
21
+ private dbPromise?: Promise<T>;
22
+
23
+ constructor(
24
+ private readonly scope: Scope,
25
+ options: SQLiteStateStoreOptions<T>,
26
+ ) {
27
+ this.chain = this.scope.chain;
28
+ this.create = options.create;
29
+ }
30
+
31
+ private async db() {
32
+ this.dbPromise ??= withExponentialBackoff("init", () => this.create());
33
+ return await this.dbPromise;
34
+ }
35
+
36
+ async init() {
37
+ await this.db();
38
+ }
39
+
40
+ async list() {
41
+ const ids = await this.run("list", (db) =>
42
+ db
43
+ .select({ id: schema.resources.id })
44
+ .from(schema.resources)
45
+ .where(eq(schema.resources.scope, this.chain)),
46
+ );
47
+ return ids.map((id) => id.id);
48
+ }
49
+
50
+ async count() {
51
+ return await this.run("count", (db) =>
52
+ db.$count(
53
+ db
54
+ .select({ id: schema.resources.id })
55
+ .from(schema.resources)
56
+ .where(eq(schema.resources.scope, this.chain)),
57
+ ),
58
+ );
59
+ }
60
+
61
+ async get(id: string) {
62
+ const [state] = await this.run("get", (db) =>
63
+ db
64
+ .select(columns)
65
+ .from(schema.resources)
66
+ .where(
67
+ and(
68
+ eq(schema.resources.id, id),
69
+ eq(schema.resources.scope, this.chain),
70
+ ),
71
+ ),
72
+ );
73
+ if (!state) {
74
+ return;
75
+ }
76
+ return await this.deserialize(state);
77
+ }
78
+
79
+ async getBatch(ids: string[]) {
80
+ const states = await this.run("getBatch", async (db) =>
81
+ db
82
+ .select(columns)
83
+ .from(schema.resources)
84
+ .where(
85
+ and(
86
+ inArray(schema.resources.id, ids),
87
+ eq(schema.resources.scope, this.chain),
88
+ ),
89
+ ),
90
+ );
91
+ return await this.deserializeMany(states);
92
+ }
93
+
94
+ async all() {
95
+ const states = await this.run("all", async (db) =>
96
+ db
97
+ .select(columns)
98
+ .from(schema.resources)
99
+ .where(eq(schema.resources.scope, this.chain)),
100
+ );
101
+ return await this.deserializeMany(states);
102
+ }
103
+
104
+ async set(_: string, state: State) {
105
+ const serialized = await this.serialize(state);
106
+ await this.run("set", (db) =>
107
+ db
108
+ .insert(schema.resources)
109
+ .values({
110
+ ...serialized,
111
+ scope: this.chain,
112
+ })
113
+ .onConflictDoUpdate({
114
+ target: [schema.resources.id, schema.resources.scope],
115
+ set: serialized,
116
+ }),
117
+ );
118
+ }
119
+
120
+ async delete(id: string) {
121
+ await this.run("delete", async (db) =>
122
+ db
123
+ .delete(schema.resources)
124
+ .where(
125
+ and(
126
+ eq(schema.resources.id, id),
127
+ eq(schema.resources.scope, this.chain),
128
+ ),
129
+ ),
130
+ );
131
+ }
132
+
133
+ private async run<U>(
134
+ label: string,
135
+ fn: (db: T) => Promise<U>,
136
+ attempt = 0,
137
+ ): Promise<U> {
138
+ const db = await this.db();
139
+ return await withExponentialBackoff(label, () => fn(db), attempt);
140
+ }
141
+
142
+ private async deserialize(
143
+ state: Omit<schema.Resource, "scope">,
144
+ ): Promise<State> {
145
+ const { deserialize } = await import("../serde.ts"); // dynamic import to avoid circular dependency
146
+ return await deserialize(this.scope, {
147
+ ...state,
148
+ oldProps: state.oldProps ?? undefined,
149
+ });
150
+ }
151
+
152
+ private async serialize(state: State) {
153
+ const { serialize } = await import("../serde.ts");
154
+ return await serialize(this.scope, state);
155
+ }
156
+
157
+ private async deserializeMany(states: Omit<schema.Resource, "scope">[]) {
158
+ const deserialized = await Promise.all(
159
+ states.map((state) => this.deserialize(state)),
160
+ );
161
+ const map: Record<string, State> = {};
162
+ for (const state of deserialized) {
163
+ map[state.id] = state;
164
+ }
165
+ return map;
166
+ }
167
+ }
168
+
169
+ const withExponentialBackoff = async <T>(
170
+ label: string,
171
+ fn: () => Promise<T>,
172
+ attempt = 0,
173
+ ): Promise<T> => {
174
+ try {
175
+ const result = await fn();
176
+ return result;
177
+ } catch (cause) {
178
+ if (attempt < 10) {
179
+ await delay(attempt);
180
+ return withExponentialBackoff(label, fn, attempt + 1);
181
+ }
182
+ throw new Error(
183
+ `[SQLiteStateStore] Failed to run "${label}" after 10 attempts`,
184
+ {
185
+ cause,
186
+ },
187
+ );
188
+ }
189
+ };
190
+
191
+ const delay = (attempt: number, initialDelay = 100, jitterFactor = 0.2) => {
192
+ const baseDelay = initialDelay * 2 ** attempt;
193
+ const jitter = Math.floor(Math.random() * baseDelay * jitterFactor);
194
+ return new Promise((resolve) => setTimeout(resolve, baseDelay + jitter));
195
+ };
196
+
197
+ export const resolveMigrationsPath = () => {
198
+ return path.join(import.meta.dirname, "..", "..", "drizzle");
199
+ };
@@ -0,0 +1 @@
1
+ export * from "./sqlite-state-store.ts";
@@ -0,0 +1,37 @@
1
+ import {
2
+ integer,
3
+ primaryKey,
4
+ sqliteTable,
5
+ text,
6
+ } from "drizzle-orm/sqlite-core";
7
+ import type {
8
+ Resource as AlchemyResource,
9
+ ResourceProps,
10
+ } from "../resource.ts";
11
+
12
+ export const resources = sqliteTable(
13
+ "resources",
14
+ {
15
+ id: text("id").notNull(),
16
+ scope: text("scope", { mode: "json" }).$type<string[]>().notNull(),
17
+ status: text("status", {
18
+ enum: [
19
+ "creating",
20
+ "created",
21
+ "updating",
22
+ "updated",
23
+ "deleting",
24
+ "deleted",
25
+ ],
26
+ }).notNull(),
27
+ kind: text("kind").notNull(),
28
+ fqn: text("fqn").notNull(),
29
+ seq: integer("seq").notNull(),
30
+ data: text("data", { mode: "json" }).notNull().$type<Record<string, any>>(),
31
+ props: text("props", { mode: "json" }).$type<ResourceProps>(),
32
+ oldProps: text("oldProps", { mode: "json" }).$type<ResourceProps>(),
33
+ output: text("output", { mode: "json" }).notNull().$type<AlchemyResource>(),
34
+ },
35
+ (t) => [primaryKey({ columns: [t.scope, t.id] })],
36
+ );
37
+ export type Resource = typeof resources.$inferSelect;
@@ -0,0 +1,174 @@
1
+ import type { Config as LibSQLConfig } from "@libsql/client";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import type { Scope } from "../scope.ts";
5
+ import { memoize } from "../util/memoize.ts";
6
+ import {
7
+ BaseSQLiteStateStore,
8
+ resolveMigrationsPath,
9
+ } from "./base-sqlite-state-store.ts";
10
+ import * as schema from "./schema.ts";
11
+
12
+ interface BunSQLiteStateStoreOptions {
13
+ /**
14
+ * Use `bun:sqlite` to connect to the SQLite database.
15
+ * Requires Bun.
16
+ */
17
+ engine: "bun";
18
+ /**
19
+ * The filename to use for the SQLite database.
20
+ * @default process.env.ALCHEMY_STATE_FILE if set, otherwise ".alchemy/state.sqlite"
21
+ */
22
+ filename?: string;
23
+
24
+ // Options are copied from Bun instead of inherited because Bun's type is not exported,
25
+ // and the constructor type isn't an interface so inheritance doesn't work.
26
+
27
+ readonly?: boolean;
28
+ create?: boolean;
29
+ readwrite?: boolean;
30
+ safeIntegers?: boolean;
31
+ strict?: boolean;
32
+ }
33
+
34
+ interface LibSQLStateStoreOptions extends Omit<LibSQLConfig, "url"> {
35
+ /**
36
+ * Use the `@libsql/client` library to connect to the SQLite database.
37
+ * Supported on Node.js and Bun.
38
+ */
39
+ engine: "libsql";
40
+ /**
41
+ * The filename to use for the SQLite database.
42
+ * @default process.env.ALCHEMY_STATE_FILE if set, otherwise ".alchemy/state.sqlite"
43
+ * @note If the `url` option is specified, this option is ignored.
44
+ */
45
+ filename?: string;
46
+ /**
47
+ * The database URL. Overrides the `filename` option.
48
+ *
49
+ * The client supports `libsql:`, `http:`/`https:`, `ws:`/`wss:` and `file:` URL. For more infomation,
50
+ * please refer to the project README:
51
+ *
52
+ * https://github.com/libsql/libsql-client-ts#supported-urls
53
+ */
54
+ url?: string;
55
+ }
56
+
57
+ interface AutoSQLiteStateStoreOptions {
58
+ /**
59
+ * Automatically choose the best SQLite engine based on your environment.
60
+ * @default "auto" - Uses `bun:sqlite` if available, otherwise uses `@libsql/client`.
61
+ */
62
+ engine?: "auto";
63
+
64
+ /**
65
+ * The filename to use for the SQLite database.
66
+ * @default ".alchemy/state.sqlite"
67
+ */
68
+ filename?: string;
69
+ }
70
+
71
+ type SQLiteStateStoreOptions =
72
+ | BunSQLiteStateStoreOptions
73
+ | LibSQLStateStoreOptions
74
+ | AutoSQLiteStateStoreOptions;
75
+
76
+ export class SQLiteStateStore extends BaseSQLiteStateStore {
77
+ constructor(scope: Scope, options?: SQLiteStateStoreOptions) {
78
+ super(scope, {
79
+ create: async () => createDatabase(options),
80
+ });
81
+ }
82
+ }
83
+
84
+ const createDatabase = memoize(
85
+ async (options: SQLiteStateStoreOptions | undefined) => {
86
+ switch (options?.engine) {
87
+ case "bun":
88
+ return await createBunSQLiteDatabase(options);
89
+ case "libsql":
90
+ return await createLibSQLDatabase(options);
91
+ default: {
92
+ return await createDefaultDatabase(options?.filename);
93
+ }
94
+ }
95
+ },
96
+ );
97
+
98
+ async function createDefaultDatabase(filename: string | undefined) {
99
+ if ("Bun" in globalThis) {
100
+ return createBunSQLiteDatabase({ engine: "bun", filename });
101
+ }
102
+ return createLibSQLDatabase({ engine: "libsql", filename });
103
+ }
104
+
105
+ async function createBunSQLiteDatabase(
106
+ options: BunSQLiteStateStoreOptions | undefined,
107
+ ) {
108
+ if (!("Bun" in globalThis)) {
109
+ throw new Error(
110
+ "[SQLiteStateStore] The `engine: 'bun'` option is only available in Bun. Please use `engine: 'libsql'` instead.",
111
+ );
112
+ }
113
+
114
+ const filename =
115
+ options?.filename ??
116
+ process.env.ALCHEMY_STATE_FILE ??
117
+ ".alchemy/state.sqlite";
118
+ ensureDirectory(filename);
119
+ const { Database } = await import("bun:sqlite");
120
+ const { drizzle } = await import("drizzle-orm/bun-sqlite").catch(() => {
121
+ throw new Error(
122
+ "[SQLiteStateStore] Missing `drizzle-orm` peer dependency. Please `bun install drizzle-orm`.",
123
+ );
124
+ });
125
+ const { migrate } = await import("drizzle-orm/bun-sqlite/migrator");
126
+ // Bun's constructor throws if we pass in an empty object or if extraneous
127
+ // options are passed in, so here's some ugly destructuring!
128
+ const { engine: _engine, filename: _filename, ...rest } = options ?? {};
129
+ const bunOptions = Object.keys(rest).length > 0 ? rest : undefined;
130
+ const client = new Database(filename, bunOptions);
131
+ client.exec("PRAGMA journal_mode = WAL;");
132
+ const db = drizzle(client, {
133
+ schema,
134
+ });
135
+ migrate(db, { migrationsFolder: resolveMigrationsPath() });
136
+ return db;
137
+ }
138
+
139
+ async function createLibSQLDatabase(
140
+ options: LibSQLStateStoreOptions | undefined,
141
+ ) {
142
+ const url =
143
+ options?.url ??
144
+ `file:${options?.filename ?? process.env.ALCHEMY_STATE_FILE ?? ".alchemy/state.sqlite"}`;
145
+ const filename = url.startsWith("file:") ? url.slice(5) : undefined;
146
+ if (filename) {
147
+ ensureDirectory(filename);
148
+ }
149
+ const { createClient } = await import("@libsql/client").catch(() => {
150
+ throw new Error(
151
+ "[SQLiteStateStore] Missing `@libsql/client` peer dependency. Please `npm install @libsql/client`.",
152
+ );
153
+ });
154
+ const { drizzle } = await import("drizzle-orm/libsql").catch(() => {
155
+ throw new Error(
156
+ "[SQLiteStateStore] Missing `drizzle-orm` peer dependency. Please `npm install drizzle-orm`.",
157
+ );
158
+ });
159
+ const { migrate } = await import("drizzle-orm/libsql/migrator");
160
+ const client = createClient({ url, ...options });
161
+ await client.execute("PRAGMA journal_mode = WAL;");
162
+ const db = drizzle(client, {
163
+ schema,
164
+ });
165
+ await migrate(db, { migrationsFolder: resolveMigrationsPath() });
166
+ return db;
167
+ }
168
+
169
+ const ensureDirectory = (filename: string) => {
170
+ const dir = path.dirname(filename);
171
+ if (!fs.existsSync(dir)) {
172
+ fs.mkdirSync(dir, { recursive: true });
173
+ }
174
+ };
package/src/test/bun.ts CHANGED
@@ -3,7 +3,6 @@
3
3
  import { afterAll, beforeAll, it } from "bun:test";
4
4
  import path from "node:path";
5
5
  import { alchemy } from "../alchemy.ts";
6
- import { DOStateStore } from "../cloudflare/index.ts";
7
6
  import { Scope } from "../scope.ts";
8
7
  import type { StateStoreType } from "../state.ts";
9
8
  import { NoopTelemetryClient } from "../util/telemetry/index.ts";
@@ -120,13 +119,6 @@ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
120
119
  defaultOptions = defaultOptions ?? {
121
120
  quiet: true,
122
121
  };
123
- if (
124
- defaultOptions.stateStore === undefined &&
125
- // process.env.CI &&
126
- process.env.ALCHEMY_STATE_STORE === "cloudflare"
127
- ) {
128
- defaultOptions.stateStore = (scope) => new DOStateStore(scope);
129
- }
130
122
 
131
123
  // Add skipIf functionality
132
124
  test.skipIf = it.skipIf.bind(it);
@@ -1,8 +1,11 @@
1
1
  import path from "node:path";
2
2
  import { afterAll, beforeAll, it } from "vitest";
3
3
  import { alchemy } from "../alchemy.ts";
4
- import { DOStateStore } from "../cloudflare/index.ts";
4
+ import { D1StateStore } from "../cloudflare/d1-state-store.ts";
5
+ import { DOStateStore } from "../cloudflare/do-state-store/index.ts";
6
+ import { FileSystemStateStore } from "../fs/file-system-state-store.ts";
5
7
  import { Scope } from "../scope.ts";
8
+ import { SQLiteStateStore } from "../sqlite/sqlite-state-store.ts";
6
9
  import type { StateStoreType } from "../state.ts";
7
10
  import { NoopTelemetryClient } from "../util/telemetry/client.ts";
8
11
 
@@ -123,13 +126,21 @@ export function test(
123
126
  if (!("quiet" in defaultOptions)) {
124
127
  defaultOptions.quiet = true;
125
128
  }
126
-
127
- if (
128
- defaultOptions.stateStore === undefined &&
129
- process.env.ALCHEMY_STATE_STORE === "cloudflare"
130
- ) {
131
- defaultOptions.stateStore = (scope) => new DOStateStore(scope);
132
- }
129
+ defaultOptions.stateStore ??= (scope) => {
130
+ const storeType = process.env.ALCHEMY_STATE_STORE;
131
+ switch (storeType) {
132
+ case "dofs":
133
+ return new DOStateStore(scope);
134
+ case "fs":
135
+ return new FileSystemStateStore(scope);
136
+ case "d1":
137
+ return new D1StateStore(scope);
138
+ default:
139
+ return new SQLiteStateStore(scope, {
140
+ filename: `.alchemy/${path.relative(process.cwd(), meta.filename)}.sqlite`,
141
+ });
142
+ }
143
+ };
133
144
 
134
145
  test.skipIf = (condition: boolean) => {
135
146
  if (condition) {
@@ -1,10 +1,12 @@
1
+ import { createHash } from "node:crypto";
2
+
1
3
  type AsyncReturnType<T> = T extends (...args: any[]) => Promise<infer R>
2
4
  ? R
3
5
  : T;
4
6
 
5
7
  export function memoize<F extends (...args: any[]) => Promise<any>>(
6
8
  fn: F,
7
- keyFn: (...args: Parameters<F>) => string,
9
+ keyFn: (...args: Parameters<F>) => string = defaultKeyFn,
8
10
  ) {
9
11
  const cache = new Map<string, Promise<AsyncReturnType<F>>>();
10
12
  return async (...args: Parameters<F>): Promise<AsyncReturnType<F>> => {
@@ -21,3 +23,13 @@ export function memoize<F extends (...args: any[]) => Promise<any>>(
21
23
  return await promise;
22
24
  };
23
25
  }
26
+
27
+ const defaultKeyFn = (...args: any[]) => {
28
+ const hash = createHash("sha256");
29
+ args.forEach((arg) => {
30
+ if (arg) {
31
+ hash.update(JSON.stringify(arg));
32
+ }
33
+ });
34
+ return hash.digest("hex");
35
+ };
@@ -1,5 +1,5 @@
1
1
  // This file infers types for the cloudflare:workers environment from your Alchemy Worker.
2
- // @see https://alchemy.run/docs/concepts/bindings.html#type-safe-bindings
2
+ // @see https://alchemy.run/concepts/bindings/#type-safe-bindings
3
3
 
4
4
  import type { worker } from "../alchemy.run.ts";
5
5
 
@@ -1,5 +1,5 @@
1
1
  // This file infers types for the cloudflare:workers environment from your Alchemy Worker.
2
- // @see https://alchemy.run/docs/concepts/bindings.html#type-safe-bindings
2
+ // @see https://alchemy.run/concepts/bindings/#type-safe-bindings
3
3
 
4
4
  import type { worker } from "../alchemy.run.ts";
5
5
 
@@ -1,5 +1,5 @@
1
1
  // This file infers types for the cloudflare:workers environment from your Alchemy Worker.
2
- // @see https://alchemy.run/docs/concepts/bindings.html#type-safe-bindings
2
+ // @see https://alchemy.run/concepts/bindings/#type-safe-bindings
3
3
 
4
4
  import type { worker } from "../alchemy.run.ts";
5
5
 
@@ -1,5 +1,5 @@
1
1
  // This file infers types for the cloudflare:workers environment from your Alchemy Worker.
2
- // @see https://alchemy.run/docs/concepts/bindings.html#type-safe-bindings
2
+ // @see https://alchemy.run/concepts/bindings/#type-safe-bindings
3
3
 
4
4
  import type { worker } from "../alchemy.run.ts";
5
5
 
@@ -1,5 +1,5 @@
1
1
  // This file infers types for the cloudflare:workers environment from your Alchemy Worker.
2
- // @see https://alchemy.run/docs/concepts/bindings.html#type-safe-bindings
2
+ // @see https://alchemy.run/concepts/bindings/#type-safe-bindings
3
3
 
4
4
  import type { worker } from "../alchemy.run.ts";
5
5