alchemy 2.0.0-beta.23 → 2.0.0-beta.25

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 (138) hide show
  1. package/bin/alchemy.js +12574 -2451
  2. package/bin/alchemy.js.map +1 -1
  3. package/bin/alchemy.ts +2 -0
  4. package/bin/cli.js +18 -0
  5. package/bin/commands/bootstrap.ts +88 -4
  6. package/bin/commands/deploy.ts +54 -20
  7. package/bin/commands/dev.ts +45 -0
  8. package/bin/exec.ts +22 -0
  9. package/lib/AdoptPolicy.d.ts +2 -2
  10. package/lib/AdoptPolicy.d.ts.map +1 -1
  11. package/lib/AdoptPolicy.js.map +1 -1
  12. package/lib/AlchemyContext.d.ts +1 -0
  13. package/lib/AlchemyContext.d.ts.map +1 -1
  14. package/lib/AlchemyContext.js +1 -0
  15. package/lib/AlchemyContext.js.map +1 -1
  16. package/lib/Build/Command.d.ts.map +1 -1
  17. package/lib/Build/Command.js +8 -1
  18. package/lib/Build/Command.js.map +1 -1
  19. package/lib/Bundle/Bundle.d.ts +21 -9
  20. package/lib/Bundle/Bundle.d.ts.map +1 -1
  21. package/lib/Bundle/Bundle.js +29 -15
  22. package/lib/Bundle/Bundle.js.map +1 -1
  23. package/lib/Cloudflare/Local/Sidecar.d.ts +27 -0
  24. package/lib/Cloudflare/Local/Sidecar.d.ts.map +1 -0
  25. package/lib/Cloudflare/Local/Sidecar.js +24 -0
  26. package/lib/Cloudflare/Local/Sidecar.js.map +1 -0
  27. package/lib/Cloudflare/Local/SidecarHandlers.d.ts +5 -0
  28. package/lib/Cloudflare/Local/SidecarHandlers.d.ts.map +1 -0
  29. package/lib/Cloudflare/Local/SidecarHandlers.js +119 -0
  30. package/lib/Cloudflare/Local/SidecarHandlers.js.map +1 -0
  31. package/lib/Cloudflare/Local/SidecarServer.d.ts +2 -0
  32. package/lib/Cloudflare/Local/SidecarServer.d.ts.map +1 -0
  33. package/lib/Cloudflare/Local/SidecarServer.js +27 -0
  34. package/lib/Cloudflare/Local/SidecarServer.js.map +1 -0
  35. package/lib/Cloudflare/SecretsStore/Secret.d.ts.map +1 -1
  36. package/lib/Cloudflare/SecretsStore/Secret.js +55 -7
  37. package/lib/Cloudflare/SecretsStore/Secret.js.map +1 -1
  38. package/lib/Cloudflare/SecretsStore/SecretsStore.d.ts.map +1 -1
  39. package/lib/Cloudflare/SecretsStore/SecretsStore.js +35 -11
  40. package/lib/Cloudflare/SecretsStore/SecretsStore.js.map +1 -1
  41. package/lib/Cloudflare/StateStore/State.d.ts +35 -0
  42. package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
  43. package/lib/Cloudflare/StateStore/State.js +187 -45
  44. package/lib/Cloudflare/StateStore/State.js.map +1 -1
  45. package/lib/Cloudflare/StateStore/index.d.ts +1 -1
  46. package/lib/Cloudflare/StateStore/index.d.ts.map +1 -1
  47. package/lib/Cloudflare/StateStore/index.js +1 -1
  48. package/lib/Cloudflare/StateStore/index.js.map +1 -1
  49. package/lib/Cloudflare/Workers/Compatibility.d.ts +6 -0
  50. package/lib/Cloudflare/Workers/Compatibility.d.ts.map +1 -0
  51. package/lib/Cloudflare/Workers/Compatibility.js +10 -0
  52. package/lib/Cloudflare/Workers/Compatibility.js.map +1 -0
  53. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +7 -0
  54. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -0
  55. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +81 -0
  56. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -0
  57. package/lib/Cloudflare/Workers/Worker.d.ts +3 -1
  58. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  59. package/lib/Cloudflare/Workers/Worker.js +89 -31
  60. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  61. package/lib/Cloudflare/Workers/WorkerBundle.d.ts +31 -0
  62. package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -0
  63. package/lib/Cloudflare/Workers/WorkerBundle.js +174 -0
  64. package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -0
  65. package/lib/Cloudflare/Workers/WorkerName.d.ts +3 -0
  66. package/lib/Cloudflare/Workers/WorkerName.d.ts.map +1 -0
  67. package/lib/Cloudflare/Workers/WorkerName.js +9 -0
  68. package/lib/Cloudflare/Workers/WorkerName.js.map +1 -0
  69. package/lib/Cloudflare/Workers/index.d.ts +1 -0
  70. package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
  71. package/lib/Cloudflare/Workers/index.js +1 -0
  72. package/lib/Cloudflare/Workers/index.js.map +1 -1
  73. package/lib/Platform.d.ts +1 -1
  74. package/lib/Platform.d.ts.map +1 -1
  75. package/lib/Sidecar/Lock.d.ts +24 -0
  76. package/lib/Sidecar/Lock.d.ts.map +1 -0
  77. package/lib/Sidecar/Lock.js +102 -0
  78. package/lib/Sidecar/Lock.js.map +1 -0
  79. package/lib/Sidecar/RpcClient.d.ts +15 -0
  80. package/lib/Sidecar/RpcClient.d.ts.map +1 -0
  81. package/lib/Sidecar/RpcClient.js +75 -0
  82. package/lib/Sidecar/RpcClient.js.map +1 -0
  83. package/lib/Sidecar/RpcHandler.d.ts +48 -0
  84. package/lib/Sidecar/RpcHandler.d.ts.map +1 -0
  85. package/lib/Sidecar/RpcHandler.js +64 -0
  86. package/lib/Sidecar/RpcHandler.js.map +1 -0
  87. package/lib/Sidecar/RpcPaths.d.ts +14 -0
  88. package/lib/Sidecar/RpcPaths.d.ts.map +1 -0
  89. package/lib/Sidecar/RpcPaths.js +22 -0
  90. package/lib/Sidecar/RpcPaths.js.map +1 -0
  91. package/lib/Sidecar/RpcServer.d.ts +10 -0
  92. package/lib/Sidecar/RpcServer.d.ts.map +1 -0
  93. package/lib/Sidecar/RpcServer.js +50 -0
  94. package/lib/Sidecar/RpcServer.js.map +1 -0
  95. package/lib/Sidecar/RpcTransport.d.ts +22 -0
  96. package/lib/Sidecar/RpcTransport.d.ts.map +1 -0
  97. package/lib/Sidecar/RpcTransport.js +81 -0
  98. package/lib/Sidecar/RpcTransport.js.map +1 -0
  99. package/lib/Telemetry/Metrics.d.ts +15 -0
  100. package/lib/Telemetry/Metrics.d.ts.map +1 -1
  101. package/lib/Telemetry/Metrics.js +20 -0
  102. package/lib/Telemetry/Metrics.js.map +1 -1
  103. package/lib/Util/PlatformServices.d.ts +3 -0
  104. package/lib/Util/PlatformServices.d.ts.map +1 -1
  105. package/lib/Util/PlatformServices.js +13 -0
  106. package/lib/Util/PlatformServices.js.map +1 -1
  107. package/lib/Util/layer-scoped.d.ts +16 -0
  108. package/lib/Util/layer-scoped.d.ts.map +1 -0
  109. package/lib/Util/layer-scoped.js +14 -0
  110. package/lib/Util/layer-scoped.js.map +1 -0
  111. package/package.json +4 -2
  112. package/src/AdoptPolicy.ts +9 -9
  113. package/src/AlchemyContext.ts +2 -0
  114. package/src/Build/Command.ts +8 -1
  115. package/src/Bundle/Bundle.ts +65 -25
  116. package/src/Cloudflare/Local/Sidecar.ts +44 -0
  117. package/src/Cloudflare/Local/SidecarHandlers.ts +182 -0
  118. package/src/Cloudflare/Local/SidecarServer.ts +57 -0
  119. package/src/Cloudflare/SecretsStore/Secret.ts +78 -7
  120. package/src/Cloudflare/SecretsStore/SecretsStore.ts +47 -12
  121. package/src/Cloudflare/StateStore/State.ts +281 -64
  122. package/src/Cloudflare/StateStore/index.ts +1 -1
  123. package/src/Cloudflare/Workers/Compatibility.ts +12 -0
  124. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +93 -0
  125. package/src/Cloudflare/Workers/Worker.ts +151 -55
  126. package/src/Cloudflare/Workers/WorkerBundle.ts +234 -0
  127. package/src/Cloudflare/Workers/WorkerName.ts +10 -0
  128. package/src/Cloudflare/Workers/index.ts +1 -0
  129. package/src/Platform.ts +1 -1
  130. package/src/Sidecar/Lock.ts +190 -0
  131. package/src/Sidecar/RpcClient.ts +141 -0
  132. package/src/Sidecar/RpcHandler.ts +169 -0
  133. package/src/Sidecar/RpcPaths.ts +29 -0
  134. package/src/Sidecar/RpcServer.ts +80 -0
  135. package/src/Sidecar/RpcTransport.ts +85 -0
  136. package/src/Telemetry/Metrics.ts +38 -0
  137. package/src/Util/PlatformServices.ts +19 -0
  138. package/src/Util/layer-scoped.ts +25 -0
@@ -7,6 +7,7 @@ import * as Redacted from "effect/Redacted";
7
7
  import * as HttpClient from "effect/unstable/http/HttpClient";
8
8
 
9
9
  import * as Config from "effect/Config";
10
+ import { adopt } from "../../AdoptPolicy.ts";
10
11
  import { AuthError } from "../../Auth/AuthProvider.ts";
11
12
  import { readCredentials, writeCredentials } from "../../Auth/Credentials.ts";
12
13
  import { ALCHEMY_PROFILE } from "../../Auth/Profile.ts";
@@ -23,7 +24,7 @@ import * as Credentials from "../Credentials.ts";
23
24
  import { AlchemyContext } from "../../AlchemyContext.ts";
24
25
  import { makeLocalState } from "../../State/LocalState.ts";
25
26
  import { State, type StateService } from "../../State/State.ts";
26
- import { syncState } from "../../State/Sync.ts";
27
+ import { recordStateStoreOp } from "../../Telemetry/Metrics.ts";
27
28
  import * as Clank from "../../Util/Clank.ts";
28
29
  import { CloudflareAuth } from "../Auth/AuthProvider.ts";
29
30
  import { EdgeSessionError, createEdgeSession } from "../EdgeSession.ts";
@@ -33,6 +34,105 @@ import { AuthTokenSecretName, TokenValue } from "./Token.ts";
33
34
  /** Filename used for stored credentials under the profile directory. */
34
35
  const CREDENTIALS_FILE = "cloudflare-state-store";
35
36
 
37
+ export interface BootstrapOptions {
38
+ /**
39
+ * The name of the script to use for the state store.
40
+ * @default "alchemy-state-store"
41
+ */
42
+ workerName?: string;
43
+ /**
44
+ * Re-run the deploy even if the worker already exists in Cloudflare.
45
+ *
46
+ * Without this flag, an already-deployed worker is **adopted**: the
47
+ * auth-token secret is re-fetched via an edge probe and the local
48
+ * credentials file is refreshed, but no new code is uploaded. With
49
+ * `force: true` the full deploy runs again, redeploying the worker
50
+ * code and reconciling every resource in place.
51
+ */
52
+ force?: boolean;
53
+ }
54
+
55
+ /**
56
+ * Manually bootstrap (or repair) the Cloudflare-hosted HTTP State
57
+ * Store. Used by `alchemy bootstrap cloudflare`.
58
+ *
59
+ * The flow mirrors what {@link state} does on first use, but is
60
+ * exposed as a standalone effect so users can trigger it (or recover
61
+ * from a partially-failed previous deploy) explicitly:
62
+ *
63
+ * 1. Resume any in-progress local `CloudflareStateStore` stack.
64
+ * 2. If the worker already exists in Cloudflare and `force` is not
65
+ * set, adopt it: read the auth token via an edge-preview probe
66
+ * and persist credentials.
67
+ * 3. Otherwise run the full deploy with adoption enabled, sync local
68
+ * state into the deployed store, persist credentials, and delete
69
+ * the local stack.
70
+ */
71
+ export const bootstrap = (options: BootstrapOptions = {}) =>
72
+ Effect.gen(function* () {
73
+ const isCI = yield* Config.boolean("CI").pipe(Config.withDefault(false));
74
+ const profileName = yield* ALCHEMY_PROFILE;
75
+ const scriptName = options.workerName ?? STATE_STORE_SCRIPT_NAME;
76
+ const force = options.force ?? false;
77
+
78
+ const localState = yield* makeLocalState();
79
+ const hasLocalStack = yield* Effect.map(
80
+ localState.listStages("CloudflareStateStore"),
81
+ (stages) => stages.includes(scriptName),
82
+ );
83
+
84
+ if (hasLocalStack) {
85
+ yield* Clank.info(
86
+ `Resuming Cloudflare State Store '${scriptName}' deployment...`,
87
+ );
88
+ yield* finishBootstrap({ scriptName, profileName, localState, isCI });
89
+ yield* Clank.success(
90
+ `Cloudflare State Store '${scriptName}' is ready.`,
91
+ );
92
+ return;
93
+ }
94
+
95
+ const { accountId } = yield* CloudflareEnvironment.CloudflareEnvironment;
96
+ const workerExists = yield* workers
97
+ .getScriptSetting({ accountId, scriptName })
98
+ .pipe(
99
+ Effect.map((setting) => setting !== undefined),
100
+ Effect.catchTag("WorkerNotFound", () => Effect.succeed(false)),
101
+ );
102
+
103
+ if (workerExists && !force) {
104
+ yield* Clank.info(
105
+ `Worker '${scriptName}' already exists; adopting and refreshing credentials. ` +
106
+ `Use --force to redeploy.`,
107
+ );
108
+ const credentials = yield* loginWithCloudflare();
109
+ if (!isCI) {
110
+ yield* writeCredentials<HttpStateStoreProps>(
111
+ profileName,
112
+ CREDENTIALS_FILE,
113
+ credentials,
114
+ );
115
+ }
116
+ yield* Clank.success(
117
+ `Cloudflare State Store '${scriptName}' is ready.`,
118
+ );
119
+ return;
120
+ }
121
+
122
+ if (workerExists) {
123
+ yield* Clank.info(
124
+ `Forcing redeploy of Cloudflare State Store '${scriptName}'...`,
125
+ );
126
+ } else {
127
+ yield* Clank.info(
128
+ `Deploying Cloudflare State Store '${scriptName}'...`,
129
+ );
130
+ }
131
+
132
+ yield* finishBootstrap({ scriptName, profileName, localState, isCI });
133
+ yield* Clank.success(`Cloudflare State Store '${scriptName}' is ready.`);
134
+ });
135
+
36
136
  export const state = (props?: {
37
137
  /**
38
138
  * The name of the script to use for the state store.
@@ -53,6 +153,38 @@ export const state = (props?: {
53
153
 
54
154
  const scriptName = props?.workerName ?? STATE_STORE_SCRIPT_NAME;
55
155
 
156
+ // The bootstrap of the Cloudflare State Store is only considered
157
+ // successful once two invariants hold:
158
+ // 1. the worker has been deployed and the credentials persisted
159
+ // 2. the local CloudflareStateStore stack has been synced into
160
+ // the HTTP store and removed from disk
161
+ //
162
+ // If a previous run failed anywhere between deploying the worker
163
+ // and deleting the local stack (e.g. the worker got created but
164
+ // sync/credential-write/delete never ran) we will detect the
165
+ // leftover local stack here and resume the bootstrap. This loop
166
+ // re-runs deploy (idempotent over already-applied resources) and
167
+ // finishes the sync/credential/cleanup steps until both
168
+ // invariants are satisfied.
169
+ const localState = yield* makeLocalState();
170
+
171
+ const hasLocalStack = yield* Effect.map(
172
+ localState.listStages("CloudflareStateStore"),
173
+ (stages) => stages.includes(scriptName),
174
+ );
175
+
176
+ if (hasLocalStack) {
177
+ yield* Clank.info(
178
+ `Resuming Cloudflare State Store '${scriptName}' deployment...`,
179
+ );
180
+ return yield* finishBootstrap({
181
+ scriptName,
182
+ profileName,
183
+ localState,
184
+ isCI,
185
+ });
186
+ }
187
+
56
188
  // TODO(sam): support upgrading state store, right now we only deploy once
57
189
  if (credentials) {
58
190
  // it's in the profile, let's go
@@ -63,79 +195,63 @@ export const state = (props?: {
63
195
  }
64
196
 
65
197
  return httpState;
66
- } else {
67
- // our profile does not contain a reference to the state store, let's try and resolve it
68
- const { accountId } =
69
- yield* CloudflareEnvironment.CloudflareEnvironment;
70
- const workerExists = yield* workers
71
- .getScriptSetting({
72
- accountId,
73
- scriptName,
74
- })
75
- .pipe(
76
- Effect.map((setting) => setting !== undefined),
77
- Effect.catchTag("WorkerNotFound", () => Effect.succeed(false)),
78
- );
198
+ }
199
+
200
+ // our profile does not contain a reference to the state store, let's try and resolve it
201
+ const { accountId } = yield* CloudflareEnvironment.CloudflareEnvironment;
202
+ const workerExists = yield* workers
203
+ .getScriptSetting({
204
+ accountId,
205
+ scriptName,
206
+ })
207
+ .pipe(
208
+ Effect.map((setting) => setting !== undefined),
209
+ Effect.catchTag("WorkerNotFound", () => Effect.succeed(false)),
210
+ );
79
211
 
80
- if (workerExists) {
81
- // it exists, so fetch the secret token
82
- const credentials = yield* loginWithCloudflare();
83
- if (!isCI) {
84
- yield* writeCredentials<HttpStateStoreProps>(
85
- profileName,
86
- CREDENTIALS_FILE,
87
- credentials,
88
- );
89
- }
90
- const httpState = yield* makeHttpStateStore(credentials);
91
-
92
- if (alchemyContext.updateStateStore) {
93
- yield* deployStateStore(scriptName, httpState);
94
- }
95
-
96
- return httpState;
97
- } else if (isCI) {
98
- // TODO(sam): do we want to support bootstrapping the state store from CI?
99
- // for now - just die here
100
- return yield* Effect.die(
101
- new AuthError({
102
- message: `State store not found for script ${scriptName}. Deploy the state store first.`,
103
- }),
212
+ if (workerExists) {
213
+ // it exists, so fetch the secret token
214
+ const credentials = yield* loginWithCloudflare();
215
+ if (!isCI) {
216
+ yield* writeCredentials<HttpStateStoreProps>(
217
+ profileName,
218
+ CREDENTIALS_FILE,
219
+ credentials,
104
220
  );
105
221
  }
222
+ const httpState = yield* makeHttpStateStore(credentials);
106
223
 
107
- const shouldDeploy = yield* Clank.confirm({
108
- message:
109
- "Cloudflare State Store not found. Do you want to deploy it?",
110
- });
111
-
112
- if (!shouldDeploy) {
113
- return yield* Effect.die(new Clank.PromptCancelled());
224
+ if (alchemyContext.updateStateStore) {
225
+ yield* deployStateStore(scriptName, httpState);
114
226
  }
115
227
 
116
- const { url, authToken, localState } =
117
- yield* deployStateStore(scriptName);
118
-
119
- const httpState = yield* makeHttpStateStore({ url, authToken });
120
-
121
- yield* syncState(localState, httpState);
228
+ return httpState;
229
+ }
122
230
 
123
- yield* writeCredentials<HttpStateStoreProps>(
124
- profileName,
125
- CREDENTIALS_FILE,
126
- {
127
- url,
128
- authToken,
129
- },
231
+ if (isCI) {
232
+ // TODO(sam): do we want to support bootstrapping the state store from CI?
233
+ // for now - just die here
234
+ return yield* Effect.die(
235
+ new AuthError({
236
+ message: `State store not found for script ${scriptName}. Deploy the state store first.`,
237
+ }),
130
238
  );
239
+ }
131
240
 
132
- yield* localState.deleteStack({
133
- stack: "CloudflareStateStore",
134
- stage: scriptName,
135
- });
241
+ const shouldDeploy = yield* Clank.confirm({
242
+ message: "Cloudflare State Store not found. Do you want to deploy it?",
243
+ });
136
244
 
137
- return httpState;
245
+ if (!shouldDeploy) {
246
+ return yield* Effect.die(new Clank.PromptCancelled());
138
247
  }
248
+
249
+ return yield* finishBootstrap({
250
+ scriptName,
251
+ profileName,
252
+ localState,
253
+ isCI,
254
+ });
139
255
  }).pipe(Effect.orDie),
140
256
  ).pipe(
141
257
  Layer.provideMerge(Credentials.fromAuthProvider()),
@@ -144,6 +260,93 @@ export const state = (props?: {
144
260
  Layer.orDie,
145
261
  );
146
262
 
263
+ /**
264
+ * Non-destructively copy every resource in the
265
+ * `CloudflareStateStore/<scriptName>` stack from `source` into
266
+ * `destination`, leaving every other stack in `destination` untouched.
267
+ *
268
+ * This intentionally does not delete anything from `destination`: at
269
+ * bootstrap time the destination is the user's live remote state
270
+ * store, and removing entries that happen to be missing locally would
271
+ * be catastrophic.
272
+ */
273
+ const hoistBootstrapStack = Effect.fnUntraced(function* (
274
+ source: StateService,
275
+ destination: StateService,
276
+ scriptName: string,
277
+ ) {
278
+ const stack = "CloudflareStateStore";
279
+ const stage = scriptName;
280
+ const fqns = yield* source.list({ stack, stage });
281
+ yield* Effect.forEach(
282
+ fqns,
283
+ Effect.fnUntraced(function* (fqn) {
284
+ const value = yield* source.get({ stack, stage, fqn });
285
+ if (value) {
286
+ yield* destination.set({ stack, stage, fqn, value });
287
+ }
288
+ }),
289
+ { concurrency: "unbounded" },
290
+ );
291
+ });
292
+
293
+ /**
294
+ * Finish (or resume) the bootstrap of the Cloudflare State Store using
295
+ * the provided local state as the source of truth. This is idempotent
296
+ * and safe to re-run: any resources already applied during a previous
297
+ * partial run will be reconciled, the local stack will be synced into
298
+ * the deployed HTTP store, credentials persisted, and finally the
299
+ * local stack removed - which is what marks the bootstrap as complete.
300
+ */
301
+ const finishBootstrap = ({
302
+ scriptName,
303
+ profileName,
304
+ localState,
305
+ isCI,
306
+ }: {
307
+ scriptName: string;
308
+ profileName: string;
309
+ localState: StateService;
310
+ isCI: boolean;
311
+ }) =>
312
+ Effect.gen(function* () {
313
+ yield* deployStateStore(scriptName, localState);
314
+
315
+ // Don't trust the `authToken` returned by `deploy(...)`: when
316
+ // adoption kicks in (the Secrets Store secret already existed),
317
+ // the locally-generated `Random` value won't match the value
318
+ // actually persisted in Cloudflare, and any HTTP call to the
319
+ // worker would 401. Re-read the live token from the deployed
320
+ // worker via the same edge-preview probe `loginWithCloudflare`
321
+ // uses, so the credentials we persist always reflect what is
322
+ // actually deployed. `loginWithCloudflare` also persists the
323
+ // credentials file (skipping the write in CI), so we don't need
324
+ // to do that explicitly here.
325
+ const { url, authToken } = yield* loginWithCloudflare();
326
+ const httpState = yield* makeHttpStateStore({ url, authToken });
327
+ // `profileName` is intentionally unused here — `loginWithCloudflare`
328
+ // resolves it itself. Reference it to keep the surrounding API
329
+ // explicit and to avoid an unused-parameter lint.
330
+ void profileName;
331
+ void isCI;
332
+
333
+ // Copy the freshly-deployed CloudflareStateStore stack into the
334
+ // remote store. We deliberately do NOT call `syncState` here:
335
+ // syncState is a *mirror* that deletes everything in the
336
+ // destination that is not in the source, which would wipe every
337
+ // user stack in the remote store on any subsequent bootstrap
338
+ // (resume / --force / partial-failure recovery), since by that
339
+ // point local state only contains the bootstrap stack itself.
340
+ yield* hoistBootstrapStack(localState, httpState, scriptName);
341
+
342
+ yield* localState.deleteStack({
343
+ stack: "CloudflareStateStore",
344
+ stage: scriptName,
345
+ });
346
+
347
+ return httpState;
348
+ });
349
+
147
350
  const deployStateStore = (scriptName: string, state?: StateService) =>
148
351
  Effect.gen(function* () {
149
352
  const localState = state ?? (yield* makeLocalState());
@@ -172,11 +375,25 @@ const deployStateStore = (scriptName: string, state?: StateService) =>
172
375
  }),
173
376
  ),
174
377
  }).pipe(
378
+ // The Cloudflare State Store is account-level infrastructure that
379
+ // outlives any single deploy: its underlying Secrets Store and
380
+ // auth-token secret may already exist from a previous (possibly
381
+ // partially-failed) bootstrap. Opt in to adoption so the
382
+ // resources reconcile in place instead of failing on conflict.
383
+ adopt(true),
175
384
  // TODO(sam): we should not need to do this, but types do complain. fix deploy
176
385
  Effect.provide(stateLayer),
177
386
  );
178
387
  return { url, authToken, localState };
179
- });
388
+ }).pipe(
389
+ Effect.withSpan("state_store.deploy", {
390
+ attributes: {
391
+ "alchemy.state_store.script_name": scriptName,
392
+ "alchemy.state_store.op": "deploy",
393
+ },
394
+ }),
395
+ recordStateStoreOp("deploy"),
396
+ );
180
397
 
181
398
  /**
182
399
  * Log in to a Cloudflare-deployed HTTP state-store.
@@ -1 +1 @@
1
- export { state } from "./State.ts";
1
+ export { bootstrap, state, type BootstrapOptions } from "./State.ts";
@@ -0,0 +1,12 @@
1
+ import type { WorkerProps } from "./Worker.ts";
2
+
3
+ // TODO: figure out why the later one from workerd breaks
4
+ const DEFAULT_COMPATIBILITY_DATE = "2026-03-17";
5
+
6
+ export const getCompatibility = (props: WorkerProps) => ({
7
+ date: props.compatibility?.date ?? DEFAULT_COMPATIBILITY_DATE,
8
+ flags: [
9
+ ...(props.compatibility?.flags ?? []),
10
+ ...(props.isExternal ? [] : ["nodejs_compat"]),
11
+ ].filter((value, index, self) => self.indexOf(value) === index),
12
+ });
@@ -0,0 +1,93 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Redacted from "effect/Redacted";
3
+ import * as Provider from "../../Provider.ts";
4
+ import type { ResourceBinding } from "../../Resource.ts";
5
+ import { Stack } from "../../Stack.ts";
6
+ import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
7
+ import { Sidecar } from "../Local/Sidecar.ts";
8
+ import { getCompatibility } from "./Compatibility.ts";
9
+ import { Worker, type WorkerBinding, type WorkerProps } from "./Worker.ts";
10
+ import { createWorkerName } from "./WorkerName.ts";
11
+
12
+ export const LocalWorkerProvider = () =>
13
+ Provider.effect(
14
+ Worker,
15
+ Effect.gen(function* () {
16
+ const { accountId } = yield* CloudflareEnvironment;
17
+ const stack = yield* Stack;
18
+ const sidecar = yield* Sidecar;
19
+
20
+ const run = Effect.fn(function* (
21
+ id: string,
22
+ props: WorkerProps,
23
+ bindings: ResourceBinding<Worker["Binding"]>[],
24
+ ) {
25
+ const name = yield* createWorkerName(id, props.name);
26
+ const workerBindings: WorkerBinding[] = [];
27
+ const durableObjectNamespaces: Record<string, string> = {};
28
+ for (const { sid, data } of bindings) {
29
+ for (const binding of data.bindings ?? []) {
30
+ workerBindings.push(binding);
31
+ if (binding.type === "durable_object_namespace") {
32
+ durableObjectNamespaces[binding.name] = sid;
33
+ }
34
+ }
35
+ }
36
+ for (const [key, value] of Object.entries(props.env ?? {})) {
37
+ if (Redacted.isRedacted(value)) {
38
+ workerBindings.push({
39
+ type: "secret_text",
40
+ name: key,
41
+ text: Redacted.value(value),
42
+ });
43
+ } else {
44
+ workerBindings.push({
45
+ type: "plain_text",
46
+ name: key,
47
+ text: value,
48
+ });
49
+ }
50
+ }
51
+ const result = yield* sidecar.serve({
52
+ id,
53
+ name,
54
+ main: props.main,
55
+ compatibility: getCompatibility(props),
56
+ entry: props.isExternal
57
+ ? {
58
+ kind: "external",
59
+ }
60
+ : {
61
+ kind: "effect",
62
+ exports: (props.exports ?? {}) as any,
63
+ },
64
+ stack: { name: stack.name, stage: stack.stage },
65
+ bindings: workerBindings,
66
+ durableObjectNamespaces: Object.entries(durableObjectNamespaces).map(
67
+ ([className, namespaceId]) => ({
68
+ className,
69
+ uniqueKey: namespaceId,
70
+ sql: true,
71
+ }),
72
+ ),
73
+ });
74
+ return {
75
+ workerId: name,
76
+ workerName: name,
77
+ logpush: undefined,
78
+ url: result.address,
79
+ tags: [],
80
+ durableObjectNamespaces,
81
+ domains: [],
82
+ accountId,
83
+ } satisfies Worker["Attributes"];
84
+ });
85
+
86
+ return {
87
+ diff: () => Effect.succeed({ action: "update" }),
88
+ create: ({ id, news, bindings }) => run(id, news, bindings),
89
+ update: ({ id, news, bindings }) => run(id, news, bindings),
90
+ delete: ({ output }) => sidecar.stop(output.workerName),
91
+ };
92
+ }),
93
+ );