alchemy 0.51.1 → 0.51.3

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 (42) hide show
  1. package/lib/apply.js +1 -1
  2. package/lib/apply.js.map +1 -1
  3. package/lib/cloudflare/miniflare/miniflare-worker-options.js +2 -2
  4. package/lib/cloudflare/miniflare/miniflare-worker-options.js.map +1 -1
  5. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts +14 -14
  6. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
  7. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +123 -66
  8. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
  9. package/lib/cloudflare/miniflare/miniflare.d.ts +12 -7
  10. package/lib/cloudflare/miniflare/miniflare.d.ts.map +1 -1
  11. package/lib/cloudflare/miniflare/miniflare.js +17 -55
  12. package/lib/cloudflare/miniflare/miniflare.js.map +1 -1
  13. package/lib/cloudflare/zone.d.ts.map +1 -1
  14. package/lib/cloudflare/zone.js +2 -2
  15. package/lib/cloudflare/zone.js.map +1 -1
  16. package/lib/scope.d.ts.map +1 -1
  17. package/lib/scope.js +4 -3
  18. package/lib/scope.js.map +1 -1
  19. package/lib/state/cloudflare-state-store.d.ts +3 -1
  20. package/lib/state/cloudflare-state-store.d.ts.map +1 -1
  21. package/lib/state/cloudflare-state-store.js +11 -2
  22. package/lib/state/cloudflare-state-store.js.map +1 -1
  23. package/lib/state/instrumented-state-store.d.ts +19 -0
  24. package/lib/state/instrumented-state-store.d.ts.map +1 -0
  25. package/lib/state/instrumented-state-store.js +59 -0
  26. package/lib/state/instrumented-state-store.js.map +1 -0
  27. package/lib/util/telemetry/client.d.ts.map +1 -1
  28. package/lib/util/telemetry/client.js +14 -12
  29. package/lib/util/telemetry/client.js.map +1 -1
  30. package/lib/util/telemetry/types.d.ts +8 -3
  31. package/lib/util/telemetry/types.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/apply.ts +1 -1
  34. package/src/cloudflare/miniflare/miniflare-worker-options.ts +2 -2
  35. package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +148 -77
  36. package/src/cloudflare/miniflare/miniflare.ts +23 -79
  37. package/src/cloudflare/zone.ts +9 -8
  38. package/src/scope.ts +7 -3
  39. package/src/state/cloudflare-state-store.ts +73 -54
  40. package/src/state/instrumented-state-store.ts +97 -0
  41. package/src/util/telemetry/client.ts +14 -12
  42. package/src/util/telemetry/types.ts +18 -2
@@ -1,97 +1,168 @@
1
- import type http from "node:http";
1
+ import * as miniflare from "miniflare";
2
+ import { once } from "node:events";
3
+ import http from "node:http";
4
+ import type Stream from "node:stream";
5
+ import { Readable } from "node:stream";
2
6
  import { WebSocket, WebSocketServer } from "ws";
3
- import { HTTPServer } from "../../util/http.ts";
7
+ import { logger } from "../../util/logger.ts";
4
8
 
5
- export interface MiniflareWorkerProxyOptions {
6
- /**
7
- * Used to proxy websocket connections to the worker.
8
- * (This is a hack; used because trying to interact with `response.webSocket` doesn't work on Bun.)
9
- */
10
- getDirectURL: () => Promise<URL>;
11
- /** Used to proxy HTTP requests to the worker. */
12
- fetch: (request: Request) => Promise<Response>;
9
+ interface MiniflareWorkerProxyOptions {
10
+ name: string;
11
+ port: number;
12
+ miniflare: miniflare.Miniflare;
13
13
  }
14
14
 
15
- export class MiniflareWorkerProxy extends HTTPServer {
16
- wsServer: WebSocketServer;
17
- wsReconnectAttempts = new Map<string, number>();
15
+ export class MiniflareWorkerProxy {
16
+ private server = http.createServer();
17
+ private wss = new WebSocketServer({ noServer: true });
18
18
 
19
19
  constructor(private readonly options: MiniflareWorkerProxyOptions) {
20
- super({
21
- fetch: options.fetch,
20
+ this.server.on("upgrade", async (req, socket, head) => {
21
+ await this.handleUpgrade(req, socket, head);
22
22
  });
23
- this.wsServer = new WebSocketServer({ noServer: true });
24
- this.httpServer.on("upgrade", async (req, socket, head) => {
25
- this.wsServer.handleUpgrade(req, socket, head, async (client) => {
26
- const id = crypto.randomUUID();
27
- await this.handleUpgrade(id, client, req);
28
- });
23
+ this.server.on("request", async (req, res) => {
24
+ await this.handleRequest(req, res);
29
25
  });
26
+ this.server.listen(this.options.port);
27
+ }
28
+
29
+ get ready() {
30
+ if (!this.server.listening) {
31
+ return once(this.server, "listening");
32
+ }
33
+ return Promise.resolve();
34
+ }
35
+
36
+ get url() {
37
+ return `http://localhost:${this.options.port}`;
38
+ }
39
+
40
+ async close() {
41
+ await Promise.all([
42
+ new Promise((resolve) => this.wss.close(resolve)),
43
+ new Promise((resolve) => this.server.close(resolve)),
44
+ ]);
30
45
  }
31
46
 
32
47
  private async handleUpgrade(
33
- id: string,
34
- client: WebSocket,
35
- request: http.IncomingMessage,
48
+ req: http.IncomingMessage,
49
+ socket: Stream.Duplex,
50
+ head: Buffer,
36
51
  ) {
37
- const attempt = this.wsReconnectAttempts.get(id) ?? 0;
38
- this.wsReconnectAttempts.set(id, attempt + 1);
39
- if (attempt > 3) {
40
- client.close(1006, "Too many reconnect attempts");
52
+ const server = await this.createServerWebSocket(req);
53
+ if (!server) {
54
+ socket.destroy();
41
55
  return;
42
56
  }
43
- if (attempt > 0) {
44
- const delay = attempt * 1000;
45
- await new Promise((resolve) => setTimeout(resolve, delay));
46
- }
47
-
48
- const target = await this.options.getDirectURL();
49
- const url = new URL(request.url ?? "/", target);
50
- const headers = { ...request.headers };
51
- // All of these headers are set by the WebSocket constructor,
52
- // so if we don't delete them, the request will fail.
53
- delete headers.host;
54
- delete headers.connection;
55
- delete headers.upgrade;
56
- delete headers["sec-websocket-version"];
57
- delete headers["sec-websocket-key"];
58
- delete headers["sec-websocket-protocol"];
59
- delete headers["sec-websocket-extensions"];
60
- delete headers["sec-websocket-accept"];
61
- const server = new WebSocket(url.toString(), {
62
- protocol: request.headers["sec-websocket-protocol"],
63
- key: request.headers["sec-websocket-key"],
64
- headers,
65
- });
66
- server.on("open", () => {
67
- // Reset the reconnect attempts when the connection is established successfully.
68
- this.wsReconnectAttempts.set(id, 0);
69
- });
70
- server.on("message", (data, binary) => {
71
- client.send(data, { binary });
72
- });
73
- server.on("close", async (code, reason) => {
74
- if (code === 1006) {
75
- // When the worker hot reloads, the websocket connection is closed with this code.
76
- // Reconnecting allows the client to maintain the same connection.
77
- await this.handleUpgrade(id, client, request);
78
- return;
79
- }
80
- client.close(code, reason);
81
- });
82
- client.on("message", (data, binary) => {
83
- server.send(data, { binary });
84
- });
85
- client.on("close", (code, reason) => {
86
- this.wsReconnectAttempts.delete(id);
87
- if (server.readyState === WebSocket.OPEN) {
57
+ this.wss.handleUpgrade(req, socket, head, (client) => {
58
+ client.on("message", (event, binary) => {
59
+ server.send(event, { binary });
60
+ });
61
+ client.on("close", (code, reason) => {
88
62
  server.close(code, reason);
89
- }
63
+ });
64
+ server.on("message", (event, binary) => {
65
+ client.send(event, { binary });
66
+ });
67
+ server.on("close", (code, reason) => {
68
+ client.close(code, reason);
69
+ });
70
+ this.wss.emit("connection", client, req);
90
71
  });
91
72
  }
92
73
 
93
- async close() {
94
- this.wsServer.close();
95
- await super.close();
74
+ private async createServerWebSocket(req: http.IncomingMessage) {
75
+ const target = await this.options.miniflare.unsafeGetDirectURL(
76
+ this.options.name,
77
+ );
78
+ if (!target) {
79
+ logger.error(
80
+ `[Alchemy] Websocket connection failed: The worker "${this.options.name}" is not running.`,
81
+ );
82
+ return null;
83
+ }
84
+ const url = new URL(req.url ?? "/", target);
85
+ url.protocol = url.protocol.replace("http", "ws");
86
+ const protocols = req.headers["sec-websocket-protocol"]
87
+ ?.split(",")
88
+ .map((p) => p.trim());
89
+ const server = new WebSocket(url, protocols);
90
+ const controller = new AbortController();
91
+ return await Promise.race([
92
+ once(server, "open", { signal: controller.signal }).then(() => server),
93
+ once(server, "close", { signal: controller.signal }).then((args) => {
94
+ const [code, reason] = args as [number, string];
95
+ logger.error(
96
+ `[Alchemy] Websocket connection failed for worker "${this.options.name}": ${code} ${reason}`,
97
+ );
98
+ return null;
99
+ }),
100
+ ]).finally(() => controller.abort());
101
+ }
102
+
103
+ private async handleRequest(
104
+ req: http.IncomingMessage,
105
+ res: http.ServerResponse,
106
+ ) {
107
+ const worker = await this.options.miniflare.getWorker(this.options.name);
108
+ if (!worker) {
109
+ res.statusCode = 503;
110
+ res.end(`[Alchemy] The worker "${this.options.name}" is not running.`);
111
+ return;
112
+ }
113
+ try {
114
+ const response = await worker.fetch(toMiniflareRequest(req));
115
+ writeMiniflareResponseToNode(response, res);
116
+ } catch (rawError) {
117
+ const message =
118
+ rawError instanceof Error
119
+ ? (rawError.stack ?? rawError.message)
120
+ : String(rawError);
121
+ res.statusCode = 500;
122
+ res.end(
123
+ `[Alchemy] The worker "${this.options.name}" threw an error:\n\n${message}`,
124
+ );
125
+ }
96
126
  }
97
127
  }
128
+
129
+ const toMiniflareRequest = (req: http.IncomingMessage) => {
130
+ const method = req.method ?? "GET";
131
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
132
+ const headers = new miniflare.Headers();
133
+ for (const [key, value] of Object.entries(req.headers)) {
134
+ if (Array.isArray(value)) {
135
+ for (const v of value) {
136
+ headers.append(key, v);
137
+ }
138
+ } else if (value) {
139
+ headers.set(key, value);
140
+ }
141
+ }
142
+ const body =
143
+ ["GET", "HEAD", "OPTIONS"].includes(method) || !req.readable
144
+ ? undefined
145
+ : Readable.toWeb(req);
146
+ return new miniflare.Request(url, {
147
+ method,
148
+ headers,
149
+ body,
150
+ redirect: "manual",
151
+ duplex: body ? "half" : undefined,
152
+ });
153
+ };
154
+
155
+ const writeMiniflareResponseToNode = (
156
+ res: miniflare.Response,
157
+ out: http.ServerResponse,
158
+ ) => {
159
+ out.statusCode = res.status;
160
+ res.headers.forEach((value, key) => {
161
+ out.setHeader(key, value);
162
+ });
163
+ if (res.body) {
164
+ Readable.fromWeb(res.body).pipe(out, { end: true });
165
+ } else {
166
+ out.end();
167
+ }
168
+ };
@@ -1,13 +1,6 @@
1
- import {
2
- MiniflareCoreError,
3
- type Miniflare,
4
- type MiniflareOptions,
5
- type RemoteProxyConnectionString,
6
- type WorkerOptions,
7
- } from "miniflare";
1
+ import * as miniflare from "miniflare";
8
2
  import path from "node:path";
9
3
  import { findOpenPort } from "../../util/find-open-port.ts";
10
- import { logger } from "../../util/logger.ts";
11
4
  import {
12
5
  promiseWithResolvers,
13
6
  type PromiseWithResolvers,
@@ -24,14 +17,14 @@ import {
24
17
  } from "./remote-binding-proxy.ts";
25
18
 
26
19
  class MiniflareServer {
27
- miniflare?: Miniflare;
28
- workers = new Map<string, WorkerOptions>();
20
+ miniflare?: miniflare.Miniflare;
21
+ workers = new Map<string, miniflare.WorkerOptions>();
29
22
  workerProxies = new Map<string, MiniflareWorkerProxy>();
30
23
  remoteBindingProxies = new Map<string, RemoteBindingProxy>();
31
24
 
32
25
  stream = new WritableStream<{
33
26
  worker: MiniflareWorkerOptions;
34
- promise: PromiseWithResolvers<MiniflareWorkerProxy>;
27
+ promise: PromiseWithResolvers<{ url: string }>;
35
28
  }>({
36
29
  write: async ({ worker, promise }) => {
37
30
  try {
@@ -48,7 +41,7 @@ class MiniflareServer {
48
41
  writer = this.stream.getWriter();
49
42
 
50
43
  async push(worker: MiniflareWorkerOptions) {
51
- const promise = promiseWithResolvers<MiniflareWorkerProxy>();
44
+ const promise = promiseWithResolvers<{ url: string }>();
52
45
  const [, server] = await Promise.all([
53
46
  this.writer.write({ worker, promise }),
54
47
  promise.promise,
@@ -73,12 +66,6 @@ class MiniflareServer {
73
66
  this.miniflare.setOptions(await this.miniflareOptions()),
74
67
  );
75
68
  } else {
76
- const { Miniflare } = await import("miniflare").catch(() => {
77
- throw new Error(
78
- "Miniflare is not installed, but is required in local mode for Workers. Please run `npm install miniflare`.",
79
- );
80
- });
81
-
82
69
  // Miniflare intercepts SIGINT and exits with 130, which is not a failure.
83
70
  // No one likes to see a non-zero exit code when they Ctrl+C, so here's our workaround.
84
71
  process.on("exit", (code) => {
@@ -86,26 +73,21 @@ class MiniflareServer {
86
73
  process.exit(0);
87
74
  }
88
75
  });
89
- this.miniflare = new Miniflare(await this.miniflareOptions());
76
+ this.miniflare = new miniflare.Miniflare(await this.miniflareOptions());
90
77
  await withErrorRewrite(this.miniflare.ready);
91
78
  }
92
- const existing = this.workerProxies.get(worker.name);
93
- if (existing) {
94
- return existing;
79
+ const existingProxy = this.workerProxies.get(worker.name);
80
+ if (existingProxy) {
81
+ return existingProxy;
95
82
  }
96
- const server = new MiniflareWorkerProxy({
97
- getDirectURL: async () => {
98
- const url = await this.miniflare?.unsafeGetDirectURL(worker.name);
99
- if (!url) {
100
- throw new Error(`Worker "${worker.name}" is not running`);
101
- }
102
- return url;
103
- },
104
- fetch: this.createRequestHandler(worker.name),
83
+ const newProxy = new MiniflareWorkerProxy({
84
+ name: worker.name,
85
+ port: worker.port ?? (await findOpenPort()),
86
+ miniflare: this.miniflare,
105
87
  });
106
- this.workerProxies.set(worker.name, server);
107
- await server.listen(worker.port ?? (await findOpenPort()));
108
- return server;
88
+ this.workerProxies.set(worker.name, newProxy);
89
+ await newProxy.ready;
90
+ return newProxy;
109
91
  }
110
92
 
111
93
  private async dispose() {
@@ -125,7 +107,7 @@ class MiniflareServer {
125
107
 
126
108
  private async maybeCreateRemoteProxy(
127
109
  worker: MiniflareWorkerOptions,
128
- ): Promise<RemoteProxyConnectionString | undefined> {
110
+ ): Promise<miniflare.RemoteProxyConnectionString | undefined> {
129
111
  const bindings = buildRemoteBindings(worker);
130
112
  if (bindings.length === 0) {
131
113
  return undefined;
@@ -148,52 +130,11 @@ class MiniflareServer {
148
130
  return proxy.connectionString;
149
131
  }
150
132
 
151
- private createRequestHandler(name: string) {
152
- return async (req: Request) => {
153
- try {
154
- if (!this.miniflare) {
155
- return new Response(
156
- "[Alchemy] Miniflare is not initialized. Please try again.",
157
- {
158
- status: 503,
159
- },
160
- );
161
- }
162
- const worker = await this.miniflare?.getWorker(name);
163
- if (!worker) {
164
- return new Response(
165
- `[Alchemy] Cannot find worker "${name}". Please try again.`,
166
- {
167
- status: 503,
168
- },
169
- );
170
- }
171
- const res = await worker.fetch(req.url, {
172
- method: req.method,
173
- headers: req.headers as any,
174
- body: req.body as any,
175
- duplex: "half",
176
- redirect: "manual",
177
- });
178
- return res as unknown as Response;
179
- } catch (error) {
180
- logger.error(error);
181
- return new Response(
182
- `[Alchemy] Internal server error: ${String(error)}`,
183
- {
184
- status: 500,
185
- },
186
- );
187
- }
188
- };
189
- }
190
-
191
- private async miniflareOptions(): Promise<MiniflareOptions> {
192
- const { getDefaultDevRegistryPath } = await import("miniflare");
133
+ private async miniflareOptions(): Promise<miniflare.MiniflareOptions> {
193
134
  return {
194
135
  workers: Array.from(this.workers.values()),
195
136
  defaultPersistRoot: path.join(process.cwd(), ".alchemy/miniflare"),
196
- unsafeDevRegistryPath: getDefaultDevRegistryPath(),
137
+ unsafeDevRegistryPath: miniflare.getDefaultDevRegistryPath(),
197
138
  analyticsEngineDatasetsPersist: true,
198
139
  cachePersist: true,
199
140
  d1Persist: true,
@@ -202,6 +143,9 @@ class MiniflareServer {
202
143
  r2Persist: true,
203
144
  secretsStorePersist: true,
204
145
  workflowsPersist: true,
146
+ log: process.env.DEBUG
147
+ ? new miniflare.Log(miniflare.LogLevel.DEBUG)
148
+ : undefined,
205
149
  };
206
150
  }
207
151
  }
@@ -219,7 +163,7 @@ async function withErrorRewrite<T>(promise: Promise<T>) {
219
163
  return await promise;
220
164
  } catch (error) {
221
165
  if (
222
- error instanceof MiniflareCoreError &&
166
+ error instanceof miniflare.MiniflareCoreError &&
223
167
  error.code === "ERR_MODULE_STRING_SCRIPT"
224
168
  ) {
225
169
  throw new ExternalDependencyError();
@@ -651,14 +651,15 @@ export async function findZoneForHostname(
651
651
  const totalPages = firstPageData.result_info?.total_pages ?? 1;
652
652
 
653
653
  // Fetch remaining pages concurrently if needed
654
- const allZones = totalPages > 1
655
- ? await Promise.all([
656
- Promise.resolve(firstPageData.result),
657
- ...Array.from({ length: totalPages - 1 }, (_, i) =>
658
- fetchZonePage(i + 2).then(data => data.result)
659
- ),
660
- ]).then(results => results.flat())
661
- : firstPageData.result;
654
+ const allZones =
655
+ totalPages > 1
656
+ ? await Promise.all([
657
+ Promise.resolve(firstPageData.result),
658
+ ...Array.from({ length: totalPages - 1 }, (_, i) =>
659
+ fetchZonePage(i + 2).then((data) => data.result),
660
+ ),
661
+ ]).then((results) => results.flat())
662
+ : firstPageData.result;
662
663
 
663
664
  // Find the zone that best matches the hostname
664
665
  // We look for the longest matching zone name (most specific)
package/src/scope.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  import type { State, StateStore, StateStoreType } from "./state.ts";
16
16
  import { D1StateStore } from "./state/d1-state-store.ts";
17
17
  import { FileSystemStateStore } from "./state/file-system-state-store.ts";
18
+ import { InstrumentedStateStore } from "./state/instrumented-state-store.ts";
18
19
  import {
19
20
  createDummyLogger,
20
21
  createLoggerInstance,
@@ -201,12 +202,15 @@ export class Scope {
201
202
 
202
203
  this.stateStore =
203
204
  options.stateStore ?? this.parent?.stateStore ?? defaultStateStore;
204
- this.state = this.stateStore(this);
205
+ this.telemetryClient =
206
+ options.telemetryClient ?? this.parent?.telemetryClient!;
207
+ this.state = new InstrumentedStateStore(
208
+ this.stateStore(this),
209
+ this.telemetryClient,
210
+ );
205
211
  if (!options.telemetryClient && !this.parent?.telemetryClient) {
206
212
  throw new Error("Telemetry client is required");
207
213
  }
208
- this.telemetryClient =
209
- options.telemetryClient ?? this.parent?.telemetryClient!;
210
214
  this.dataMutex = new AsyncMutex();
211
215
  }
212
216
 
@@ -43,11 +43,20 @@ export interface CloudflareStateStoreOptions extends CloudflareApiOptions {
43
43
  * @see {@link https://alchemy.run/guides/do-state-store DOStateStore}
44
44
  */
45
45
  export class CloudflareStateStore extends StateStoreProxy {
46
- constructor(
47
- scope: Scope,
48
- private readonly options: CloudflareStateStoreOptions = {},
49
- ) {
46
+ options: CloudflareStateStoreOptions & { stateToken: Secret<string> };
47
+ constructor(scope: Scope, options: CloudflareStateStoreOptions = {}) {
50
48
  super(scope);
49
+ const stateToken =
50
+ options.stateToken ?? alchemy.secret(process.env.ALCHEMY_STATE_TOKEN);
51
+ if (!stateToken) {
52
+ throw new Error(
53
+ "Missing token for DOStateStore. Please set ALCHEMY_STATE_TOKEN in the environment or set the `stateToken` option in the DOStateStore constructor.",
54
+ );
55
+ }
56
+ this.options = {
57
+ ...options,
58
+ stateToken: stateToken,
59
+ };
51
60
  }
52
61
 
53
62
  async provision(): Promise<StateStoreProxy.Dispatch> {
@@ -87,58 +96,68 @@ export class CloudflareStateStore extends StateStoreProxy {
87
96
  }
88
97
  }
89
98
 
90
- const provision = memoize(async (options: CloudflareStateStoreOptions) => {
91
- const scriptName = options.scriptName ?? "alchemy-state-service";
92
- const token =
93
- options.stateToken ??
94
- (await alchemy.secret.env(
95
- "ALCHEMY_STATE_TOKEN",
96
- undefined,
97
- "Missing token for DOStateStore. Please set ALCHEMY_STATE_TOKEN in the environment or set the `stateToken` option in the DOStateStore constructor.",
98
- ));
99
+ const provision = memoize(
100
+ async (
101
+ options: CloudflareStateStoreOptions & {
102
+ stateToken: Secret<string>;
103
+ },
104
+ ) => {
105
+ const scriptName = options.scriptName ?? "alchemy-state-service";
106
+ const token =
107
+ options.stateToken ??
108
+ (await alchemy.secret.env(
109
+ "ALCHEMY_STATE_TOKEN",
110
+ undefined,
111
+ "Missing token for DOStateStore. Please set ALCHEMY_STATE_TOKEN in the environment or set the `stateToken` option in the DOStateStore constructor.",
112
+ ));
99
113
 
100
- const api = await createCloudflareApi(options);
101
- const [bundle, settings, subdomain] = await Promise.all([
102
- getInternalWorkerBundle("cloudflare-state-store"),
103
- getWorkerSettings(api, scriptName),
104
- getWorkerSubdomain(api, scriptName),
105
- ]);
106
- if (!settings || !settings.tags.includes(bundle.tag) || options.forceUpdate) {
107
- logger.log(
108
- `[CloudflareStateStore] ${settings ? "Updating" : "Creating"}...`,
114
+ const api = await createCloudflareApi(options);
115
+ const [bundle, settings, subdomain] = await Promise.all([
116
+ getInternalWorkerBundle("cloudflare-state-store"),
117
+ getWorkerSettings(api, scriptName),
118
+ getWorkerSubdomain(api, scriptName),
119
+ ]);
120
+ if (
121
+ !settings ||
122
+ !settings.tags.includes(bundle.tag) ||
123
+ options.forceUpdate
124
+ ) {
125
+ logger.log(
126
+ `[CloudflareStateStore] ${settings ? "Updating" : "Creating"}...`,
127
+ );
128
+ await putWorker(api, {
129
+ workerName: scriptName,
130
+ compatibilityDate: DEFAULT_COMPATIBILITY_DATE,
131
+ format: "esm",
132
+ scriptBundle: {
133
+ entrypoint: bundle.file.name,
134
+ files: [bundle.file],
135
+ hash: bundle.tag,
136
+ },
137
+ compatibilityFlags: [],
138
+ bindings: {
139
+ STORE: DurableObjectNamespace(scriptName, {
140
+ className: "Store",
141
+ sqlite: true,
142
+ }),
143
+ STATE_TOKEN: token,
144
+ },
145
+ tags: [bundle.tag],
146
+ });
147
+ }
148
+ if (!subdomain.enabled) {
149
+ await enableWorkerSubdomain(api, scriptName);
150
+ }
151
+ const url = `https://${scriptName}.${await getAccountSubdomain(api)}.workers.dev`;
152
+ await pollUntilReady(() =>
153
+ fetch(url, {
154
+ method: "HEAD",
155
+ headers: { Authorization: `Bearer ${token.unencrypted}` },
156
+ }),
109
157
  );
110
- await putWorker(api, {
111
- workerName: scriptName,
112
- compatibilityDate: DEFAULT_COMPATIBILITY_DATE,
113
- format: "esm",
114
- scriptBundle: {
115
- entrypoint: bundle.file.name,
116
- files: [bundle.file],
117
- hash: bundle.tag,
118
- },
119
- compatibilityFlags: [],
120
- bindings: {
121
- STORE: DurableObjectNamespace(scriptName, {
122
- className: "Store",
123
- sqlite: true,
124
- }),
125
- STATE_TOKEN: token,
126
- },
127
- tags: [bundle.tag],
128
- });
129
- }
130
- if (!subdomain.enabled) {
131
- await enableWorkerSubdomain(api, scriptName);
132
- }
133
- const url = `https://${scriptName}.${await getAccountSubdomain(api)}.workers.dev`;
134
- await pollUntilReady(() =>
135
- fetch(url, {
136
- method: "HEAD",
137
- headers: { Authorization: `Bearer ${token.unencrypted}` },
138
- }),
139
- );
140
- return { url, token: token.unencrypted };
141
- });
158
+ return { url, token: token.unencrypted };
159
+ },
160
+ );
142
161
 
143
162
  async function pollUntilReady(fn: () => Promise<Response>) {
144
163
  // This ensures the token is correct and the worker is ready to use.